简体   繁体   English

C# 异步不创建新线程

[英]C# Async without creating a new thread

I've seen a lot of post explaining that async/await in C# do not create a new thread like this one: tasks are still not threads and async is not parallel .我看到很多帖子解释说 C# 中的 async/await 不会创建像这样的新线程: tasks are still not threads and async is not parallel I wanted to test it out myself so I wrote this code:我想自己测试一下,所以我写了这段代码:

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);
}

Surprisingly I ended up with an output that looks like this:令人惊讶的是,我最终得到了一个看起来像这样的 output:

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

From what I see, it is indeed creating new threads, and even allowing two piece of code to run concurrently?在我看来,它确实是在创建新线程,甚至允许两段代码同时运行? I need to use async/await in a non thread-safe environment so I can't let it create new threads.我需要在非线程安全的环境中使用 async/await,所以我不能让它创建新线程。 Why is it that the task "1" is allowed to resume ( after the Task.Delay ) while task "2" is currently running?为什么在任务“2”当前运行时允许任务“1”恢复(在Task.Delay之后)?

I tried adding ConfigureAwait(true) to all the await but it doesn't change anything.我尝试将ConfigureAwait(true)添加到所有await中,但它没有改变任何东西。

Thanks!谢谢!

On first question - async/await do not create threads by themselves, which is very different from what you expect this claim to be - threads never created when code uses async/await.在第一个问题上 - async/await 不会自己创建线程,这与您期望的这个声明非常不同 - 当代码使用 async/await 时从未创建线程。

In your particular case it's timer code that schedules return from delay is picking a threadpool thread to run the rest of the code.在您的特定情况下,计划从延迟返回的计时器代码正在选择一个线程池线程来运行代码的 rest。 Note that if you would use environment which has non-empty synchronization context (like WPF in linked article) then code that continue execution after delay would come back to original thread and wait if another task/main code is running at that point.请注意,如果您将使用具有非空同步上下文的环境(如链接文章中的 WPF),那么延迟后继续执行的代码将返回原始线程并等待另一个任务/主代码在该点运行。

On second point - how to ensure non-thread safe objects are not accessed from other threads: you really have to know what operation can trigger code to run on other threads.第二点 - 如何确保不从其他线程访问非线程安全对象:您确实必须知道什么操作可以触发代码在其他线程上运行。 Ie System.Timers.Timer will do that for example even if it does not look like creating threads.例如,即使它看起来不像创建线程, System.Timers.Timer也会这样做。 Using async/await in environment that guarantees that execution will continue on same (WinForms/WPF) or at least only one at a time (ASP.Net) thread can get you mostly there.在保证执行将在相同(WinForms/WPF)或一次至少一个(ASP.Net)线程上继续执行的环境中使用 async/await 可以让你大部分时间都在那里。

If you want stronger guarantees you can follow WinForms/WPF approach that on enforces access to methods to be on the same thread (known as main UI thread) and provide guidance to use async/await with synchronization context that ensures execution continues on the same thread it was await -ed (and also provides Invoke functionality directly for others to manage threading themselves).如果您想要更强的保证,您可以遵循 WinForms/WPF 方法,该方法强制对方法的访问位于同一线程(称为主 UI 线程)上,并提供使用 async/await 和同步上下文的指导,以确保在同一线程上继续执行它是await -ed(并且还直接为其他人提供Invoke功能来管理自己的线程)。 This would likely require wrapping classes you don't own with some "thread enforcement" proxies.这可能需要使用一些“线程强制”代理来包装您不拥有的类。

After looking at Alexei Levenkov response and reading this , it looks like it should be working correctly if I wasn't running my tests in a win32 console app .在查看Alexei Levenkov响应并阅读内容后,如果我没有在win32 console app中运行测试,它看起来应该可以正常工作。 If I bring this code into a WinForm app and call it inside a button click event, it works as intended, no additional thread is created / use.如果我将此代码带入WinForm app并在按钮单击事件中调用它,它会按预期工作,不会创建/使用额外的线程。 I also found this library that fixed the problem for the console app: Nito.AsyncEx .我还发现这个库可以解决控制台应用程序的问题: Nito.AsyncEx Using Nito.AsyncEx.AsyncContext.Run created the behavior I was looking for and gave these results:使用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

This way, the total time is less then running task 1 and 2 one after the other, but still doesn't requires me to become thread-safe.这样,总时间少于依次运行任务 1 和 2,但仍然不需要我成为线程安全的。

Async-await uses threads, and if it happens that all thread-pool threads are busy at the time then new threads are created. Async-await使用线程,如果碰巧所有线程池线程当时都忙,则创建新线程。 So saying that async-await doesn't create threads is not exactly accurate.所以说 async-await 不创建线程并不完全准确。 The difference is on how these threads are typically used.不同之处在于通常如何使用这些线程。 In a classic multithreaded scenario you create new threads with the intention of keeping them alive for a while.在经典的多线程场景中,您创建新线程的目的是让它们保持一段时间。 You don't start a new thread to do an 1 millisecond workload.您不会启动一个新线程来执行 1 毫秒的工作负载。 You typically run a CPU-intensive calculation, or read some big files from the disk, or call some web method that may take a while to respond.您通常会运行 CPU 密集型计算,或者从磁盘读取一些大文件,或者调用一些可能需要一段时间才能响应的 web 方法。 In some of these examples the threads doesn't actually do much work, they just wait most of the time to get some data from the disk or the network drivers.在其中一些示例中,线程实际上并没有做太多工作,它们只是等待大部分时间从磁盘或网络驱动程序获取一些数据。 And while waiting they consume a fairly big chunk of memory, around 1 MB each thread, just for the purpose of their own existence.在等待期间,他们消耗了相当大的一块 memory,每个线程大约1 MB ,只是为了他们自己的存在。

Enter the world of async-await, where very few threads are allowed to waste resources by being idle.进入 async-await 的世界,其中很少有线程可以通过空闲来浪费资源。 These are the threads of the shared thread-pool, ready to respond to events from the drivers, from the computer clock or from elsewhere.这些是共享线程池的线程,准备好响应来自驱动程序、计算机时钟或其他地方的事件。 Typically each individual workload of a thread-pool thread is fairly small.通常,线程池线程的每个单独的工作负载都相当小。 It can be one millisecond or even less.它可以是一毫秒甚至更少。 The .NET infrastructure is impressive at its ability to handle huge amounts of scheduled tasks: .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");

Waiting, Elapsed: 296 msec等待,经过:296 毫秒
Sum: 100000, Elapsed: 1898 msec总和:100000,经过:1898 毫秒

This is with .NET Framework 4.8.这是 .NET 框架 4.8。 .NET Core 3.0 performs even better. .NET Core 3.0 性能更佳。

The more you work with tasks and async-await, the more you trust the infrastructure and give it more and more fine-grained work to do.您处理任务和 async-await 的次数越多,您就越信任基础架构并为其提供越来越细粒度的工作。 Like reading lines from a text file for example.例如,从文本文件中读取行。 You would be mad to start a thread just to read a single line from a file, but with tasks and async-await it's not crazy at all: StreamReader.ReadLineAsync .你会因为仅仅从文件中读取一行而启动一个线程而发疯,但是对于任务和异步等待它一点也不疯狂: StreamReader.ReadLineAsync

You're trying to use async but at the end you want to work like a synchronous task, even if you use Sleep or Delay you're not sure the task its going to finish by that time.您正在尝试使用异步,但最终您希望像同步任务一样工作,即使您使用睡眠或延迟,您也不确定到那时任务是否会完成。

If you don't want to use good things that Asynchronous gives, use the standard way or: Place all tasks on run method or (create a method and call it from run several times);如果您不想使用异步提供的好东西,请使用标准方式或:将所有任务放在 run 方法上或(创建一个方法并从 run 调用它多次); use 1 thread so it won't lock your computer and everything will be executing in 1 thread.使用 1 个线程,因此它不会锁定您的计算机,并且所有内容都将在 1 个线程中执行。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM