【问题标题】:Why does running a hundred async tasks take longer than running a hundred threads?为什么运行一百个异步任务比运行一百个线程花费更长的时间?
【发布时间】:2015-09-28 14:04:35
【问题描述】:

为什么运行一百个异步任务比运行一百个线程花费更长的时间?

我有以下测试类:

public class AsyncTests
{

    public void TestMethod1()
    {
        var tasks = new List<Task>();

        for (var i = 0; i < 100; i++)
        {
            var task = new Task(Action);
            tasks.Add(task);
            task.Start();
        }

        Task.WaitAll(tasks.ToArray());            
    }


    public void TestMethod2()
    {
        var threads = new List<Thread>();

        for (var i = 0; i < 100; i++)
        {
            var thread = new Thread(Action);
            threads.Add(thread);
            thread.Start();
        }

        foreach (var thread in threads)
        {
            thread.Join();
        }
    }

    private void Action()
    {
        var task1 = LongRunningOperationAsync();
        var task2 = LongRunningOperationAsync();
        var task3 = LongRunningOperationAsync();
        var task4 = LongRunningOperationAsync();
        var task5 = LongRunningOperationAsync();

        Task[] tasks = {task1, task2, task3, task4, task5};
        Task.WaitAll(tasks);
    }

    public async Task<int> LongRunningOperationAsync()
    {
        var sw = Stopwatch.StartNew();

        await Task.Delay(500);

        Debug.WriteLine("Completed at {0}, took {1}ms", DateTime.Now, sw.Elapsed.TotalMilliseconds);

        return 1;
    }
}

据我所知,TestMethod1TestMethod2 应该完全一样。一个使用 TPL,两个使用普通的普通线程。一个需要 1:30 分钟,两个需要 0.54 秒。

为什么?

【问题讨论】:

  • Click - 排队任务到线程。也请参阅this 问题。
  • 请注意,一个新任务并不总是意味着一个新线程。 asyncawait 通常会将任务作为延续 现有线程。

标签: c# multithreading async-await


【解决方案1】:

Action 方法当前正在使用Task.WaitAll(tasks) 阻塞。默认情况下使用Task 时,将使用ThreadPool 执行,这意味着您正在阻塞共享的ThreadPool 线程。

尝试以下方法,您将看到相同的性能:

  1. 添加Action的非阻塞实现,我们称之为ActionAsync

    private Task ActionAsync()
    {
        var task1 = LongRunningOperationAsync();
        var task2 = LongRunningOperationAsync();
        var task3 = LongRunningOperationAsync();
        var task4 = LongRunningOperationAsync();
        var task5 = LongRunningOperationAsync();
    
        Task[] tasks = {task1, task2, task3, task4, task5};
        return Task.WhenAll(tasks);
    }
    
  2. 修改TestMethod1以正确处理新的Task返回ActionAsync方法

    public void TestMethod1()
    {
        var tasks = new List<Task>();
    
        for (var i = 0; i < 100; i++)
        {
            tasks.Add(Task.Run(new Func<Task>(ActionAsync)));
        }
    
        Task.WaitAll(tasks.ToArray());            
    }
    

性能缓慢的原因是ThreadPool 会在需要时“缓慢”生成新线程,如果您阻塞了它可用的少数线程,您会遇到明显的减速。这就是为什么ThreadPool 仅适用于运行短任务的原因。

如果您打算使用Task 运行长时间阻塞操作,请确保在创建Task 实例时使用TaskCreationOptions.LongRunning(这将创建一个新的底层Thread 而不是使用ThreadPool) .

ThreadPool 是问题的一些进一步证据,以下也可以缓解您的问题(不要使用它):

ThreadPool.SetMinThreads(500, 500);

这表明新 ThreadPool 线程的“缓慢”生成导致了您的瓶颈。

【讨论】:

  • 感谢您的回复。它部分回答了我的问题。不过我还是很疑惑。为什么我的代码被阻止而你的没有?它们都需要相同数量的线程,不是吗?您的代码仍然阻塞在 TestMethod1 中的 Task.WaitAll 上。我还是有点失落
  • 另外,我必须编辑我的帖子。我的方法 1 的原始版本需要 1:30 分钟而不是 1.3 秒。
  • TestMethod1 仅被调用一次并阻塞主线程,问题出现在Action 中,其中许多线程被阻塞,我的实现ActionAsync 没有这种阻塞,因为它正在使用@ 987654346@ 不会阻塞。我的实现很可能使用较少的线程,因为它能够重用 ThreadPool 线程,因为它们不再被阻塞。
  • 我想我现在看到了不同之处。谢谢解释
  • 您在 SetMinThreads 部分中说过“不要使用它”,但是正确的方法是什么?只启动我自己的线程而不使用线程池?
【解决方案2】:

任务在线程池中的线程上执行。线程池作为可重复使用的有限数量的线程。所有任务或所有请求的操作都会在这些线程空闲时排队并执行。

假设您的线程池有 10 个线程,并且您有 100 个任务在等待,然后执行 10 个任务,而其他 90 个任务只是在队列中等待,直到前 10 个任务完成。

在第二个测试方法中,您创建了 100 个专用于其任务的线程。所以不是 10 个线程同时运行,而是 100 个线程在做这项工作。

【讨论】:

  • ThreadPool.GetMaxThreads 返回 1000 个线程,据我所知应该没有区别。此外,您的解释仍然无法解释为什么 testmethod1 需要 1:30 分钟
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-10-15
  • 2018-09-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-25
  • 1970-01-01
相关资源
最近更新 更多