C# golang 開10000個無限循環的性能

知乎上有人提了個問題,可惜作者已把賬號註銷了。
複製一下他的問題,僅討論技術用,侵刪。

問題

作者:知乎用戶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沒有重現了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章