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代码

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代码

golang 复制代码
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#代码

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毫秒。

C# 复制代码
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();

再次尝试

C# 复制代码
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问题?

我给的回答:

C# 复制代码
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,如果是1万个plc呢?如果要求Delay(15),就不能像我这样写了。但是,你看看,这样写有多么简单?!本来一个多线程并行问题,写起来很复杂,很容易写出bug,如果能像同步代码这样写,写出来性能不亚于多线程并行,逻辑简单,不容易出bug。

相关推荐
..空空的人3 天前
C++基于protobuf实现仿RabbitMQ消息队列---技术认识2
服务器·数据库·c++·网络协议·gtest·异步·protobuf
武子康6 天前
Java-194 RabbitMQ 分布式通信怎么选:SOA/Dubbo、微服务 OpenFeign、同步重试与 MQ 异步可靠性落地
大数据·分布式·微服务·消息队列·rabbitmq·dubbo·异步
闲人编程10 天前
FastAPI框架架构与设计哲学
python·架构·api·fastapi·异步·codecapsule
亚林瓜子1 个月前
Spring中的异步任务(CompletableFuture版)
java·spring boot·spring·async·future·异步
xiangji1 个月前
重构《手搓》TaskFactory带你更安全的起飞
线程池·异步·taskfactory
xiangji2 个月前
《手搓》线程池优化的追求
线程池·异步·手搓
xiangji2 个月前
《手搓》TaskFactory带你安全的起飞
异步·手搓·taskfactory
非凡的世界2 个月前
PHP 异步IO扩展包 AsyncIO v2.0.0 发布
php·异步·1024程序员节
weixin_445476682 个月前
Java并发编程——提前聊一聊CompletableFuture和相关业务场景
java·并发·异步
闲人编程2 个月前
使用Celery处理Python Web应用中的异步任务
开发语言·前端·python·web·异步·celery