1

I would like to preface this question with the following:

  1. I'm familiar with the IAsyncStateMachine implementation that the await keyword in C# generates.

  2. My question is not about the basic flow of control that ensures when you use the async and await keywords.

Assumption A

The default threading behaviour in any threading environment, whether it be at the Windows operating system level or in POSIX systems or in the .NET thread pool, has been that when a thread makes a request for an I/O bound operation, say for a disk read, it issues the request to the disk device driver and enters a waiting state. Of course, I am glossing over the details because they are not of moment to our discussion.

Importantly, that thread can do nothing useful until it is unblocked by an interrupt from the device driver notifying it of completion. During this time, the thread remains on the wait queue and cannot be re-used for any other work.

I would first like a confirmation of the above description.

Assumption B

Secondly, even with the introduction of TPL, and its enhancements done in v4.5 of the .NET framework, and with the language level support for asynchronous operations involving tasks, this default behaviour described in Assumption A has not changed.

Question

Then, I'm at a loss trying to reconcile Assumptions A and B with the claim that suddenly emerged in all TPL literature that:

When the, say, main thread, starts this request for this I/O bound work, it immediately returns and continues executing the rest of the queued up messages in the message pump.

Well, what makes that thread return back to do other work? Isn't that thread supposed to be in the waiting state in the wait queue?

You might be tempted to reply that the code in the state machine launches the task awaiter and if the awaiter hasn't completed, the main thread returns.

That beggars the question -- what thread does the awaiter run on?

And the answer that springs up to mind is: whatever the implementation of the method be, of whose task it is awaiting.

That drives us down the rabbit hole further until we reach the last of such implementations that actually delivers the I/O request.

Where is that part of the source code in the .NET framework that changes this underlying fundamental mechanism about how threads work?

Side Note

While some blocking asynchronous methods such as WebClient.DownloadDataTaskAsync, if one were to follow their code through their (the method's and not one's own) oval tract into their intestines, one would see that they ultimately either execute the download synchronously, blocking the current thread if the operation was requested to be performed synchronously (Task.RunSynchronously()) or if requested asynchronously, they offload the blocking I/O bound call to a thread pool thread using the Asynchronous Programming Model (APM) Begin and End methods.

This surely will cause the main thread to return immediately because it just offloaded blocking I/O work to a thread pool thread, thereby adding approximately diddlysquat to the application's scalability.

But this was a case where, within the bowels of the beast, the work was secretly offloaded to a thread pool thread. In the case of an API that doesn't do that, say an API that looks like this:

public async Task<string> GetDataAsync()
{
  var tcs = new TaskCompletionSource<string>();

  // If GetDataInternalAsync makes the network request
  // on the same thread as the calling thread, it will block, right?
  // How then do they claim that the thread will return immediately?
  // If you look inside the state machine, it just asks the TaskAwaiter
  // if it completed the task, and if it hasn't it registers a continuation
  // and comes back. But that implies that the awaiter is on another thread
  // and that thread is happily sleeping until it gets a kick in the butt
  // from a wait handle, right?
  // So, the only way would be to delegate the making of the request
  // to a thread pool thread, in which case, we have not really improved
  // scalability but only improved responsiveness of the main/UI thread
  var s = await GetDataInternalAsync();

  tcs.SetResult(s); // omitting SetException and 
                    // cancellation for the sake of brevity

  return tcs.Task;
}

Please be gentle with me if my question appears to be nonsensical. The extent of knowledge of things in almost all matters is limited. I am just learning anything.

Water Cooler v2
  • 32,724
  • 54
  • 166
  • 336
  • 2
    I think bodangly's done a good job, but for completeness, here's a link to the man page for the [POSIX Async IO](http://man7.org/linux/man-pages/man7/aio.7.html) (within Linux), demonstrating again how broken Assumption A is. – Damien_The_Unbeliever Jun 09 '16 at 06:54
  • @Damien_The_Unbeliever Wow! Thank you. That did it for me. – Water Cooler v2 Jun 09 '16 at 06:57
  • Also see my answer here: http://stackoverflow.com/questions/37419572/if-async-await-doesnt-create-any-additional-threads-then-how-does-it-make-appl/37419845#37419845 – Lasse V. Karlsen Jun 09 '16 at 08:32

1 Answers1

2

When you are talking about an async I/O operation, the truth, as pointed out here by Stephen Cleary (http://blog.stephencleary.com/2013/11/there-is-no-thread.html) is that there is no thread. An async I/O operation is completed at a lower level than the threading model. It generally occurs within interrupt handler routines. Therefore, there is no I/O thread handling the request.

You ask how a thread that launches a blocking I/O request returns immediately. The answer is because an I/O request is not at its core actually blocking. You could block a thread such that you are intentionally saying not to do anything else until that I/O request finishes, but it was never the I/O that was blocking, it was the thread deciding to spin (or possibly yield its time slice).

The thread returns immediately because nothing has to sit there polling or querying the I/O operation. That is the core of true asynchronicity. An I/O request is made, and ultimately the completion bubbles up from an ISR. Yes, this may bubble up into the thread pool to set the task completion, but that happens in a nearly imperceptible amount of time. The work itself never had to be ran on a thread. The request itself may have been issued from a thread, but as it is an asynchronous request, the thread can immediately return.

Let's forget C# for a moment. Lets say I am writing some embedded code and I request data from a SPI bus. I send the request, continue my main loop, and when the SPI data is ready, an ISR is triggered. My main loop resumes immediately precisely because my request is asynchronous. All it has to do is push some data into a shift register and continue on. When data is ready for me to read back, an interrupt triggers. This is not running on a thread. It may interrupt a thread to complete the ISR, but you could not say that it actually ran on that thread. Just because its C#, this process is not ultimately any different.

Similarly, lets say I want to transfer data over USB. I place the data in a DMA location, set a flag to tell the bus to transfer my URB, and then immediately return. When I get a response back it also is moved into memory, an interrupt occurs and sets a flag to let the system know hey, heres a packet of data sitting in a buffer for you.

So once again, I/O is never truly blocking. It could appear to block, but that is not what is happening at the low level. It is higher level processes that may decide that an I/O operation has to happen synchronously with some other code. This is not to say of course that I/O is instant. Just that the CPU is not stuck doing work to service the I/O. It COULD block if implemented that way, and this COULD involve threads. But that is not how async I/O is implemented.

bodangly
  • 2,473
  • 17
  • 28
  • Thank you. I have read that article in the past but that doesn't answer the core of my question. That article does not answer the fundamental question -- which is the thread that delivers the I/O request? And what happens to the thread soon after it has delivered the request? The point it makes is: the work to fulfill the I/O request is not the business of any thread, which is not my question at all. – Water Cooler v2 Jun 09 '16 at 05:41
  • And in overarching that little detail with the assertion that "no thread is involved at all in the entire operation" is a bit misleading, I think. – Water Cooler v2 Jun 09 '16 at 05:42
  • In fact, the explanation I seek is buried somewhere in a much needed elaboration of this statement from the article, *"With the IRP “pending”, the OS returns to the library, which returns an incomplete task to the button click event handler, which suspends the async method, and the UI thread continues executing."* – Water Cooler v2 Jun 09 '16 at 05:45
  • My suspicion is that there *has* to be a thread blocked somewhere waiting on the I/O operation. It is very likely a thread pool thread but it could even be the main thread if the operation completes immediately. Which specific thread it will be probably depends on the task scheduler used to start the task. – Water Cooler v2 Jun 09 '16 at 05:59
  • @WaterCoolerv2 See my additions, maybe that helps clarify it for you. – bodangly Jun 09 '16 at 06:04
  • I read your update but it does two things: (1) It emphasizes that the actual work in fulfilling an I/O request is not done by any thread. This is a known fact that I already know and am not at all debating. (2) It says that when a thread issues the I/O request, it immediately returns *and is free to do other work*. This is the bit I am asking about. This little thing goes against *Assumption A* I made in the question, which is what I had about how threads work. Could you please clarify? Are you saying that *Assumption A* is wrong? – Water Cooler v2 Jun 09 '16 at 06:10
  • And if *Assumption A* was wrong to begin with, then the TPL does not add anything in terms of scalability. Because then, even earlier than the TPL, any thread issuing an I/O request would not block and get into a wait state? – Water Cooler v2 Jun 09 '16 at 06:11
  • And I am not contending that I know for a fact that my *Assumption A* is correct. I would like to be corrected if it isn't. I just want to reconcile it all into one explanation that makes sense. – Water Cooler v2 Jun 09 '16 at 06:15
  • I am sure there is *some* gap in my understanding. There is *something* that I am assuming wrong. I just wish someone could *definitively* provide a holistic explanation. – Water Cooler v2 Jun 09 '16 at 06:21
  • I am pretty certain it is some fault in my understanding. It is looking more and more like my assumption A is faulty. If someone could only provide me an authoritative source, and an assertive, "Yes, your assumption A is wrong. Go read here and find out." Someone that categorically debunks that assumption and shows me some documentation to prove it. I'll be satisfied. Because this challenges my idea about threading as far as I/O operations are concerned. – Water Cooler v2 Jun 09 '16 at 06:25
  • 2
    https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/13_IOSystems.html I think you are conflating blocking I/O with non blocking I/O with async I/O. As this link points out, for asynchronous I/O, Assumption A is definitively wrong. – bodangly Jun 09 '16 at 06:34
  • Thank you. I will read and come back. It will take some time, may be a day or two. – Water Cooler v2 Jun 09 '16 at 06:39
  • And thank you for your persistence. I appreciate it. – Water Cooler v2 Jun 09 '16 at 06:40
  • I can't thank you enough. That has annihilated a big dinosaur I had been haggling with in my mind for a very long time. – Water Cooler v2 Jun 09 '16 at 06:59
  • @WaterCoolerv2: What was missing in [my blog post](http://blog.stephencleary.com/2013/11/there-is-no-thread.html) on this subject? How can it be improved? – Stephen Cleary Jun 09 '16 at 13:08
  • @StephenCleary Somehow, the answer in this thread has added so tremendously to my existing knowledge of I/O operations and threading that I am still picking up the pieces and assimilating them into a cohesive theory. I still may have a follow-up question about this to finally nail the coffin. But it appears now that with this essential knowledge gained, there is nothing missing from your article. My knowledge was wrong. – Water Cooler v2 Jun 09 '16 at 13:48
  • @StephenCleary However, I believe some bit of detail can be added to improve your article for many others who might have incomplete knowledge and thus might be holding faulty assumptions as I did. I need to think more before I can articulate what that might be. In summary, nothing is essentially missing from the article for those who know about the existence of asynchronous I/O operations that do not block threads. Those that don't could be served better with a prelude that explains the difference. – Water Cooler v2 Jun 09 '16 at 13:50