SoFunction
Updated on 2025-03-08

C# Several common TAP asynchronous operations

In the previous article in this series15: Basics of Asynchronous ProgrammingIn ], we mentioned that modern applications are widely used in task-based asynchronous programming mode (TAP), and historical EAP and AMP modes are outdated and not recommended. Today I will continue to summarize the asynchronous operations of TAP, such as canceling tasks, reporting progress, (), ConfigureAwait() and parallel operations, etc.

Although task state is rarely used in actual TAP programming, it is the basis of many TAP operating mechanisms, so let’s start with task state.

1 Task Status

The Task class provides a life cycle for asynchronous operations, which is represented by the TaskStatus enumeration, and has the following values:

public enum TaskStatus
{
    Created = 0,
    WaitingForActivation = 1,
    WaitingToRun = 2,
    Running = 3,
    WaitingForChildrenToComplete = 4,
    RanToCompletion = 5,
    Canceled = 6,
    Faulted = 7
}

Among them, the Canceled, Faulted and RanToCompletion states are considered as the final state of the task. Therefore, if the task is in the final state, its IsCompleted property is a true value.

Manually control task startup

To support manual control of task startup and to support the separation of construction and calls, the Task class provides a Start method. Tasks created by the Task constructor are called cold tasks because their lifecycle is in Created and will only be started if the instance's Start method is called.

The task state is not used much. Generally, we may use it when encapsulating a task-related method. For example, in the following example, it is necessary to determine that a task meets certain conditions before it can be started:

static void Main(string[] args)
{
    MyTask t = new(() =>
    {
        // do something.
    });

    StartMyTask(t);

    ();
}

public static void StartMyTask(MyTask t)
{
    if ( ==  && >10)
    {
        ();
    }
    else
    {
        // Simulate count here until Counter>10 and execute Start        while ( <= 10)
        {
            // Do something
            ++;
        }
        ();
    }
}

public class MyTask : Task
{
    public MyTask(Action action) : base(action)
    {
    }

    public int Counter { get; set; }
}

Similarly, if a state other than the state, we call it a hot task. The hot task must have been activated by the Start method.

Make sure the task is activated

Note that all tasks returned from the TAP method must be activated, such as the following code:

MyTask task = new(() =>
{
    ("Do something.");
});

// Call it elsewhereawait task;

Before await, the task was not executed and activated, and the program will wait for await. So if a TAP method internally uses the Task constructor to instantiate the Task to be returned, the TAP method must call Start on the Task object before returning it.

2 Mission cancellation

In TAP, cancellation is optional for both asynchronous method implementers and consumers. If an operation allows cancellation, it exposes an overload of an asynchronous method that accepts a cancellation token (CancellationToken instance). By convention, the parameter is named cancellationToken. For example:

public Task ReadAsync(
    byte [] buffer, int offset, int count,
    CancellationToken cancellationToken)

Asynchronous operation will monitor whether the token has a cancel request. If a cancel request is received, it can choose to cancel the operation, as shown in the following example to monitor the cancel request of the token via while:

static void Main(string[] args)
{
    CancellationTokenSource source = new();
    CancellationToken token = ;

    var task = DoWork(token);

    // The actual situation may be that other threads request cancellation later    (100);
    ();

    ($"The status returned by the task after cancellation:{}");

    ();
}

public static Task DoWork(CancellationToken cancellationToken)
{
    while (!)
    {
        // Do something.
        (1000);

        return ;
    }
    return (cancellationToken);
}

If the cancellation request causes the work to end early, and even receive the cancellation before it even begins, the TAP method returns a task ending with the Canceled state, with its IsCompleted property true and no exception is thrown. When a task is completed in the Canceled state, any continuation task registered with the task will still be called and executed unless an option such as NotOnCanceled is specified to select Not Continuation.

However, if the asynchronous task receives a cancel request while working, the asynchronous operation can also choose not to end immediately, but to wait until the currently executed work is completed before ending, and return to the task in the RanToCompletion state; it can also terminate the current work and force it to end, and return the Canceled or Faulted state based on the actual business situation and whether the production exception result is produced.

For business methods that cannot be cancelled, do not provide overloads that accept cancel tokens, which helps to show the caller whether the target method can be cancelled.

3 Progress Report

Almost all asynchronous operations can provide progress notifications, which are often used to update the user interface with progress information of asynchronous operations.

In TAP, progress is processed through the IProgress<T> interface, which is passed as a parameter to the asynchronous method. Here is a typical usage example:

static void Main(string[] args)
{
    var progress = new Progress&lt;int&gt;(n =&gt;
    {
        ($"Current progress:{n}%");
    });

    var task = DoWork(progress);

    ();
}

public static async Task DoWork(IProgress&lt;int&gt; progress)
{
    for (int i = 1; i &lt;= 100; i++)
    {
        await (100);
        if (i % 10 == 0)
        {
            progress?.Report(i);
        };
    }
}

The output is as follows:

Current progress: 10%
Current progress: 20%
Current progress: 30%
Current progress: 40%
Current progress: 50%
Current progress: 60%
Current progress: 70%
Current progress: 80%
Current progress: 90%
Current progress: 100%

The IProgress<T> interface supports different progress implementations, which are determined by the consumption code. For example, the consumption code may only care about the latest progress updates, or wish to buffer all updates, or wish to call an action for each update, etc. All of these options can be achieved by using this interface and customized to the needs of a specific consumer. For example, if the ReadAsync method earlier in this article can report progress in the form of the number of bytes currently read, the progress callback can be an IProgress<long> interface.

public Task ReadAsync(
    byte[] buffer, int offset, int count,
    IProgress<long> progress)

For example, the FindFilesAsync method returns a list of all files that meet a specific search pattern. The progress callback can provide the percentage of work completion and the current partial result set. It can provide this information with a tuple.

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<Tuple<double, ReadOnlyCollection<List<FileInfo>>>> progress)

Or use API-specific data types:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)

If the implementation of TAP provides an overload that accepts the IProgress<T> parameter, they must allow the parameter to be empty, in which case the progress is not reported. The IProgress<T> instance can be used as a standalone object, allowing the caller to decide how and where to process this progress information.

4 Concessions

Let's first look at a code of ():

(async () =>
{
    for(int i=0; i<10; i++)
    {
        await ();
        ...
    }
});

The () here actually did nothing, it returned an empty task. So what's the use of await an empty task that does nothing?

We know that for computers, task scheduling is arranged to execute threads according to certain priority strategies. If there are too many tasks and not enough threads, the tasks will enter a queue state. Yield's function is to give up the waiting position and let the tasks excluded later take the lead. It literally means concessions. When a task makes concessions, other tasks can be assigned threads to be executed as soon as possible. To give a real life example, it’s like when you are queuing up for business, you finally got you, but you are not in a hurry. You voluntarily give up your seat and let others handle it first, pretending that you have something to do and go out for a while and don’t do anything, and then come back to queue up again. I was a great charity in silence.

The () method is to introduce a concession point into the asynchronous method. When the code is executed to the concession point, control will be given up, and you will go around the thread pool for a while and don’t do anything before you come back and queue up again.

5 Customize the follow-up operations of asynchronous tasks

We can customize the subsequent operations completed by asynchronous tasks. Two common methods are ConfigureAwait and ContinueWith.

ConfigureAwait

Let's first look at a piece of code in Windows Form:

private void button1_Click(object sender, EventArgs e)
{
    var content = CurlAsync().Result;
    ...
}

private async Task<string> CurlAsync()
{
    using (var client = new HttpClient())
    {
        returnawait ("");
    }
}

I believe everyone knows that CurlAsync().Result code will cause deadlocks in Windows Form programs. The reason is that when the main UI thread executes this code, it starts waiting for the result of the asynchronous task and is in a blocking state. However, when the asynchronous task was executed after it was completed and was about to find the UI thread to continue executing the subsequent code, it found that the UI thread was always in a "busy" state and had no time to take care of the asynchronous task. This has caused an embarrassing situation where you are waiting for me and I am waiting for you again.

Of course, this deadlock will only happen in Winform and early WebForm, and will not produce deadlocks in Console and Web API applications.

The solution is very simple. As an asynchronous method caller, we just need to use await instead:

private async void button1_Click(object sender, EventArgs e)
{
    var content = await CurlAsync();
    ...
}

Inside the asynchronous method, we can also call the task's ConfigureAwait(false) method to solve this problem. like:

private async Task<string> CurlAsync()
{
    using (var client = new HttpClient())
    {
        returnawait client
            .GetStringAsync("")
            .ConfigureAwait(false);
    }
}

Although both methods are feasible, if we are async method provider, such as encapsulating a general library, considering that there will inevitably be novices who will use CurlAsync().Result, in order to improve the fault tolerance of the general library, we may need to use ConfigureAwait for compatibility.

The function of ConfigureAwait(false) is to tell the main thread that I am going to travel far, you can do other things, don't wait for me. As long as you first make sure that one party is not waiting for the other party, you can avoid waiting for each other and causing deadlocks.

ContinueWith

The ContinueWith method is easy to understand, and it means literally. The function is to arrange the subsequent work to be executed after the asynchronous task is completed. Sample code:

private void Button1_Click(object sender, EventArgs e)
{
    var backgroundScheduler = ;
    var uiScheduler = ();
    
        .StartNew(_ => DoBackgroundComputation(), backgroundScheduler)
        .ContinueWith(_ => UpdateUI(), uiScheduler)
        .ContinueWith(_ => DoAnotherBackgroundComputation(), backgroundScheduler)
        .ContinueWith(_ => UpdateUIAgain(), uiScheduler);
}

As mentioned above, you can continue to write it in a chain form, and the tasks will be executed in sequence, and one will be executed before the next one will be executed. If the status returned by one of the tasks is Canceled, subsequent tasks will also be cancelled. There are a lot of overloads in this method, and you can check the document when it is actually used.

6 Summary

The content of this article is relatively basic TAP asynchronous operation knowledge points. The TAP of C# is very powerful and provides many APIs, which are far more than what is mentioned in this article, and are all centered around Task. The key is to understand the basic operations well in order to flexibly use more advanced functions. Hope this article helps you.

The above are the detailed contents of several common TAP asynchronous operations in c#. For more information about TAP asynchronous operations in c#, please pay attention to my other related articles!