Reputation: 693
Sample Code:
class Program
{
static void sleepFunc()
{
int before = Thread.CurrentThread.ManagedThreadId;
Thread.Sleep(5000);
int after = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"{before} -> sleep -> {after}");
}
static async void delayFunc()
{
int before = Thread.CurrentThread.ManagedThreadId;
await Task.Delay(5000);
int after = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"{before} -> delay -> {after}");
}
static void Main(string[] args)
{
List<Thread> threads = new List<Thread>();
for(int i = 0; i < 10; i++)
{
var thread = new Thread(sleepFunc);
thread.Start();
threads.Add(thread);
}
Thread.Sleep(1000); // just to separate the result sections
for (int i = 0; i < 10; i++)
{
var thread = new Thread(delayFunc);
thread.Start();
threads.Add(thread);
}
Console.ReadLine();
}
}
Sample Output:
3 -> sleep -> 3
7 -> sleep -> 7
4 -> sleep -> 4
5 -> sleep -> 5
6 -> sleep -> 6
8 -> sleep -> 8
9 -> sleep -> 9
10 -> sleep -> 10
11 -> sleep -> 11
12 -> sleep -> 12
21 -> delay -> 25
18 -> delay -> 37
15 -> delay -> 36
16 -> delay -> 32
19 -> delay -> 24
20 -> delay -> 27
13 -> delay -> 32
17 -> delay -> 27
22 -> delay -> 25
14 -> delay -> 26
The continuation for Thread.Sleep() runs on the same explicitly created thread, but the continuation for Task.Delay() runs on a different (thread pool) thread.
Since the continuation always runs on the thread pool, is it just pointless/wasteful/antipattern to use a new Thread(func) if there is an await anywhere inside the func? Is there any way to force a task to continue on the original non-thread-pool new thread?
I'm asking because I read someone recommending to put a long running loop (for socket communication for example) in a Thread instead of in a Task, but it seems that if they call ReadAsync then it ends up being a Task on the thread pool anyway, and the new Thread was pointless?
Upvotes: 1
Views: 114
Reputation: 70671
Since the continuation always runs on the thread pool, is it just pointless/wasteful/antipattern to use a new Thread(func) if there is an await anywhere inside the func?
Never say never. But generally, yes, it would be pointless.
Is there any way to force a task to continue on the original non-thread-pool new thread?
Yes, there is a way. create your own synchronization context and run that context in that thread. Then any await
will continue in that context (by default of course…if you call ConfigureAwait(false)
, it's allowed not to).
Creating one's own synchronization context is (should be) relatively rare. Either you're running the code in a context that naturally has its own, or you're in a situation where you really don't care what thread handles the code.
Explicit threads and async
/await
don't really go together all that well. The await
statement is a way to compose asynchronous code in a linear, semmingly-synchronous fashion. In general, if you created an explicit thread, it's because you intended for all of the work to be executed in that thread. There's no reason to even call asynchronous methods in that context.
Conversely, if you are using asynchronous methods, these by definition are executed asynchronously in a manner that is independent of whatever threading you might be using. Many asynchronous operations don't even use a thread at all! If you are using await
in your logic, then by definition you have code that runs briefly for some time, then is suspended while it waits for something to complete asynchronously. There's no reason to use an explicit thread in this sort of scenario; the thread pool is an ideal way to have your own code executed during those brief intervals when you're not waiting for something else.
I'm asking because I read someone recommending to put a long running loop (for socket communication for example) in a Thread instead of in a Task, but it seems that if they call ReadAsync then it ends up being a Task on the thread pool anyway, and the new Thread was pointless?
I/O is a classic example of where using await
is an ideal technique. A socket involves (from the CPU's point of view) long periods of time waiting for something to happen, interrupted by very, very short periods of time doing work with whatever data showed up. It has always been the case that dedicating a whole thread to reading from a socket was a bad idea. Long before .NET, there was still an asynchronous I/O model that allowed for a pool of threads to handle these intermittently completing I/O operations. See "I/O completion ports". Indeed, the .NET socket API is built on top of that API.
One could get away with the "one thread per connection" model if one had very few connections, but it scales very poorly. The older alternative, using the "select" model from the BSD socket API, does a little better, but is very inefficient. Using IOCP allows Windows to efficiently manage a few threads which are handling a large number of sockets, without dedicating too much CPU time to the I/O, nor too many threads (each thread uses a relatively large amount of resources on Windows).
Not only is the explicit thread pointless in the scenario you describe, there are other really good reasons to not do it that way anyway.
Useful additional reading:
What is the difference between task and thread?
Task vs Thread differences
Upvotes: 5