In a browser, Javascript is a great example of an asynchronous program that has no threads.
You don’t need to worry that several pieces of code are touching the same objects at the same time: each function will exit before any other javascript is launched on the page.
However, when you do something like an AJAX request, no code is run at all, so other javascript can respond to events such as click events until that request returns and calls the associated callback. If one of these other event handlers still works when the AJAX request returns, its handler will not be called until it is executed. Only one “stream” of JavaScript works there, although you can effectively pause what you are doing until you get the information you need.
In C # applications, the same thing happens when you are dealing with user interface elements — you are allowed to interact with user interface elements when you are in the user interface stream. If the user clicks the button and you want to respond by reading a large file from disk, an inexperienced programmer may make a mistake in reading the file in the click event handler itself, which will lead to the application "freezing" until the file has finished downloading, since it was not allowed Respond to any clicks, freezes, or any other events related to the user interface until this thread is released.
One of the programmers can use this problem to create a new chain to download the file, and then tell this stream code that when loading the file it needs to run the remaining code in the user interface stream again so that it can update the user interface elements based on the found in file. Until recently, this approach was very popular, because it was that the C # libraries and language were simplified, but fundamentally more complicated than it should be.
If you think about what the CPU does when it reads a file at the hardware / operating system level, it basically gives instructions for reading pieces of data from disk to memory and enters the operating system with an “interrupt” when reading is completed. In other words, reading from disk (or any I / O really) is essentially an asynchronous operation. The concept of a thread waiting for I / O to complete is an abstraction that library developers have created to make programming easier. It's not needed.
Now, most .NET input / output operations have a corresponding method ...Async() , which you can call, which returns Task almost immediately. You can add callbacks to this Task to specify the code that you want to run when the asynchronous operation is completed. You can also specify which thread you want to use for this code, and you can provide a token that the asynchronous operation can check from time to time to find out if you decide to cancel the asynchronous task, allowing it to quickly stop working and elegantly.
Until the async/await keywords were added, C # was much more obvious about how the callback code was called, as these callbacks were in the form of delegates that were associated with the task. To still take advantage of the operation ...Async() , avoiding code complexity, async/await abstracts the creation of these delegates. But they still exist in the compiled code.
That way, you can have an await UI event handler for an I / O operation, freeing up the user interface thread to do other things, and more or less automatically returning to the user interface thread after you finish reading the file - without having to create a new stream.