我看过很多文章,说明C#中的async / await不会创建像这样的新线程:tasks are still not threads and async is not parallel。我想自己测试一下,所以我写了这段代码:
private static async Task Run(int id)
{
Console.WriteLine("Start:\t" + id + "\t" + System.Threading.Thread.CurrentThread.ManagedThreadId);
System.Threading.Thread.Sleep(500);
Console.WriteLine("Delay:\t" + id + "\t" + System.Threading.Thread.CurrentThread.ManagedThreadId);
await Task.Delay(100);
Console.WriteLine("Resume:\t" + id + "\t" + System.Threading.Thread.CurrentThread.ManagedThreadId);
System.Threading.Thread.Sleep(500);
Console.WriteLine("Exit:\t" + id + "\t" + System.Threading.Thread.CurrentThread.ManagedThreadId);
}
private static async Task Main(string[] args)
{
Console.WriteLine("Action\tid\tthread");
var task1 = Run(1);
var task2 = Run(2);
await Task.WhenAll(task1, task2);
}
令人惊讶的是,我最终得到的输出看起来像这样:
Action id thread
Start: 1 1
Delay: 1 1
Start: 2 1
Resume: 1 4 < ------ problem here
Delay: 2 1
Exit: 1 4
Resume: 2 5
Exit: 2 5
从我看来,它确实是在创建新线程,甚至允许两段代码同时运行?我需要在非线程安全的环境中使用async / await,所以我不能让它创建新线程。为什么在任务“ 2”当前正在运行时允许任务“ 1”(在Task.Delay
之后)继续执行?
我尝试将ConfigureAwait(true)
添加到所有await
中,但是它没有任何改变。
谢谢!
答案 0 :(得分:3)
第一个问题-异步/等待不会自己创建线程,这与您期望的说法有很大不同-代码使用异步/等待时永远不会创建线程。
在您的特定情况下,安排延迟返回的计时器代码是选择线程池线程来运行其余代码。请注意,如果您使用的环境具有非空的同步上下文(如链接文章中的WPF),则在延迟后继续执行的代码将返回到原始线程,并等待此时是否正在运行其他任务/主代码。>
第二点-如何确保非线程安全对象不会从其他线程访问:您真的必须知道什么操作可以触发代码在其他线程上运行。即System.Timers.Timer
可以做到这一点,即使它看起来并不像创建线程一样。在保证执行将在同一(WinForms / WPF)上继续执行或一次至少只有一个(ASP.Net)线程继续执行的环境中使用async / await可以使您基本上在那里。
如果您想要更强的保证,则可以遵循WinForms / WPF方法,该方法强制将对方法的访问权强制在同一线程(称为主UI线程)上,并提供在异步上下文中使用async / await的指南,以确保继续执行与await
版本相同的线程(并且还直接提供Invoke
功能供其他人自己管理线程)。这可能需要使用一些“线程强制”代理包装您不拥有的类。
答案 1 :(得分:2)
查看了Alexei Levenkov
的响应并阅读了this之后,如果我没有在win32 console app
中运行测试,看起来应该可以正常工作。如果我将此代码带入WinForm app
中并在按钮单击事件中调用它,则它将按预期工作,不会创建/使用其他线程。
我还发现此库解决了控制台应用程序的问题:Nito.AsyncEx
。使用Nito.AsyncEx.AsyncContext.Run
创建了我正在寻找的行为并给出了以下结果:
Action id thread
Start: 1 1
Delay: 1 1
Start: 2 1
Delay: 2 1
Resume: 1 1
Exit: 1 1
Resume: 2 1
Exit: 2 1
这样一来,总时间就比一个接一个地执行任务1和2少,但是仍然不需要我成为线程安全的。
答案 2 :(得分:1)
异步等待使用线程,如果碰巧当时所有线程池线程都处于繁忙状态,则会创建新线程。因此,说async-await不会创建线程并不完全准确。不同之处在于通常如何使用这些线程。在经典的多线程方案中,您创建新线程的目的是使它们保持一段时间。您无需启动新线程即可完成1毫秒的工作量。通常,您需要运行CPU密集型计算,或者从磁盘中读取一些大文件,或者调用一些可能需要一段时间才能响应的Web方法。在其中的一些示例中,线程实际上并没有做很多工作,它们只是在大多数时间里等待从磁盘或网络驱动程序中获取一些数据。并且在等待期间,它们消耗相当大的内存块,每个线程大约1 MB,仅出于它们自身存在的目的。
进入async-await的世界,在该世界中,很少有线程被允许通过空闲来浪费资源。这些是共享线程池中的线程,准备响应驱动程序,计算机时钟或其他位置的事件。通常,线程池线程的每个单独的工作量都很小。它可以是一毫秒甚至更少。 .NET基础结构在处理大量计划任务方面的能力令人印象深刻:
var stopwatch = Stopwatch.StartNew();
var tasks = new List<Task>();
int sum = 0;
foreach (var i in Enumerable.Range(0, 100_000))
{
tasks.Add(Task.Run(async () =>
{
await Task.Delay(i % 1000);
Interlocked.Increment(ref sum);
}));
}
Console.WriteLine($"Waiting, Elapsed: {stopwatch.ElapsedMilliseconds} msec");
await Task.WhenAll(tasks);
Console.WriteLine($"Sum: {sum}, Elapsed: {stopwatch.ElapsedMilliseconds} msec");
等待时间:296毫秒
总和:100000,经过时间:1898毫秒
.NET Framework 4.8附带。 .NET Core 3.0的性能甚至更好。
您处理任务和进行异步等待的次数越多,您对基础架构的信任就越高,并给予它越来越多的细粒度工作。例如,从文本文件中读取行。你会疯启动一个线程只是从文件中读取一行,但任务和异步等待这不是疯了所有:StreamReader.ReadLineAsync
答案 3 :(得分:0)
您正在尝试使用异步,但是最后您想像同步任务一样工作,即使您使用“睡眠”或“延迟”,也不确定该任务在那个时候会完成。
如果您不想使用异步提供的好东西,请使用标准方法或:将所有任务放在run方法上(或创建一个方法并多次调用它);使用1个线程,这样就不会锁定您的计算机,并且所有内容都将在1个线程中执行。