C# golang 开10000个无限循环的性能

  • C# golang 开10000个无限循环的性能已关闭评论
  • 76 次浏览
  • A+
所属分类:.NET技术
摘要

知乎上有人提了个问题,可惜作者已把账号注销了。
复制一下他的问题,仅讨论技术用,侵删。

知乎上有人提了个问题,可惜作者已把账号注销了。
复制一下他的问题,仅讨论技术用,侵删。

问题

作者:知乎用户fLP2gX
链接:https://www.zhihu.com/question/634840187/answer/3328710757
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最近遇见个需求,需要开2000个线程无限循环,每个循环有sleep(1),这个在其他语言很容易实现,在c#中就很难了,我试过task.delay(1)直接一秒钟10次gc。今天有空测试下多种语言的协程,都是开10000个协程无限循环,中间有个sleep(15ms), cpu使用率rust 40%,golang 3%,c# 16%, 都是release,把我搞不自信了。cpu是11代i5 ,rust的开销简直无法忍受。为了严谨测试了系统线程,cpu使用率43%

rust代码

static NUM: i64 = 0; async fn fff() {     let t = tokio::time::Duration::from_millis(15);     loop {         tokio::time::sleep(t).await;         if NUM > 1000 {             println!("大于");         }     } } #[tokio::main] async fn main() {     let mut i = 0;     while i < 10000 {         tokio::task::spawn(fff());         i = i + 1;     }     println!("over");     let mut s = String::new();     std::io::stdin().read_line(&mut s).unwrap(); } 

go代码

package main import ( 	"fmt" 	"time" ) var AAA int func fff() { 	for { 		time.Sleep(time.Millisecond * 15) 		if AAA > 10000 { 			fmt.Println("大于") 		} 	} } func main() { 	for i := 0; i < 10000; i++ { 		go fff() 	} 	fmt.Println("begin") 	var s string 	fmt.Scanln(&s) } 

c#代码

internal class Program {     static Int64 num = 0;     static async void fff()     {         while (true)         {             await Task.Delay(15);             if (num > 100000)                 Console.WriteLine("大于");         }     }     static void Main()     {         for (int i = 0; i < 10000; i++)             fff();         Console.WriteLine("begin");         Console.ReadLine();     } } 

我的测试

我使用Task.Delay测试,发现速度只有30多万次/秒,然后CPU占用达到30%。
然后我又上网了找了一个时间轮算法HashedWheelTimer,使用它的Delay,经过调参,速度可以达到50多万次/秒,达到了题主的要求,但CPU占用依然高达30%。我不知道是不是我找的这个HashedWheelTimer写的不好。

我的尝试

如下代码勉强达到了题主的要求,速度可以达到50多万次/秒,CPU占用8%,比go的3%要高一些,但比用Task.Delay要好很多了。但有个缺点,就是任务延迟可能会高达500毫秒。

int num = 0;  async void func(int i) {     int n = 25; // 无延迟干活次数     int m = 1; // 干n次活,m次延迟干活     int t = 500; // 延迟干活时间,根据具体业务设置可以接受的延迟时间     long count = 0;     while (true)     {         if (count < n)         {             await Task.CompletedTask;         }         else if (count < n + m)         {             await Task.Delay(t); // 循环执行了若干次,休息一会,把机会让给其它循环,毕竟CPU就那么多         }         else         {             count = 0;         }         count++;          Interlocked.Increment(ref num); // 干活     } }  for (int i = 0; i < 10000; i++) {     func(i); }  _ = Task.Factory.StartNew(() => {     Stopwatch sw = Stopwatch.StartNew();     while (true)     {         Thread.Sleep(5000);         double speed = num / sw.Elapsed.TotalSeconds;         Console.WriteLine($"10000个循环干活总速度={speed:#### ####.0} 次/秒");     } }, TaskCreationOptions.LongRunning);  Console.WriteLine("begin"); Console.ReadLine(); 

再次尝试

using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices;  int num = 0; MyTimer myTimer = new MyTimer(15, 17000);  async void func(int i) {     while (true)     {         await myTimer.Delay();         // Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff} - {i}");          Interlocked.Increment(ref num); // 干活     } }  for (int i = 0; i < 10000; i++) {     func(i); }  _ = Task.Factory.StartNew(() => {     Stopwatch sw = Stopwatch.StartNew();     while (true)     {         Thread.Sleep(5000);         double speed = num / sw.Elapsed.TotalSeconds;         Console.WriteLine($"10000个循环干活总速度={speed:#### ####.0} 次/秒");     } }, TaskCreationOptions.LongRunning);  Console.WriteLine("开始"); Console.ReadLine(); myTimer.Dispose();  class MyTimer : IDisposable {     private int _interval;     private Thread _thread;     private bool _threadRunning = false;     private ConcurrentQueue<MyAwaiter> _queue;      /// <summary>     /// Timer     /// </summary>     /// <param name="interval">时间间隔</param>     /// <param name="parallelCount">并行数量,建议小于或等于循环次数</param>     public MyTimer(int interval, int parallelCount)     {         _interval = interval;         _queue = new ConcurrentQueue<MyAwaiter>();         _threadRunning = true;          _thread = new Thread(() =>         {             while (_threadRunning)             {                 for (int i = 0; i < parallelCount; i++)                 {                     if (_queue.TryDequeue(out MyAwaiter myAwaiter))                     {                         myAwaiter.Run();                     }                 }                  Thread.Sleep(_interval);             }         });         _thread.Start();     }      public MyAwaiter Delay()     {         MyAwaiter awaiter = new MyAwaiter(this);         _queue.Enqueue(awaiter);         return awaiter;     }      public void Dispose()     {         _threadRunning = false;     } }  class MyAwaiter : INotifyCompletion {     private MyTimer _timer;      private Action _continuation;      private object _lock = new object();      public bool IsCompleted { get; private set; }      public MyAwaiter(MyTimer timer)     {         _timer = timer;     }      public void OnCompleted(Action continuation)     {         lock (_lock)         {             _continuation = continuation;             if (IsCompleted)             {                 _continuation.Invoke();             }         }     }      public void Run()     {         lock (_lock)         {             IsCompleted = true;             _continuation?.Invoke();         }     }      public MyAwaiter GetAwaiter()     {         return this;     }      public object GetResult()     {         return null;     }  } 

时间轮算法有点难写,我还没有掌握,换了一种写法,达到了题主的要求,速度可以达到50多万次/秒,CPU占用3%。但有缺点,MyTimer用完需要Dispose,有个并行度参数parallelCount需要根据测试代码中for循环次数设置,设置为for循环次数的1.7倍,这个参数很讨厌,再一个就是Delay时间设置了15毫秒,但是不精确,实际任务延迟可能会超出15毫秒,或者小于15毫秒,当然这里假设计时器是精确的,实际上计时器误差可能到达10毫秒,这里认为它是精确无误差的,在这个前提下,任务执行间隔不精确,但比上次尝试,最大延迟500毫秒应该要好很多。
本人水平有限,写的匆忙,但我感觉这个问题还是很重要的。问题简单来说就是大量Task.Delay会导致性能问题,有没有更高效的Delay实现?
注意,和时间轮算法所面对的需求可能并不一样,这里是为了实现高性能的Delay,而不是为了实现简化版的Quartz。

自己写复杂的东西很容易写出事

有一段代码我是这么写的:

public void OnCompleted(Action continuation) {     _continuation = continuation; }  public void Run() {     IsCompleted = true;     _continuation?.Invoke(); } 

然后在某些场景下测试就出事了,我默默改成了:

public void OnCompleted(Action continuation) {     _continuation = continuation;     if (IsCompleted)     {         Interlocked.Exchange(ref _continuation, null)?.Invoke();     } }  public void Run() {     IsCompleted = true;     Interlocked.Exchange(ref _continuation, null)?.Invoke(); } 

但是这个代码,在特定场景下依然不靠谱,测试代码部分改动如下:

int num = 0; MyTimer myTimer = new MyTimer(15, 2000); ThreadPool.SetMaxThreads(2300, 2300); ThreadPool.SetMinThreads(2200, 2200);  async void func(int i) {     while (true)     {         await myTimer.Delay();         // Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff} - {i}");          await Task.Run(() =>         {             Thread.Sleep(100);             Interlocked.Increment(ref num); // 干活         });     } }  for (int i = 0; i < 2000; i++) {     func(i); } 

然后又出事了,await myTimer.Delay()换成Task.Delay(15)就正常。但是我就不知道原因了。在测试过程中,有一些循环跑着跑着就停止了,我抄的大牛写的代码,按说应该不存在线程安全问题了吧,但还是不行。
Task.Delay靠谱是靠谱,但是CPU占用高,因为每一个Delay都会创建一个Timer,大量Delay会占用较多CPU。
再改一下:

public void OnCompleted(Action continuation) {     lock (_lock)     {         _continuation = continuation;         if (IsCompleted)         {             _continuation.Invoke();         }     } }  public void Run() {     lock (_lock)     {         IsCompleted = true;         _continuation?.Invoke();     } } 

bug没有重现了。

再思考

感觉异步的使用还是要结合具体问题具体分析。异步主要用于IO密集型任务,用来提高吞吐量。但总有人拿他来测试CPU密集型任务,然后问出很diao钻的问题。