0

Why ThreadPool decides to use the exact context thread which called Task.Wait?

There is the question. For some reasons I do not see a comment button neither for the question nor for any of its answers. So, I am asking a related question in a separate thread.

In the linked question there is an answer which points to the blog. According to this blog the following statement holds.

There is the code piece:

// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  // (real-world code shouldn't use HttpClient in a using block; this is just example code)
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

// My "top-level" method.
public void Button1_Click(...)
{
  var jsonTask = GetJsonAsync(...);
  textBox1.Text = jsonTask.Result;
}

And there is the deadlock occurrence explanation:

  1. The top-level method calls GetJsonAsync (within the UI/ASP.NET context).
  2. GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context).
  3. GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.
  4. GetJsonAsync awaits the Task returned by GetStringAsync. The context is captured and will be used to continue running the GetJsonAsync method later. GetJsonAsync returns an uncompleted Task, indicating that the GetJsonAsync method is not complete.
  5. The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.
  6. … Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.
  7. The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context.
  8. Deadlock. The top-level method is blocking the context thread, waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for the context to be free so it can complete.

And my question is particularly about the step 7. Why does the ThreadPool decides to take a blocked thread and wait while it will unblock to ask that thread to run the code? Why not just take any other thread?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    _"For some reasons I do not see a comment button neither for the question nor for any of its answers."_ you don't have enough reputation points, yet. If you gain some, you'll be able to comment. Should be documented somewhere in the [help] section. – Fildor Mar 30 '20 at 07:31
  • 1
    To the question: That's the default behavior, because usually, you'll want to async something away from your GUI-Thread and after, you'll want to continue on that GUI-Thread because you want to manipulate GUI Elements using the result from the async call. Mind that the example is probably purposefully written with bad code. Also mind: you can switch off this default behavior on per-call basis: [Task.ConfigureAwait(bool)](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.configureawait?view=netframework-4.8#System_Threading_Tasks_Task_1_ConfigureAwait_System_Boolean_) – Fildor Mar 30 '20 at 07:38
  • @Fildor, so the issue here is the following. ThreadPool may try to decide to use the same thread which spawned the task to continue the execution after the `await` of the task. And it may happen that the (system) configuration of the ThreadPool will be such, that it will be in a greater priority for the ThreadPool to use the same thread (i.e. context) to continue execution than to use a non-blocked thread. Do I understand the matter correctly? – gladtomeetyou Mar 30 '20 at 08:07
  • Maybe "the master" himself can explain this better, than me: [Async and Await](https://blog.stephencleary.com/2012/02/async-and-await.html) - section "Context" – Fildor Mar 30 '20 at 08:12
  • The `ThreadPool` doesn't decide anything related to this. The decisions are made by the async-await machinery. – Theodor Zoulias Mar 30 '20 at 09:28

1 Answers1

1

GetJsonAsync is not executed on an arbitrary thread pool thread. It is executed on the context thread.

As per the example code, the task GetJsonAsync was created by a button click event, which is executed on UI thread (context thread). When the task is being waited, current context (in this case the UI synchronization context) is captured. After the task is completed, the continuation is resumed on the same context.

In step 7, the task attempts to return to the UI thread, but the UI thread is blocked by .Result, waiting the task to return. So the deadlock happens.

I noticed that the referenced question was asking about ASP.NET WebApi applications. So just want to clarify some points:

ASP.Net WebAPI does have a special synchronization context, but it is different from the UI context. There's only one UI thread, so the context schedules callbacks / continuations to only the UI thread.

However, the synchronization context for ASP.Net WebAPI does not capture one single thread. ASP.Net code can run on different / arbitrary thread. The context is in charge of restoring thread data and making sure the continuations are chained together on a first come first served basis.

weichch
  • 9,306
  • 1
  • 13
  • 25
  • *When the task runs, it captures the current context*. Actually the context is captured when the task is awaited. It happens when the code hits the `await` keyword. – Theodor Zoulias Mar 30 '20 at 09:25
  • I am grateful for your time. But you did not answer my question. Why don`t we take a different thread from a ThreadPool for the method completion? Is it because the UI system was developed to have only one thread and not being able to use other ThreadPool threads to manage the UI components? – gladtomeetyou Mar 30 '20 at 09:38
  • @gladtomeetyou I think you just answered the question yourself. Yes the threading model of UI decides UI elements can only be accessed by the UI thread which is the thread creates them. The task scheduler cannot schedule code to ThreadPool because there might be UI access code needs the UI thread. However, developers would know whether the continuation needs or not the UI thread. And in the case of not, `ConfigureAwait(false)` could be used to instruct the task can resume on a thread pool thread. – weichch Mar 30 '20 at 09:44