";“新线程”;和Async

本文关键字:新线程 Async 线程 quot | 更新日期: 2023-09-27 18:28:35

我用下面的代码做了一个模拟并发的示例:

var threads = new Thread[200]; 
//starting threads logic
for (int i = 0; i < 200; i++)
         {
             threads[i].Start();
         }
         for (int i = 0; i < 200; i++)
         {
             threads[i].Join();
         } 

该代码应该向数据库中插入数千条记录,而且它似乎工作得很好,因为线程几乎同时完成。

但是,当我使用时

 var tasks = new List<Task<int>>();
        for (int i = 0; i < 200; i++)
        {
            tasks.Add(insert(i));
            //   await insert(i);
        }
        int[] result = await Task.WhenAll(tasks);

完成同样的逻辑需要很多时间。

有人能向我解释一下有什么区别吗?我认为Await应该创建线程。

";“新线程”;和Async

如果需要复制最初基于Thread的行为,可以使用Task.Factory.StartNew(... , TaskCreationOptions.LongRunning)来安排工作,然后通过Task.WaitAll阻止,直到工人任务完成。我不推荐这种方法,但就行为而言,这将非常接近您的代码以前的工作方式。

关于为什么在您的场景中可能无法获得预期性能的更深入分析如下:

解释,第1部分async并不意味着"在不同的线程上")

async关键字标记的方法不会神奇地异步运行。它们只是能够将不可用的操作(它们本身可能异步运行,也可能不异步运行)组合成一个更大的单元(通常为TaskTask<T>)。

如果您的insert方法是async,那么它仍然可能同步执行至少某些工作。第一个await语句之前的所有代码肯定会出现这种情况。这项工作将在"主"线程(调用insert的线程)上执行,这将是您的瓶颈或至少是瓶颈的一部分,因为当您在紧密循环中调用insert时,无论您是否await生成的任务,代码的该部分的并行度都将为1。

为了说明上述观点,请考虑以下示例:

void Test()
{
    Debug.Print($"Kicking off async chain (thread {Thread.CurrentThread.ManagedThreadId}) - this is the main thread");
    OuterTask().Wait(); // Do not block on Tasks - educational purposes only.
}
async Task OuterTask()
{
    Debug.Print($"OuterTask before await (thread {Thread.CurrentThread.ManagedThreadId})");
    await InnerTask().ConfigureAwait(false);
    Debug.Print($"OuterTask after await (thread {Thread.CurrentThread.ManagedThreadId})");
}
async Task InnerTask()
{
    Debug.Print($"InnerTask before await (thread {Thread.CurrentThread.ManagedThreadId})");
    await Task.Delay(10).ConfigureAwait(false);
    Debug.Print($"InnerTask after await (thread {Thread.CurrentThread.ManagedThreadId}) - we are now on the thread pool");
}

这会产生以下输出:

启动异步链(线程6)-这是主线程等待之前的OuterTask(线程6)等待之前的InnerTask(线程6)等待后的InnerTask(线程8)-我们现在在线程池上等待后的OuterTask(线程8)

请注意,Task1甚至Task2中第一个await之前的代码仍然在"主"线程上执行。我们的链实际上是同步执行的,在启动外部任务的同一个线程上,直到我们await第一个真正的异步操作(在本例中为Task.Delay)。

此外

如果您在SynchronizationContext.Current不为null的环境中运行(即Windows窗体、WPF),并且您没有在insert方法中等待的任务上使用ConfigureAwait(false),则异步状态机第一条await语句之后调度的延续也将可能在"主"线程上执行,尽管这在某些环境(即ASP.NET)中无法保证

说明,第2部分(在线程池上执行Task s)

如果作为insert方法的一部分,您选择手动启动任何Task,那么您很可能是通过使用Task.Run启动不指定TaskCreationOptions.LongRunning的新任务的任何其他方法来安排线程池上的工作。一旦线程池饱和,任何新启动的任务都将排队,从而降低并行系统的吞吐量。

证明:

IEnumerable<Task> tasks = Enumerable
    .Range(0, 200)
    .Select(_ => Task.Run(() => Thread.Sleep(100))); // Using Thread.Sleep to simulate blocking calls.
await Task.WhenAll(tasks); // Completes in 2+ seconds.

现在使用TaskCreationOptions.LongRunning:

IEnumerable<Task> tasks = Enumerable
    .Range(0, 200)
    .Select(_ => Task.Factory.StartNew(
        () => Thread.Sleep(100), TaskCreationOptions.LongRunning
    ));
await Task.WhenAll(tasks); // Completes in under 130 milliseconds.

通常生成200个线程不是一个好主意(这不会很好地扩展),但如果阻塞调用的大规模并行化是一个绝对的要求,那么上面的片段向您展示了一种使用TPL的方法。

在第一个示例中,您手动创建了线程。在第二秒内,您创建了任务。任务可能正在使用线程池,其中存在有限数量的线程。因此,大多数任务都在队列中等待,而很少有任务在可用线程上并行执行。