You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/standard/async-in-depth.md
+19-17Lines changed: 19 additions & 17 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,15 +17,15 @@ Tasks are constructs used to implement what is known as the [Promise Model of Co
17
17
-`Task` represents a single operation that does not return a value.
18
18
-`Task<T>` represents a single operation that returns a value of type `T`.
19
19
20
-
It’s important to reason about tasks as abstractions of work happening asynchronously, and *not* an abstraction over threading. By default, tasks execute on the current thread and delegate work to the Operating System, as appropriate. Optionally, tasks can be explicitly requested to run on a separate thread via the `Task.Run` API.
20
+
It's important to reason about tasks as abstractions of work happening asynchronously, and *not* an abstraction over threading. By default, tasks execute on the current thread and delegate work to the operating system, as appropriate. Optionally, tasks can be explicitly requested to run on a separate thread via the `Task.Run` API.
21
21
22
-
Tasks expose an API protocol for monitoring, waiting upon and accessing the result value (in the case of `Task<T>`) of a task. Language integration, with the `await` keyword, provides a higher-level abstraction for using tasks.
22
+
Tasks expose an API protocol for monitoring, waiting upon, and accessing the result value (in the case of `Task<T>`) of a task. Language integration, with the `await` keyword, provides a higher-level abstraction for using tasks.
23
23
24
-
Using `await` allows your application or service to perform useful work while a task is running by yielding control to its caller until the task is done. Your code does not need to rely on callbacks or events to continue execution after the task has been completed. The language and task API integration does that for you. If you’re using `Task<T>`, the `await` keyword will additionally "unwrap" the value returned when the Task is complete. The details of how this works are explained further below.
24
+
Using `await` allows your application or service to perform useful work while a task is running by yielding control to its caller until the task is done. Your code does not need to rely on callbacks or events to continue execution after the task has been completed. The language and task API integration does that for you. If you're using `Task<T>`, the `await` keyword will additionally "unwrap" the value returned when the Task is complete. The details of how this works are explained later in this article.
25
25
26
-
You can learn more about tasks and the different ways to interact with them in the [Task-based Asynchronous Pattern (TAP)](./asynchronous-programming-patterns/task-based-asynchronous-pattern-tap.md)topic.
26
+
You can learn more about tasks and the different ways to interact with them in the [Task-based Asynchronous Pattern (TAP)](./asynchronous-programming-patterns/task-based-asynchronous-pattern-tap.md)article.
27
27
28
-
## Deeper Dive into Tasks for an I/O-Bound Operation
28
+
## Tasks for an I/O-bound operation
29
29
30
30
The following section describes a 10,000 foot view of what happens with a typical async I/O call. Let's start with a couple examples from the following class.
31
31
@@ -69,27 +69,27 @@ class DotNetFoundationClient
69
69
}
70
70
```
71
71
72
-
The call to `GetStringAsync()` calls through lower-level .NET libraries (perhaps calling other async methods) until it reaches a P/Invoke interop call into a native networking library. The native library may subsequently call into a System API call (such as `write()` to a socket on Linux). A task object will be created at the native/managed boundary, possibly using [TaskCompletionSource](xref:System.Threading.Tasks.TaskCompletionSource%601.SetResult(%600)). The task object will be passed up through the layers, possibly operated on or directly returned, eventually returned to the initial caller.
72
+
The call to `GetStringAsync()` calls through lower-level .NET libraries (perhaps calling other async methods) until it reaches a P/Invoke interop call into a native networking library. The native library may subsequently call into a System API call (such as `write()` to a socket on Linux). A task object is created at the native/managed boundary, possibly using [TaskCompletionSource](xref:System.Threading.Tasks.TaskCompletionSource%601.SetResult(%600)). The task object is passed up through the layers, possibly operated on or directly returned, eventually returned to the initial caller.
73
73
74
-
In the second example method `GetFirstCharactersCountAsync()` above, a `Task<T>` object will be returned from `GetStringAsync`. The use of the `await` keyword causes the method to return a newly created task object. Control returns to the caller from this location in the `GetFirstCharactersCountAsync` method. The methods and properties of the [Task<T>](xref:System.Threading.Tasks.Task%601) object enable callers to monitor the progress of the task, which will complete when the remaining code in GetFirstCharactersCountAsync has executed.
74
+
In the second example method,`GetFirstCharactersCountAsync()`, a `Task<T>` object is returned from `GetStringAsync`. The use of the `await` keyword causes the method to return a newly created task object. Control returns to the caller from this location in the `GetFirstCharactersCountAsync` method. The methods and properties of the [Task<T>](xref:System.Threading.Tasks.Task%601) object enable callers to monitor the progress of the task, which completes when the remaining code in GetFirstCharactersCountAsync has executed.
75
75
76
-
After the System API call, the request is now in kernel space, making its way to the networking subsystem of the OS (such as `/net` in the Linux Kernel). Here the OS will handle the networking request *asynchronously*. Details may be different depending on the OS used (the device driver call may be scheduled as a signal sent back to the runtime, or a device driver call may be made and *then* a signal sent back), but eventually the runtime will be informed that the networking request is in progress. At this time, the work for the device driver will either be scheduled, in-progress, or already finished (the request is already out "over the wire") - but because this is all happening asynchronously, the device driver is able to immediately handle something else!
76
+
After the System API call, the request is now in kernel space, making its way to the networking subsystem of the OS (such as `/net` in the Linux Kernel). Here, the OS will handle the networking request *asynchronously*. Details may be different depending on the OS used (for example, the device driver call may be scheduled as a signal sent back to the runtime, or a device driver call may be made and *then* a signal sent back), but eventually the runtime will be informed that the networking request is in progress. At this time, the work for the device driver will either be scheduled, in-progress, or already finished (the request is already out "over the wire"). But because this is all happening asynchronously, the device driver is able to immediately handle something else!
77
77
78
-
For example, in Windows an OS thread makes a call to the network device driver and asks it to perform the networking operation via an Interrupt Request Packet (IRP) which represents the operation. The device driver receives the IRP, makes the call to the network, marks the IRP as "pending", and returns back to the OS. Because the OS thread now knows that the IRP is "pending", it doesn't have any more work to do for this job and "returns" back so that it can be used to perform other work.
78
+
For example, in Windows, an OS thread makes a call to the network device driver and asks it to perform the networking operation via an Interrupt Request Packet (IRP), which represents the operation. The device driver receives the IRP, makes the call to the network, marks the IRP as "pending", and returns back to the OS. Because the OS thread now knows that the IRP is "pending", it doesn't have any more work to do for this job and "returns" back so that it can be used to perform other work.
79
79
80
-
When the request is fulfilled and data comes back through the device driver, it notifies the CPU of new data received via an interrupt. How this interrupt gets handled will vary depending on the OS, but eventually the data will be passed through the OS until it reaches a system interop call (for example, in Linux an interrupt handler will schedule the bottom half of the IRQ to pass the data up through the OS asynchronously). This *also* happens asynchronously! The result is queued up until the next available thread is able to execute the async method and "unwrap" the result of the completed task.
80
+
When the request is fulfilled and data comes back through the device driver, it notifies the CPU of new data received via an interrupt. How this interrupt is handled varies depending on the OS, but eventually the data is passed through the OS until it reaches a system interop call. For example, in Linux, an interrupt handler will schedule the bottom half of the interrupt request (IRQ) to pass the data up through the OS asynchronously. This *also* happens asynchronously! The result is queued up until the next available thread is able to execute the async method and "unwrap" the result of the completed task.
81
81
82
-
Throughout this entire process, a key takeaway is that **no thread is dedicated to running the task**. Although work is executed in some context (that is, the OS does have to pass data to a device driver and respond to an interrupt), there is no thread dedicated to *waiting* for data from the request to come back. This allows the system to handle a much larger volume of work rather than waiting for some I/O call to finish.
82
+
Throughout this entire process, a key takeaway is that **no thread is dedicated to running the task**. Although work is executed in some context (that is, the OS does have to pass data to a device driver and respond to an interrupt), there is no thread dedicated to *waiting* for data from the request to come back. This allows the system to handle a much larger volume of work rather than waiting for some I/O call to finish.
83
83
84
-
Although that may seem like a lot of work to be done, when measured in terms of wall clock time, it’s minuscule compared to the time it takes to do the actual I/O work. Although not at all precise, a potential timeline for such a call would look like this:
84
+
Although that may seem like a lot of work to be done, when measured in terms of wall clock time, it's minuscule compared to the time it takes to do the actual I/O work. Although not at all precise, a potential timeline for such a call would look like this:
- Time spent from points `0` to `1` is everything up until an async method yields control to its caller.
89
89
- Time spent from points `1` to `2` is the time spent on I/O, with no CPU cost.
90
90
- Finally, time spent from points `2` to `3` is passing control back (and potentially a value) to the async method, at which point it is executing again.
91
91
92
-
### What does this mean for a server scenario?
92
+
### Server
93
93
94
94
This model works well with a typical server scenario workload. Because there are no threads dedicated to blocking on unfinished tasks, the server thread pool can service a much higher volume of web requests.
95
95
@@ -101,17 +101,19 @@ The server *with* async code running on it still queues up the sixth request, bu
101
101
102
102
Although this is a contrived example, it works in a similar fashion in the real world. In fact, you can expect a server to be able to handle an order of magnitude more requests using `async` and `await` than if it were dedicating a thread for each request it receives.
103
103
104
-
### What does this mean for client scenario?
104
+
### Client
105
105
106
106
The biggest gain for using `async` and `await` for a client app is an increase in responsiveness. Although you can make an app responsive by spawning threads manually, the act of spawning a thread is an expensive operation relative to just using `async` and `await`. Especially for something like a mobile game, it's crucial to impact the UI thread as little as possible where I/O is concerned.
107
107
108
108
More importantly, because I/O-bound work spends virtually no time on the CPU, dedicating an entire CPU thread to perform barely any useful work would be a poor use of resources.
109
109
110
110
Additionally, dispatching work to the UI thread (such as updating a UI) is simple with `async` methods, and does not require extra work (such as calling a thread-safe delegate).
111
111
112
-
## Deeper Dive into Task and Task\<T> for a CPU-Bound Operation
112
+
## Task and Task\<T> for a CPU-bound operation
113
113
114
-
CPU-bound `async` code is a bit different than I/O-bound `async` code. Because the work is done on the CPU, there's no way to get around dedicating a thread to the computation. The use of `async` and `await` provides you with a clean way to interact with a background thread and keep the caller of the async method responsive. Note that this does not provide any protection for shared data. If you are using shared data, you will still need to apply an appropriate synchronization strategy.
114
+
CPU-bound `async` code is a bit different than I/O-bound `async` code. Because the work is done on the CPU, there's no way to get around dedicating a thread to the computation. The use of `async` and `await` provides you with a clean way to interact with a background thread and keep the caller of the async method responsive.
115
+
116
+
This doesn't provide any protection for shared data. If you are using shared data, you still need to apply an appropriate synchronization strategy.
115
117
116
118
Here's a 10,000 foot view of a CPU-bound async call:
117
119
@@ -137,7 +139,7 @@ Once `await` is encountered, the execution of `CalculateResult()` is yielded to
137
139
138
140
### Why does async help here?
139
141
140
-
`async` and `await` are the best practice for managing CPU-bound work when you need responsiveness. There are multiple patterns for using async with CPU-bound work. It's important to note that there is a small cost to using async and it's not recommended for tight loops. It's up to you to determine how you write your code around this new capability.
142
+
`async` and `await` are the best practice for managing CPU-bound work when you need responsiveness. There are multiple patterns for using async with CPU-bound work. It's important to note that there is a small cost to using async and it's not recommended for tight loops.
0 commit comments