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

10

执行一百个异步任务为什么比执行一百个线程需要更长的时间?

下面是我的测试类:

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

为什么呢?


点击 - 将任务排队到线程上。也请参阅此问题。 - Sinatr
请注意,新任务并不总是意味着新线程asyncawait通常会将任务标记为现有线程的续集 - Robert Harvey
2个回答

12

目前,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);
}
  • 修改TestMethod1以正确处理返回ActionAsync方法的新Task

  • 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)运行长时间阻塞操作,请确保在创建任务实例时使用TaskCreationOptions.LongRunning选项(它将创建一个新的底层线程而不是使用线程池ThreadPool)。
    以下也可以缓解线程池(ThreadPool)的问题(请勿使用):
    ThreadPool.SetMinThreads(500, 500);
    

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


    谢谢回复。它部分地回答了我的问题。但我仍然感到非常困惑。为什么我的代码会阻塞而你的不会呢?它们都需要相同数量的线程,不是吗?你的代码在TestMethod1中仍然会阻塞在Task.WaitAll上。我还是有点迷茫。 - Tamas Pataky
    另外,我不得不编辑我的帖子。我的method1的原始版本需要1分30秒而不是1.3秒。 - Tamas Pataky
    “TestMethod1” 只被调用一次并阻塞主线程,问题出现在 “Action” 中,许多线程被阻塞。我的实现 “ActionAsync” 没有这种阻塞,因为它使用的是不会阻塞的 “Task.WhenAll”。由于我的实现能够重复使用未被阻塞的 “ThreadPool” 线程,所以很可能使用更少的线程。 - Lukazoid
    我现在明白了。感谢解释。 - Tamas Pataky
    你在SetMinThreads部分说“不要使用这个”,那么正确的方法是什么呢?只需启动自己的线程而不使用线程池吗? - Riki

    1
    任务在线程池中执行。线程池有限数量的线程被重复使用。所有任务或请求的操作都排队并在空闲时由这些线程执行。
    假设您的线程池有10个线程,有100个任务等待执行,则会执行10个任务,而其他90个任务只是在队列中等待,直到前10个任务完成。
    在第二个测试方法中,您创建了100个专门用于其任务的线程。因此,不是同时运行10个线程,而是100个线程在执行工作。

    ThreadPool.GetMaxThreads返回1000个线程,据我所知应该没有区别。此外,您的解释仍然无法解释为什么testmethod1需要1分30秒。 - Tamas Pataky

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接