Asynchronous programming patterns : Task-based asynchronous pattern:TAP vs the Asynchronous Progr
作者:互联网
Async Overview
Not so long ago, apps got faster simply by buying a newer PC or server and then that trend stopped. In fact, it reversed. Mobile phones appeared with 1ghz single core ARM chips and server workloads transitioned to VMs. Users still want responsive UI and business owners want servers that scale with their business. The transition to mobile and cloud and an internet-connected population of >3B users has resulted in a new set of software patterns.
- Client applications are expected to be always-on, always-connected and constantly responsive to user interaction (for example, touch) with high app store ratings!
- Services are expected to handle spikes in traffic by gracefully scaling up and down.
Async programming is a key technique that makes it straightforward to handle blocking I/O and concurrent operations on multiple cores. .NET provides the capability for apps and services to be responsive and elastic with easy-to-use, language-level asynchronous programming models in C#, Visual Basic, and F#.
Why Write Async Code?
Modern apps make extensive use of file and networking I/O. I/O APIs traditionally block by default, resulting in poor user experiences and hardware utilization unless you want to learn and use challenging patterns. Task-based async APIs and the language-level asynchronous programming model invert this model, making async execution the default with few new concepts to learn.
Async code has the following characteristics:
- Handles more server requests by yielding threads to handle more requests while waiting for I/O requests to return.
- Enables UIs to be more responsive by yielding threads to UI interaction while waiting for I/O requests and by transitioning long-running work to other CPU cores.
- Many of the newer .NET APIs are asynchronous.
- It's easy to write async code in .NET!
What's next?
For more information, see the Async in depth topic.
The Asynchronous Programming Patterns topic provides an overview of the three asynchronous programming patterns supported in .NET:
-
Asynchronous Programming Model (APM) (legacy)
-
Task-based Asynchronous Pattern (TAP) (recommended for new development)
For more information about recommended task-based programming model, see the Task-based asynchronous programming topic.
Async in depth
Writing I/O- and CPU-bound asynchronous code is straightforward using the .NET Task-based async model. The model is exposed by the Task
and Task<T>
types and the async
and await
keywords in C# and Visual Basic. (Language-specific resources are found in the See also section.) This article explains how to use .NET async and provides insight into the async framework used under the covers.
Task and Task<T>
Tasks are constructs used to implement what is known as the Promise Model of Concurrency. In short, they offer you a "promise" that work will be completed at a later point, letting you coordinate with the promise with a clean API.
Task
represents a single operation that does not return a value.Task<T>
represents a single operation that returns a value of typeT
.
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.
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.
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.
You can learn more about tasks and the different ways to interact with them in the Task-based Asynchronous Pattern (TAP) article.
class DotNetFoundationClient { // HttpClient is intended to be instantiated once per application, rather than per-use. private static readonly HttpClient s_client = new HttpClient(); public Task<string> GetHtmlAsync() { // Execution is synchronous here var uri = new Uri("https://www.dotnetfoundation.org"); return s_client.GetStringAsync(uri); } public async Task<string> GetFirstCharactersCountAsync(int count) { // Execution is synchronous here var uri = new Uri("https://www.dotnetfoundation.org"); // Execution of GetFirstCharactersCountAsync() is yielded to the caller here // GetStringAsync returns a Task<string>, which is *awaited* var page = await s_client.GetStringAsync(uri); // Execution resumes when the client.GetStringAsync task completes, // becoming synchronous again. if (count > page.Length) { return page; } else { return page.Substring(0, count); } } }
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. The task object is passed up through the layers, possibly operated on or directly returned, eventually returned to the initial caller.
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> object enable callers to monitor the progress of the task, which completes when the remaining code in GetFirstCharactersCountAsync has executed.
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!
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.
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.
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.
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:
0-1————————————————————————————————————————————————–2-3
- Time spent from points
0
to1
is everything up until an async method yields control to its caller. - Time spent from points
1
to2
is the time spent on I/O, with no CPU cost. - Finally, time spent from points
2
to3
is passing control back (and potentially a value) to the async method, at which point it is executing again.
Server
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.
Consider two servers: one that runs async code, and one that does not. For this example, each server only has five threads available to service requests. This number is unrealistically small and serves only in a demonstrative context.
Assume both servers receive six concurrent requests. Each request performs an I/O operation. The server without async code has to queue up the sixth request until one of the five threads have finished the I/O-bound work and written a response. At the point that the 20th request comes in, the server might start to slow down, because the queue is getting too long.
The server with async code running on it still queues up the sixth request, but because it uses async
and await
, each of its threads are freed up when the I/O-bound work starts, rather than when it finishes. By the time the 20th request comes in, the queue for incoming requests will be far smaller (if it has anything in it at all), and the server won't slow down.
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.
Client
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.
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.
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).
Task and Task<T> for a CPU-bound operation
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.
This doesn't provide any protection for shared data. If you are using shared data, you still need to apply an appropriate synchronization strategy.
Here's a 10,000 foot view of a CPU-bound async call:
C#public async Task<int> CalculateResult(InputData data)
{
// This queues up the work on the threadpool.
var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));
// Note that at this point, you can do some other work concurrently,
// as CalculateResult() is still executing!
// Execution of CalculateResult is yielded here!
var result = await expensiveResultTask;
return result;
}
CalculateResult()
executes on the thread it was called on. When it calls Task.Run
, it queues the expensive CPU-bound operation, DoExpensiveCalculation()
, on the thread pool and receives a Task<int>
handle. DoExpensiveCalculation()
is eventually run concurrently on the next available thread, likely on another CPU core. It's possible to do concurrent work while DoExpensiveCalculation()
is busy on another thread, because the thread that called CalculateResult()
is still executing.
Once await
is encountered, the execution of CalculateResult()
is yielded to its caller, allowing other work to be done with the current thread while DoExpensiveCalculation()
is churning out a result. Once it has finished, the result is queued up to run on the main thread. Eventually, the main thread will return to executing CalculateResult()
, at which point it will have the result of DoExpensiveCalculation()
.
Why does async help here?
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.
See also
- Asynchronous programming in C#
- Asynchronous programming with async and await (C#)
- Async Programming in F#
- Asynchronous Programming with Async and Await (Visual Basic)
Asynchronous programming patterns
.NET provides three patterns for performing asynchronous operations:
-
Task-based Asynchronous Pattern (TAP), which uses a single method to represent the initiation and completion of an asynchronous operation. TAP was introduced in .NET Framework 4. It's the recommended approach to asynchronous programming in .NET. The async and await keywords in C# and the Async and Await operators in Visual Basic add language support for TAP. For more information, see Task-based Asynchronous Pattern (TAP).
-
Event-based Asynchronous Pattern (EAP), which is the event-based legacy model for providing asynchronous behavior. It requires a method that has the
Async
suffix and one or more events, event handler delegate types, andEventArg
-derived types. EAP was introduced in .NET Framework 2.0. It's no longer recommended for new development. For more information, see Event-based Asynchronous Pattern (EAP). -
Asynchronous Programming Model (APM) pattern (also called the IAsyncResult pattern), which is the legacy model that uses the IAsyncResult interface to provide asynchronous behavior. In this pattern, synchronous operations require
Begin
andEnd
methods (for example,BeginWrite
andEndWrite
to implement an asynchronous write operation). This pattern is no longer recommended for new development. For more information, see Asynchronous Programming Model (APM).
Comparison of patterns
For a quick comparison of how the three patterns model asynchronous operations, consider a Read
method that reads a specified amount of data into a provided buffer starting at a specified offset:
public class MyClass
{
public int Read(byte [] buffer, int offset, int count);
}
The TAP counterpart of this method would expose the following single ReadAsync
method:
public class MyClass
{
public Task<int> ReadAsync(byte [] buffer, int offset, int count);
}
The EAP counterpart would expose the following set of types and members:
C#public class MyClass
{
public void ReadAsync(byte [] buffer, int offset, int count);
public event ReadCompletedEventHandler ReadCompleted;
}
The APM counterpart would expose the BeginRead
and EndRead
methods:
public class MyClass
{
public IAsyncResult BeginRead(
byte [] buffer, int offset, int count,
AsyncCallback callback, object state);
public int EndRead(IAsyncResult asyncResult);
}
See also
- Async in depth
- Asynchronous programming in C#
- Async Programming in F#
- Asynchronous Programming with Async and Await (Visual Basic)
Task-based asynchronous pattern:TAP
The task-based asynchronous pattern (TAP) is based on the System.Threading.Tasks.Task and System.Threading.Tasks.Task<TResult> types in the System.Threading.Tasks namespace, which are used to represent arbitrary asynchronous operations. TAP is the recommended asynchronous design pattern for new development.
Naming, parameters, and return types
TAP uses a single method to represent the initiation and completion of an asynchronous operation.
This contrasts with both the Asynchronous Programming Model (APM or IAsyncResult
) pattern and the Event-based Asynchronous Pattern (EAP).
APM requires Begin
and End
methods.
EAP requires a method that has the Async
suffix and also requires one or more events, event handler delegate types, and EventArg
-derived types.
Asynchronous methods in TAP include the Async
suffix after the operation name for methods that return awaitable types, such as Task, Task<TResult>, ValueTask, and ValueTask<TResult>.
For example, an asynchronous Get
operation that returns a Task<String>
can be named GetAsync
. If you're adding a TAP method to a class that already contains an EAP method name with the Async
suffix, use the suffix TaskAsync
instead. For example, if the class already has a GetAsync
method, use the name GetTaskAsync
. If a method starts an asynchronous operation but does not return an awaitable type, its name should start with Begin
, Start
, or some other verb to suggest that this method does not return or throw the result of the operation.
A TAP method returns either a System.Threading.Tasks.Task or a System.Threading.Tasks.Task<TResult>, based on whether the corresponding synchronous method returns void or a type TResult
.
The parameters of a TAP method should match the parameters of its synchronous counterpart and should be provided in the same order. However, out
and ref
parameters are exempt from this rule and should be avoided entirely. Any data that would have been returned through an out
or ref
parameter should instead be returned as part of the TResult
returned by Task<TResult>, and should use a tuple or a custom data structure to accommodate multiple values. Also, consider adding a CancellationToken parameter even if the TAP method's synchronous counterpart does not offer one.
Methods that are devoted exclusively to the creation, manipulation, or combination of tasks (where the asynchronous intent of the method is clear in the method name or in the name of the type to which the method belongs) need not follow this naming pattern; such methods are often referred to as combinators. Examples of combinators include WhenAll and WhenAny, and are discussed in the Using the Built-in Task-based Combinators section of the article Consuming the Task-based Asynchronous Pattern.
For examples of how the TAP syntax differs from the syntax used in legacy asynchronous programming patterns such as the Asynchronous Programming Model (APM) and the Event-based Asynchronous Pattern (EAP), see Asynchronous Programming Patterns.
Initiating an asynchronous operation
An asynchronous method that is based on TAP can do a small amount of work synchronously, such as validating arguments and initiating the asynchronous operation, before it returns the resulting task. Synchronous work should be kept to the minimum so the asynchronous method can return quickly. Reasons for a quick return include:
-
Asynchronous methods may be invoked from user interface (UI) threads, and any long-running synchronous work could harm the responsiveness of the application.
-
Multiple asynchronous methods may be launched concurrently. Therefore, any long-running work in the synchronous portion of an asynchronous method could delay the initiation of other asynchronous operations, thereby decreasing the benefits of concurrency.
In some cases, the amount of work required to complete the operation is less than the amount of work required to launch the operation asynchronously. Reading from a stream where the read operation can be satisfied by data that is already buffered in memory is an example of such a scenario. In such cases, the operation may complete synchronously, and may return a task that has already been completed.
Exceptions
An asynchronous method should raise an exception to be thrown out of the asynchronous method call only in response to a usage error. Usage errors should never occur in production code. For example, if passing a null reference (Nothing
in Visual Basic) as one of the method's arguments causes an error state (usually represented by an ArgumentNullException exception), you can modify the calling code to ensure that a null reference is never passed. For all other errors, exceptions that occur when an asynchronous method is running should be assigned to the returned task, even if the asynchronous method happens to complete synchronously before the task is returned. Typically, a task contains at most one exception. However, if the task represents multiple operations (for example, WhenAll), multiple exceptions may be associated with a single task.
Target environment
When you implement a TAP method, you can determine where asynchronous execution occurs. You may choose to execute the workload on the thread pool, implement it by using asynchronous I/O (without being bound to a thread for the majority of the operation's execution), run it on a specific thread (such as the UI thread), or use any number of potential contexts. A TAP method may even have nothing to execute, and may just return a Task that represents the occurrence of a condition elsewhere in the system (for example, a task that represents data arriving at a queued data structure).
The caller of the TAP method may block waiting for the TAP method to complete by synchronously waiting on the resulting task, or may run additional (continuation) code when the asynchronous operation completes. The creator of the continuation code has control over where that code executes. You may create the continuation code either explicitly, through methods on the Task class (for example, ContinueWith) or implicitly, by using language support built on top of continuations (for example, await
in C#, Await
in Visual Basic, AwaitValue
in F#).
Task status
The Task class provides a life cycle for asynchronous operations, and that cycle is represented by the TaskStatus enumeration. To support corner cases of types that derive from Task and Task<TResult>, and to support the separation of construction from scheduling, the Task class exposes a Start method. Tasks that are created by the public Task constructors are referred to as cold tasks, because they begin their life cycle in the non-scheduled Created state and are scheduled only when Start is called on these instances.
All other tasks begin their life cycle in a hot state, which means that the asynchronous operations they represent have already been initiated and their task status is an enumeration value other than TaskStatus.Created. All tasks that are returned from TAP methods must be activated. If a TAP method internally uses a task's constructor to instantiate the task to be returned, the TAP method must call Start on the Task object before returning it. Consumers of a TAP method may safely assume that the returned task is active and should not try to call Start on any Task that is returned from a TAP method. Calling Start on an active task results in an InvalidOperationException exception.
Cancellation (optional)
In TAP, cancellation is optional for both asynchronous method implementers and asynchronous method consumers. If an operation allows cancellation, it exposes an overload of the asynchronous method that accepts a cancellation token (CancellationToken instance). By convention, the parameter is named cancellationToken
.
public Task ReadAsync(byte [] buffer, int offset, int count,
CancellationToken cancellationToken)
The asynchronous operation monitors this token for cancellation requests. If it receives a cancellation request, it may choose to honor that request and cancel the operation. If the cancellation request results in work being ended prematurely, the TAP method returns a task that ends in the Canceled state; there is no available result and no exception is thrown. The Canceled state is considered to be a final (completed) state for a task, along with the Faulted and RanToCompletion states. Therefore, if a task is in the Canceled state, its IsCompleted property returns true
. When a task completes in the Canceled state, any continuations registered with the task are scheduled or executed, unless a continuation option such as NotOnCanceled was specified to opt out of continuation. Any code that is asynchronously waiting for a canceled task through use of language features continues to run but receives an OperationCanceledException or an exception derived from it. Code that is blocked synchronously waiting on the task through methods such as Wait and WaitAll also continue to run with an exception.
If a cancellation token has requested cancellation before the TAP method that accepts that token is called, the TAP method should return a Canceled task. However, if cancellation is requested while the asynchronous operation is running, the asynchronous operation need not accept the cancellation request. The returned task should end in the Canceled state only if the operation ends as a result of the cancellation request. If cancellation is requested but a result or an exception is still produced, the task should end in the RanToCompletion or Faulted state.
For asynchronous methods that want to expose the ability to be canceled first and foremost, you don't have to provide an overload that doesn't accept a cancellation token. For methods that cannot be canceled, do not provide overloads that accept a cancellation token; this helps indicate to the caller whether the target method is actually cancelable. Consumer code that does not desire cancellation may call a method that accepts a CancellationToken and provide None as the argument value. None is functionally equivalent to the default CancellationToken.
Progress reporting (optional)
Some asynchronous operations benefit from providing progress notifications; these are typically used to update a user interface with information about the progress of the asynchronous operation.
In TAP, progress is handled through an IProgress<T> interface, which is passed to the asynchronous method as a parameter that is usually named progress
. Providing the progress interface when the asynchronous method is called helps eliminate race conditions that result from incorrect usage (that is, when event handlers that are incorrectly registered after the operation starts may miss updates). More importantly, the progress interface supports varying implementations of progress, as determined by the consuming code. For example, the consuming code may only care about the latest progress update, or may want to buffer all updates, or may want to invoke an action for each update, or may want to control whether the invocation is marshaled to a particular thread. All these options may be achieved by using a different implementation of the interface, customized to the particular consumer's needs. As with cancellation, TAP implementations should provide an IProgress<T> parameter only if the API supports progress notifications.
For example, if the ReadAsync
method discussed earlier in this article is able to report intermediate progress in the form of the number of bytes read thus far, the progress callback could be an IProgress<T> interface:
public Task ReadAsync(byte[] buffer, int offset, int count,
IProgress<long> progress)
If a FindFilesAsync
method returns a list of all files that meet a particular search pattern, the progress callback could provide an estimate of the percentage of work completed and the current set of partial results. It could provide this information with either a tuple:
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
string pattern,
IProgress<Tuple<double,
ReadOnlyCollection<List<FileInfo>>>> progress)
or with a data type that's specific to the API:
C#public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
string pattern,
IProgress<FindFilesProgressInfo> progress)
In the latter case, the special data type is usually suffixed with ProgressInfo
.
If TAP implementations provide overloads that accept a progress
parameter, they must allow the argument to be null
, in which case no progress is reported. TAP implementations should report the progress to the Progress<T> object synchronously, which enables the asynchronous method to quickly provide progress. It also allows the consumer of the progress to determine how and where best to handle the information. For example, the progress instance could choose to marshal callbacks and raise events on a captured synchronization context.
IProgress<T> implementations
.NET provides the Progress<T> class, which implements IProgress<T>. The Progress<T> class is declared as follows:
C#public class Progress<T> : IProgress<T>
{
public Progress();
public Progress(Action<T> handler);
protected virtual void OnReport(T value);
public event EventHandler<T>? ProgressChanged;
}
An instance of Progress<T> exposes a ProgressChanged event, which is raised every time the asynchronous operation reports a progress update. The ProgressChanged event is raised on the SynchronizationContext object that was captured when the Progress<T> instance was instantiated. If no synchronization context was available, a default context that targets the thread pool is used. Handlers may be registered with this event. A single handler may also be provided to the Progress<T> constructor for convenience, and behaves just like an event handler for the ProgressChanged event. Progress updates are raised asynchronously to avoid delaying the asynchronous operation while event handlers are executing. Another IProgress<T> implementation could choose to apply different semantics.
Choosing the overloads to provide
If a TAP implementation uses both the optional CancellationToken and optional IProgress<T> parameters, it could potentially require up to four overloads:
C#public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress<T> progress);
However, many TAP implementations don't provide cancellation or progress capabilities, so they require a single method:
C#public Task MethodNameAsync(…);
If a TAP implementation supports either cancellation or progress but not both, it may provide two overloads:
C#public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
// … or …
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, IProgress<T> progress);
If a TAP implementation supports both cancellation and progress, it may expose all four overloads. However, it may provide only the following two:
C#public Task MethodNameAsync(…);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress<T> progress);
To compensate for the two missing intermediate combinations, developers may pass None or a default CancellationToken for the cancellationToken
parameter and null
for the progress
parameter.
If you expect every usage of the TAP method to support cancellation or progress, you may omit the overloads that don't accept the relevant parameter.
If you decide to expose multiple overloads to make cancellation or progress optional, the overloads that don't support cancellation or progress should behave as if they passed None for cancellation or null
for progress to the overload that does support these.
elated articles
Title | Description |
---|---|
Asynchronous Programming Patterns | Introduces the three patterns for performing asynchronous operations: the Task-based Asynchronous Pattern (TAP), the Asynchronous Programming Model (APM), and the Event-based Asynchronous Pattern (EAP). |
Implementing the Task-based Asynchronous Pattern | Describes how to implement the Task-based Asynchronous Pattern (TAP) in three ways: by using the C# and Visual Basic compilers in Visual Studio, manually, or through a combination of the compiler and manual methods. |
Consuming the Task-based Asynchronous Pattern | Describes how you can use tasks and callbacks to achieve waiting without blocking. |
Interop with Other Asynchronous Patterns and Types | Describes how to use the Task-based Asynchronous Pattern (TAP) to implement the Asynchronous Programming Model (APM) and Event-based Asynchronous Pattern (EAP). |
标签:Task,based,vs,Asynchronous,progress,asynchronous,method,TAP 来源: https://www.cnblogs.com/panpanwelcome/p/15826057.html