C
C#2y ago
Dusty

TaskCompletionSource.SetResult() executes on the same thread as the waiting one [Answered]

Hey, as the title already says I've found a deadlock in my code and would like to understand it. To give you an example:
// Called from Thread Id 1
await SomeMethodAsync();
// Continues on Thread Id 5

// await Task.Yield() - fixes the issue

Console.ReadKey(); // example to block the thread

class Sample
{

Task SomeMethodAsync()
{
var tcs = new TaskCompletionSource();
queue.Enqueue(tcs);
return tcs.Task;
}

void OnMessage()
{
// Called from Thread Id 5
var tcs = queue.Dequeue();
tcs.TrySetResult(); // Blocks the whole event loop

// Do some other stuff
}

}
// Called from Thread Id 1
await SomeMethodAsync();
// Continues on Thread Id 5

// await Task.Yield() - fixes the issue

Console.ReadKey(); // example to block the thread

class Sample
{

Task SomeMethodAsync()
{
var tcs = new TaskCompletionSource();
queue.Enqueue(tcs);
return tcs.Task;
}

void OnMessage()
{
// Called from Thread Id 5
var tcs = queue.Dequeue();
tcs.TrySetResult(); // Blocks the whole event loop

// Do some other stuff
}

}
So why is the awaited method continuing on the thread that called TrySetResult ?
17 Replies
MODiX
MODiX2y ago
Enum: System.Threading.Tasks.TaskCreationOptions Specifies flags that control optional behavior for the creation and execution of tasks. Field: System.Threading.Tasks.TaskCreationOptions.None Specifies that the default behavior should be used. Field: System.Threading.Tasks.TaskCreationOptions.LongRunning Specifies that a task will be a long-running, coarse-grained operation involving fewer, larger components than fine-grained systems. It provides a hint to the TaskScheduler that oversubscription may be warranted. Oversubscription lets you create more threads than the available number of hardware threads. It also provides a hint to the task scheduler that an additional thread might be required for the task so that it does not block the forward progress of other threads or work items on the local thread-pool queue. 3/8 results shown ~ Click Here for more results
React with ❌ to remove this embed.
Xymanek
Xymanek2y ago
You want the RunContinuationsAsynchronously
Dusty
Dusty2y ago
Thanks that works But could you also explain why it continues on the thread setting the result, not the calling/currently waiting thread?
Xymanek
Xymanek2y ago
Sorry, on mobile currently
toddlahakbar
toddlahakbar2y ago
@Dusty !nothread
Dusty
Dusty2y ago
hm?
toddlahakbar
toddlahakbar2y ago
$nothread
MODiX
MODiX2y ago
There Is No Thread
This is an essential truth of async in its purest form: There is no thread.
toddlahakbar
toddlahakbar2y ago
Is why Sorry was looking for the tag syntax Because there is no thread associated with the callback
Dusty
Dusty2y ago
I know that, it just makes no sense for me why my task switches the thread to the thread which sets the result Well yea but why does it always resume on the same thread? I'd like to understand that
toddlahakbar
toddlahakbar2y ago
If you don't have multiple tasks running then almost certainly because there's no reason to create another execution thread
Dusty
Dusty2y ago
But why is there event a switch then?
Xymanek
Xymanek2y ago
likely because there is no synchronization context to pin the async method to a specific thread by default async methods can be executed on effectively any thread, without any guarantees in cases where there is a need, synchronization context and/or task scheduler is used to enforce where the task should be executed but again, you should avoid writing your async methods in a way that requires them to be executed on specific threads (where possible) as it can degrade your app's performance
Dusty
Dusty2y ago
So that means it could also be another thread from the thread pool in theory but it uses the same thread as the runtime thinks that the thread is not needed at that moment? Well I have a separate thread for my message loop which needs to run in parallel with everything else. From that message loop thread I set the result, so the solution you sent me earlier is okay or is there anything else or bad about it? The only restriction I have for that async method is that it shouldn't run on the message loop thread
Xymanek
Xymanek2y ago
it uses the same thread as the runtime thinks that the thread is not needed at that moment?
no, because by default continuations (i.e. code waiting for the task's completion) execute synchronously with the task's completion (i.e. SetResult() in your case). IF you had a sync context (e.g. how UI frameworks behave), it would've pushed the continuation to that sync context (which would've likely forwarded the execution to some other thread) nope, RunContinuationsAsynchronously is precisely made for this purpose. It means "send all continuations to the thread pool, regardless of how they are setup internally" so the only cost that you are paying (on your message loop thread) is the queuing to the TP, but that's basically free
Dusty
Dusty2y ago
Ah i see Then thank you for the explanation and solution!
Accord
Accord2y ago
✅ This post has been marked as answered!