简体   繁体   中英

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 . 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:

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. Why is it that the task "1" is allowed to resume ( after the Task.Delay ) while task "2" is currently running?

I tried adding ConfigureAwait(true) to all the await but it doesn't change anything.

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.

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. 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.

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. 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.

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). 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 . 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. I also found this library that fixed the problem for the console app: Nito.AsyncEx . Using Nito.AsyncEx.AsyncContext.Run created the behavior I was looking for and gave these results:

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.

Async-await uses threads, and if it happens that all thread-pool threads are busy at the time then new threads are created. So saying that async-await doesn't create threads is not exactly accurate. 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. 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. 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.

Enter the world of async-await, where very few threads are allowed to waste resources by being idle. 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:

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
Sum: 100000, Elapsed: 1898 msec

This is with .NET Framework 4.8. .NET Core 3.0 performs even better.

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. 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 .

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); use 1 thread so it won't lock your computer and everything will be executing in 1 thread.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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