ChatGPT解决这个技术问题 Extra ChatGPT

What is the difference between asynchronous programming and multithreading?

I thought that they were basically the same thing — writing programs that split tasks between processors (on machines that have 2+ processors). Then I'm reading this, which says:

Async methods are intended to be non-blocking operations. An await expression in an async method doesn’t block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method. The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.

and I'm wondering whether someone can translate that to English for me. It seems to draw a distinction between asynchronicity (is that a word?) and threading and imply that you can have a program that has asynchronous tasks but no multithreading.

Now I understand the idea of asynchronous tasks such as the example on pg. 467 of Jon Skeet's C# In Depth, Third Edition

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

The async keyword means "This function, whenever it is called, will not be called in a context in which its completion is required for everything after its call to be called."

In other words, writing it in the middle of some task

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, since DisplayWebsiteLength() has nothing to do with x or y, will cause DisplayWebsiteLength() to be executed "in the background", like

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Obviously that's a stupid example, but am I correct or am I totally confused or what?

(Also, I'm confused about why sender and e aren't ever used in the body of the above function.)

sender and e are suggesting this is actually an event handler - pretty much the only place where async void is desirable. Most likely, this is called on a button click or something like that - the result being that this action happens completely asynchronously with respect to the rest of the application. But it's still all on one thread - the UI thread (with a tiny sliver of time on an IOCP thread that posts the callback to the UI thread).
A very important note on the DisplayWebsiteLength code sample: You should not use HttpClient in a using statement - Under a heavy load, the code can exhaust the number of sockets available resulting in SocketException errors. More info on Improper Instantiation.
@JakubLortz I don't know who the article is for really. Not for beginners, since it requires good knowledge about threads, interrupts, CPU-related stuff, etc. Not for advanced users, since for them it's all clear already. I'm sure it will not help anyone understand what it's all about -too high level of abstraction.

R
Robert Harvey

Your misunderstanding is extremely common. Many people are taught that multithreading and asynchrony are the same thing, but they are not.

An analogy usually helps. You are cooking in a restaurant. An order comes in for eggs and toast.

Synchronous: you cook the eggs, then you cook the toast.

Asynchronous, single threaded: you start the eggs cooking and set a timer. You start the toast cooking, and set a timer. While they are both cooking, you clean the kitchen. When the timers go off you take the eggs off the heat and the toast out of the toaster and serve them.

Asynchronous, multithreaded: you hire two more cooks, one to cook eggs and one to cook toast. Now you have the problem of coordinating the cooks so that they do not conflict with each other in the kitchen when sharing resources. And you have to pay them.

Now does it make sense that multithreading is only one kind of asynchrony? Threading is about workers; asynchrony is about tasks. In multithreaded workflows you assign tasks to workers. In asynchronous single-threaded workflows you have a graph of tasks where some tasks depend on the results of others; as each task completes it invokes the code that schedules the next task that can run, given the results of the just-completed task. But you (hopefully) only need one worker to perform all the tasks, not one worker per task.

It will help to realize that many tasks are not processor-bound. For processor-bound tasks it makes sense to hire as many workers (threads) as there are processors, assign one task to each worker, assign one processor to each worker, and have each processor do the job of nothing else but computing the result as quickly as possible. But for tasks that are not waiting on a processor, you don't need to assign a worker at all. You just wait for the message to arrive that the result is available and do something else while you're waiting. When that message arrives then you can schedule the continuation of the completed task as the next thing on your to-do list to check off.

So let's look at Jon's example in more detail. What happens?

Someone invokes DisplayWebSiteLength. Who? We don't care.

It sets a label, creates a client, and asks the client to fetch something. The client returns an object representing the task of fetching something. That task is in progress.

Is it in progress on another thread? Probably not. Read Stephen's article on why there is no thread.

Now we await the task. What happens? We check to see if the task has completed between the time we created it and we awaited it. If yes, then we fetch the result and keep running. Let's suppose it has not completed. We sign up the remainder of this method as the continuation of that task and return.

Now control has returned to the caller. What does it do? Whatever it wants.

Now suppose the task completes. How did it do that? Maybe it was running on another thread, or maybe the caller that we just returned to allowed it to run to completion on the current thread. Regardless, we now have a completed task.

The completed task asks the correct thread -- again, likely the only thread -- to run the continuation of the task.

Control passes immediately back into the method we just left at the point of the await. Now there is a result available so we can assign text and run the rest of the method.

It's just like in my analogy. Someone asks you for a document. You send away in the mail for the document, and keep on doing other work. When it arrives in the mail you are signalled, and when you feel like it, you do the rest of the workflow -- open the envelope, pay the delivery fees, whatever. You don't need to hire another worker to do all that for you.


@user5648283: The hardware is the wrong level to think about tasks. A task is simply an object that (1) represents that a value will become available in the future and (2) can run code (on the correct thread) when that value is available. How any individual task obtains the result in the future is up to it. Some will use special hardware like "disks" and "network cards" to do that; some will use hardware like CPUs.
@user5648283: Again, think about my analogy. When someone asks you to cook eggs and toast, you use special hardware -- a stove and a toaster -- and you can clean the kitchen while the hardware is doing its work. If someone asks you for eggs, toast, and an original critique of the last Hobbit movie, you can write your review while the eggs and toast are cooking, but you don't need to use hardware for that.
@user5648283: Now as for your question about "rearranging the code", consider this. Suppose you have a method P which has a yield return, and a method Q which does a foreach over the result of P. Step through the code. You'll see that we run a little bit of Q then a little bit of P then a little bit of Q... Do you understand the point of that? await is essentially yield return in fancy dress. Now is it more clear?
The toaster is hardware. Hardware doesn't need a thread to service it; disks and network cards and whatnot run at a level far below that of OS threads.
@ShivprasadKoirala: That is absolutely not true at all. If you believe that, then you have some very false beliefs about asynchrony. The whole point of asynchrony in C# is that it does not create a thread.
S
StriplingWarrior

In-browser Javascript is a great example of an asynchronous program that has no multithreading.

You don't have to worry about multiple pieces of code touching the same objects at the same time: each function will finish running before any other javascript is allowed to run on the page. (Update: Since this was written, JavaScript has added async functions and generator functions. These functions do not always run to completion before any other javascript is executed: whenever they reach a yield or await keyword, they yield execution to other javascript, and can continue execution later, similar to C#'s async methods.)

However, when doing something like an AJAX request, no code is running at all, so other javascript can respond to things like click events until that request comes back and invokes the callback associated with it. If one of these other event handlers is still running when the AJAX request gets back, its handler won't be called until they're done. There's only one JavaScript "thread" running, even though it's possible for you to effectively pause the thing you were doing until you have the information you need.

In C# applications, the same thing happens any time you're dealing with UI elements--you're only allowed to interact with UI elements when you're on the UI thread. If the user clicked a button, and you wanted to respond by reading a large file from the disk, an inexperienced programmer might make the mistake of reading the file within the click event handler itself, which would cause the application to "freeze" until the file finished loading because it's not allowed to respond to any more clicking, hovering, or any other UI-related events until that thread is freed.

One option programmers might use to avoid this problem is to create a new thread to load the file, and then tell that thread's code that when the file is loaded it needs to run the remaining code on the UI thread again so it can update UI elements based on what it found in the file. Until recently, this approach was very popular because it was what the C# libraries and language made easy, but it's fundamentally more complicated than it has to be.

If you think about what the CPU is doing when it reads a file at the level of the hardware and Operating System, it's basically issuing an instruction to read pieces of data from the disk into memory, and to hit the operating system with an "interrupt" when the read is complete. In other words, reading from disk (or any I/O really) is an inherently asynchronous operation. The concept of a thread waiting for that I/O to complete is an abstraction that the library developers created to make it easier to program against. It's not necessary.

Now, most I/O operations in .NET have a corresponding ...Async() method you can invoke, which returns a Task almost immediately. You can add callbacks to this Task to specify code that you want to have run when the asynchronous operation completes. You can also specify which thread you want that code to run on, and you can provide a token which the asynchronous operation can check from time to time to see if you decided to cancel the asynchronous task, giving it the opportunity to stop its work quickly and gracefully.

Until the async/await keywords were added, C# was much more obvious about how callback code gets invoked, because those callbacks were in the form of delegates that you associated with the task. In order to still give you the benefit of using the ...Async() operation, while avoiding complexity in code, async/await abstracts away the creation of those delegates. But they're still there in the compiled code.

So you can have your UI event handler await an I/O operation, freeing up the UI thread to do other things, and more-or-less automatically returning to the UI thread once you've finished reading the file--without ever having to create a new thread.


There's only one JavaScript "thread" running - no longer true with Web Workers.
@oleksii: That's technically true, but I wasn't going to go into that because the Web Workers API itself is asynchronous, and Web Workers aren't allowed to directly impact the javascript values or the DOM on the web page they're invoked from, which means the crucial second paragraph of this answer still holds true. From the programmer's perspective, there's little difference between invoking a Web Worker and invoking an AJAX request.
In-browser Javascript is a great example of an asynchronous program that has no threads Being a little pedantic - there is always at least 1 thread of execution
@KejsiStruga: LOL, point taken. Changed "no threads" to "no multithreading"