翼度科技»论坛 编程开发 .net 查看内容

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

7

主题

7

帖子

21

积分

新手上路

Rank: 1

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

作者:知乎用户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代码
  1. static NUM: i64 = 0;
  2. async fn fff() {
  3.     let t = tokio::time::Duration::from_millis(15);
  4.     loop {
  5.         tokio::time::sleep(t).await;
  6.         if NUM > 1000 {
  7.             println!("大于");
  8.         }
  9.     }
  10. }
  11. #[tokio::main]
  12. async fn main() {
  13.     let mut i = 0;
  14.     while i < 10000 {
  15.         tokio::task::spawn(fff());
  16.         i = i + 1;
  17.     }
  18.     println!("over");
  19.     let mut s = String::new();
  20.     std::io::stdin().read_line(&mut s).unwrap();
  21. }
复制代码
go代码
  1. package main
  2. import (
  3.         "fmt"
  4.         "time"
  5. )
  6. var AAA int
  7. func fff() {
  8.         for {
  9.                 time.Sleep(time.Millisecond * 15)
  10.                 if AAA > 10000 {
  11.                         fmt.Println("大于")
  12.                 }
  13.         }
  14. }
  15. func main() {
  16.         for i := 0; i < 10000; i++ {
  17.                 go fff()
  18.         }
  19.         fmt.Println("begin")
  20.         var s string
  21.         fmt.Scanln(&s)
  22. }
复制代码
c#代码
  1. internal class Program
  2. {
  3.     static Int64 num = 0;
  4.     static async void fff()
  5.     {
  6.         while (true)
  7.         {
  8.             await Task.Delay(15);
  9.             if (num > 100000)
  10.                 Console.WriteLine("大于");
  11.         }
  12.     }
  13.     static void Main()
  14.     {
  15.         for (int i = 0; i < 10000; i++)
  16.             fff();
  17.         Console.WriteLine("begin");
  18.         Console.ReadLine();
  19.     }
  20. }
复制代码
我的测试

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

如下代码勉强达到了题主的要求,速度可以达到50多万次/秒,CPU占用8%,比go的3%要高一些,但比用Task.Delay要好很多了。但有个缺点,就是任务延迟可能会高达500毫秒。
  1. int num = 0;
  2. async void func(int i)
  3. {
  4.     int n = 25; // 无延迟干活次数
  5.     int m = 1; // 干n次活,m次延迟干活
  6.     int t = 500; // 延迟干活时间,根据具体业务设置可以接受的延迟时间
  7.     long count = 0;
  8.     while (true)
  9.     {
  10.         if (count < n)
  11.         {
  12.             await Task.CompletedTask;
  13.         }
  14.         else if (count < n + m)
  15.         {
  16.             await Task.Delay(t); // 循环执行了若干次,休息一会,把机会让给其它循环,毕竟CPU就那么多
  17.         }
  18.         else
  19.         {
  20.             count = 0;
  21.         }
  22.         count++;
  23.         Interlocked.Increment(ref num); // 干活
  24.     }
  25. }
  26. for (int i = 0; i < 10000; i++)
  27. {
  28.     func(i);
  29. }
  30. _ = Task.Factory.StartNew(() =>
  31. {
  32.     Stopwatch sw = Stopwatch.StartNew();
  33.     while (true)
  34.     {
  35.         Thread.Sleep(5000);
  36.         double speed = num / sw.Elapsed.TotalSeconds;
  37.         Console.WriteLine($"10000个循环干活总速度={speed:#### ####.0} 次/秒");
  38.     }
  39. }, TaskCreationOptions.LongRunning);
  40. Console.WriteLine("begin");
  41. Console.ReadLine();
复制代码
再次尝试
  1. using System.Collections.Concurrent;
  2. using System.Diagnostics;
  3. using System.Runtime.CompilerServices;
  4. int num = 0;
  5. MyTimer myTimer = new MyTimer(15, 17000);
  6. async void func(int i)
  7. {
  8.     while (true)
  9.     {
  10.         await myTimer.Delay();
  11.         // Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff} - {i}");
  12.         Interlocked.Increment(ref num); // 干活
  13.     }
  14. }
  15. for (int i = 0; i < 10000; i++)
  16. {
  17.     func(i);
  18. }
  19. _ = Task.Factory.StartNew(() =>
  20. {
  21.     Stopwatch sw = Stopwatch.StartNew();
  22.     while (true)
  23.     {
  24.         Thread.Sleep(5000);
  25.         double speed = num / sw.Elapsed.TotalSeconds;
  26.         Console.WriteLine($"10000个循环干活总速度={speed:#### ####.0} 次/秒");
  27.     }
  28. }, TaskCreationOptions.LongRunning);
  29. Console.WriteLine("开始");
  30. Console.ReadLine();
  31. myTimer.Dispose();
  32. class MyTimer : IDisposable
  33. {
  34.     private int _interval;
  35.     private Thread _thread;
  36.     private bool _threadRunning = false;
  37.     private ConcurrentQueue<MyAwaiter> _queue;
  38.     /// <summary>
  39.     /// Timer
  40.     /// </summary>
  41.     /// <param name="interval">时间间隔</param>
  42.     /// <param name="parallelCount">并行数量,建议小于或等于循环次数</param>
  43.     public MyTimer(int interval, int parallelCount)
  44.     {
  45.         _interval = interval;
  46.         _queue = new ConcurrentQueue<MyAwaiter>();
  47.         _threadRunning = true;
  48.         _thread = new Thread(() =>
  49.         {
  50.             while (_threadRunning)
  51.             {
  52.                 for (int i = 0; i < parallelCount; i++)
  53.                 {
  54.                     if (_queue.TryDequeue(out MyAwaiter myAwaiter))
  55.                     {
  56.                         myAwaiter.Run();
  57.                     }
  58.                 }
  59.                 Thread.Sleep(_interval);
  60.             }
  61.         });
  62.         _thread.Start();
  63.     }
  64.     public MyAwaiter Delay()
  65.     {
  66.         MyAwaiter awaiter = new MyAwaiter(this);
  67.         _queue.Enqueue(awaiter);
  68.         return awaiter;
  69.     }
  70.     public void Dispose()
  71.     {
  72.         _threadRunning = false;
  73.     }
  74. }
  75. class MyAwaiter : INotifyCompletion
  76. {
  77.     private MyTimer _timer;
  78.     private Action _continuation;
  79.     private object _lock = new object();
  80.     public bool IsCompleted { get; private set; }
  81.     public MyAwaiter(MyTimer timer)
  82.     {
  83.         _timer = timer;
  84.     }
  85.     public void OnCompleted(Action continuation)
  86.     {
  87.         lock (_lock)
  88.         {
  89.             _continuation = continuation;
  90.             if (IsCompleted)
  91.             {
  92.                 _continuation.Invoke();
  93.             }
  94.         }
  95.     }
  96.     public void Run()
  97.     {
  98.         lock (_lock)
  99.         {
  100.             IsCompleted = true;
  101.             _continuation?.Invoke();
  102.         }
  103.     }
  104.     public MyAwaiter GetAwaiter()
  105.     {
  106.         return this;
  107.     }
  108.     public object GetResult()
  109.     {
  110.         return null;
  111.     }
  112. }
复制代码
时间轮算法有点难写,我还没有掌握,换了一种写法,达到了题主的要求,速度可以达到50多万次/秒,CPU占用3%。但有缺点,MyTimer用完需要Dispose,有个并行度参数parallelCount需要根据测试代码中for循环次数设置,设置为for循环次数的1.7倍,这个参数很讨厌,再一个就是Delay时间设置了15毫秒,但是不精确,实际任务延迟可能会超出15毫秒,或者小于15毫秒,当然这里假设计时器是精确的,实际上计时器误差可能到达10毫秒,这里认为它是精确无误差的,在这个前提下,任务执行间隔不精确,但比上次尝试,最大延迟500毫秒应该要好很多。
本人水平有限,写的匆忙,但我感觉这个问题还是很重要的。问题简单来说就是大量Task.Delay会导致性能问题,有没有更高效的Delay实现?
注意,和时间轮算法所面对的需求可能并不一样,这里是为了实现高性能的Delay,而不是为了实现简化版的Quartz。
自己写复杂的东西很容易写出事

有一段代码我是这么写的:
  1. public void OnCompleted(Action continuation)
  2. {
  3.     _continuation = continuation;
  4. }
  5. public void Run()
  6. {
  7.     IsCompleted = true;
  8.     _continuation?.Invoke();
  9. }
复制代码
然后在某些场景下测试就出事了,我默默改成了:
  1. public void OnCompleted(Action continuation)
  2. {
  3.     _continuation = continuation;
  4.     if (IsCompleted)
  5.     {
  6.         Interlocked.Exchange(ref _continuation, null)?.Invoke();
  7.     }
  8. }
  9. public void Run()
  10. {
  11.     IsCompleted = true;
  12.     Interlocked.Exchange(ref _continuation, null)?.Invoke();
  13. }
复制代码
但是这个代码,在特定场景下依然不靠谱,测试代码部分改动如下:
  1. int num = 0;
  2. MyTimer myTimer = new MyTimer(15, 2000);
  3. ThreadPool.SetMaxThreads(2300, 2300);
  4. ThreadPool.SetMinThreads(2200, 2200);
  5. async void func(int i)
  6. {
  7.     while (true)
  8.     {
  9.         await myTimer.Delay();
  10.         // Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff} - {i}");
  11.         await Task.Run(() =>
  12.         {
  13.             Thread.Sleep(100);
  14.             Interlocked.Increment(ref num); // 干活
  15.         });
  16.     }
  17. }
  18. for (int i = 0; i < 2000; i++)
  19. {
  20.     func(i);
  21. }
复制代码
然后又出事了,await myTimer.Delay()换成Task.Delay(15)就正常。但是我就不知道原因了。在测试过程中,有一些循环跑着跑着就停止了,我抄的大牛写的代码,按说应该不存在线程安全问题了吧,但还是不行。
Task.Delay靠谱是靠谱,但是CPU占用高,因为每一个Delay都会创建一个Timer,大量Delay会占用较多CPU。
再改一下:
  1. public void OnCompleted(Action continuation)
  2. {
  3.     lock (_lock)
  4.     {
  5.         _continuation = continuation;
  6.         if (IsCompleted)
  7.         {
  8.             _continuation.Invoke();
  9.         }
  10.     }
  11. }
  12. public void Run()
  13. {
  14.     lock (_lock)
  15.     {
  16.         IsCompleted = true;
  17.         _continuation?.Invoke();
  18.     }
  19. }
复制代码
bug没有重现了。
再思考

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

来源:https://www.cnblogs.com/s0611163/p/17970369
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具