- A+
知乎上有人提了个问题,可惜作者已把账号注销了。
复制一下他的问题,仅讨论技术用,侵删。
问题
作者:知乎用户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; public bool IsCompleted { get; private set; } public MyAwaiter(MyTimer timer) { _timer = timer; } public void OnCompleted(Action continuation) { _continuation = continuation; } public void Run() { 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实现?
这个问题有什么实际价值?看我另一个回答:求助多线程读取大量PLC问题?
我给的回答:
for (int i = 0; i < 500; i++) { ReadPLC(i); } async void ReadPLC(int plcIndex) { while (true) { // todo: 读取PLC Console.WriteLine($"读取PLC {plcIndex}"); await Task.Delay(200); } }
还好它这只要求500个plc,间隔200毫秒也比较长,用Task.Delay完全可以,如果是1万个plc呢?如果要求Delay(15),就不能像我这样写了。但是,你看看,这样写有多么简单?!本来一个多线程并行问题,写起来很复杂,很容易写出bug,如果能像同步代码这样写,写出来性能不亚于多线程并行,逻辑简单,不容易出bug。
注意,和时间轮算法所面对的需求可能并不一样,这里是为了实现高性能的Delay,而不是为了实现简化版的Quartz。