- TaskAwaiter
- Another Method for Continuation
- Another Way to Create Tasks
- Implementing a Type that Supports Synchronous and Asynchronous Tasks
- Task.FromCanceled()
- How to Internally Cancel a Task
- Yield Keyword
- Supplementary Knowledge Points
The basics of tasks consist of three articles. This is the third article, and afterward, we will begin learning about asynchronous programming, concurrency, and asynchronous I/O.
This article will continue to discuss some APIs and common operations associated with Task.
TaskAwaiter
Let's talk about TaskAwaiter
. TaskAwaiter
represents an object that waits for an asynchronous task to complete and provides the result.
Task has a GetAwaiter()
method, which returns either TaskAwaiter
or TaskAwaiter<TResult>
. The TaskAwaiter
type is defined in the System.Runtime.CompilerServices
namespace.
The properties and methods of the TaskAwaiter
type are as follows:
Properties:
| Property | Description |
|----------|-------------|
| IsCompleted | Gets a value that indicates whether the asynchronous task has completed. |
Methods:
| Method | Description |
|--------|-------------|
| GetResult() | Ends the waiting for the asynchronous task to complete. |
| OnCompleted(Action) | Specifies an operation to be executed when the TaskAwaiter object stops waiting for the asynchronous task to complete. |
| UnsafeOnCompleted(Action) | Schedules continuation operations associated with this awaiter. |
An example of usage is shown below:
static void Main()
{
Task<int> task = new Task<int>(() =>
{
Console.WriteLine("I am the predecessor task");
Thread.Sleep(TimeSpan.FromSeconds(1));
return 666;
});
TaskAwaiter<int> awaiter = task.GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine("I will continue executing when the predecessor task is completed");
});
task.Start();
Console.ReadKey();
}
Additionally, we mentioned earlier that if an unhandled exception occurs in a task, or if the task is terminated, it is still considered a completed task.
Another Method for Continuation
In the previous section, we introduced the .ContinueWith()
method to implement continuation. Here, we will discuss another method for continuation: .ConfigureAwait()
.
The .ConfigureAwait()
method takes a Boolean value where true
attempts to marshal the continuation back to the original context; otherwise, it is false
.
Let me explain: the continuation task of .ContinueWith()
will execute on the same thread as the predecessor task once it completes. This method is synchronous, meaning both tasks run sequentially on the same thread.
The .ConfigureAwait(false)
method enables asynchronous execution; after the predecessor task completes, the subsequent task can run on any thread without regard for the original context. This feature is particularly useful in UI applications.
You can refer to: https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f
Here is how it is used:
static void Main()
{
Task<int> task = new Task<int>(() =>
{
Console.WriteLine("I am the predecessor task");
Thread.Sleep(TimeSpan.FromSeconds(1));
return 666;
});
ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine("I will continue executing when the predecessor task is completed");
});
task.Start();
Console.ReadKey();
}
ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter
has the same properties and methods as TaskAwaiter
.
Another difference between .ContinueWith()
and .ConfigureAwait(false)
is that the former can continue multiple tasks and continuation tasks (multi-layer), whereas the latter can only continue one layer of tasks (though one layer can contain multiple tasks).
Another Way to Create Tasks
As previously mentioned, there are three ways to create tasks: new Task()
, Task.Run()
, and Task.Factory.StartNew()
. Now, we will learn about the fourth method: the TaskCompletionSource<TResult>
type.
Let's take a look at the properties and methods of the TaskCompletionSource<TResult>
type:
Properties:
| Property | Description |
|----------|-------------|
| Task | Gets the Task created by this TaskCompletionSource. |
Methods:
| Method | Description |
|--------|-------------|
| SetCanceled() | Transitions the underlying Task to the Canceled state. |
| SetException(Exception) | Transitions the underlying Task to the Faulted state with a specified exception. |
| SetException(IEnumerable) | Transitions the underlying Task to the Faulted state with a collection of specified exceptions. |
| SetResult(TResult) | Transitions the underlying Task to the RanToCompletion state. |
| TrySetCanceled() | Attempts to transition the underlying Task to the Canceled state. |
| TrySetCanceled(CancellationToken) | Attempts to transition the underlying Task to the Canceled state while enabling the stored cancellation token. |
| TrySetException(Exception) | Attempts to transition the underlying Task to the Faulted state with a specified exception. |
| TrySetException(IEnumerable) | Attempts to transition the underlying Task to the Faulted state with a collection of specified exceptions. |
| TrySetResult(TResult) | Attempts to transition the underlying Task to the RanToCompletion state. |
The TaskCompletionSource<TResult>
class can control the lifecycle of a task.
First, access a Task
or Task<TResult>
through the .Task
property.
TaskCompletionSource<int> task = new TaskCompletionSource<int>();
Task<int> myTask = task.Task; // Task myTask = task.Task;
Then, control the lifecycle of myTask
through the task.xxx()
methods, but note that myTask
itself does not have any task logic.
An example of use is below:
static void Main()
{
TaskCompletionSource<int> task = new TaskCompletionSource<int>();
Task<int> myTask = task.Task; // task controls myTask
// Start a new task for experimentation
Task mainTask = new Task(() =>
{
Console.WriteLine("I can control the myTask task");
Console.WriteLine("Press any key, and I will immediately complete the myTask task");
Console.ReadKey();
task.SetResult(666);
});
mainTask.Start();
Console.WriteLine("Starting to wait for the myTask result");
Console.WriteLine(myTask.Result);
Console.WriteLine("End");
Console.ReadKey();
}
Other methods such as SetException(Exception)
can be explored on your own, and we won't elaborate on them here.
Reference materials: https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/
This article is quite good and includes illustrations: https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/
Implementing a Type that Supports Synchronous and Asynchronous Tasks
This section continues the explanation of TaskCompletionSource<TResult>
.
Here we will design a class similar to Task that supports both synchronous and asynchronous tasks.
- Users can use
GetResult()
for synchronous result retrieval; - Users can use
RunAsync()
to execute the task andResult
property to retrieve the result asynchronously.
The implementation is as follows:
/// <summary>
/// Implement a type that supports synchronous and asynchronous tasks
/// </summary>
/// <typeparam name="TResult"></typeparam>
public class MyTaskClass<TResult>
{
private readonly TaskCompletionSource<TResult> source = new TaskCompletionSource<TResult>();
private Task<TResult> task;
// Store the task the user wants to execute
private Func<TResult> _func;
// Whether the task has been completed, either synchronously or asynchronously
private bool isCompleted = false;
// Task execution result
private TResult _result;
/// <summary>
/// Retrieve the execution result
/// </summary>
public TResult Result
{
get
{
if (isCompleted)
return _result;
else return task.Result;
}
}
public MyTaskClass(Func<TResult> func)
{
_func = func;
task = source.Task;
}
/// <summary>
/// Synchronously retrieves result
/// </summary>
/// <returns></returns>
public TResult GetResult()
{
_result = _func.Invoke();
isCompleted = true;
return _result;
}
/// <summary>
/// Asynchronously executes the task
/// </summary>
public void RunAsync()
{
Task.Factory.StartNew(() =>
{
source.SetResult(_func.Invoke());
isCompleted = true;
});
}
}
In the Main method, we create an example of a task:
class Program
{
static void Main()
{
// Instantiate the task class
MyTaskClass<string> myTask1 = new MyTaskClass<string>(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
return "www.whuanle.cn";
});
// Directly get the result synchronously
Console.WriteLine(myTask1.GetResult());
// Instantiate the task class
MyTaskClass<string> myTask2 = new MyTaskClass<string>(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
return "www.whuanle.cn";
});
// Asynchronously get the result
myTask2.RunAsync();
Console.WriteLine(myTask2.Result);
Console.ReadKey();
}
}
Task.FromCanceled()
According to Microsoft's documentation: it creates a Task that is completed due to a cancellation operation from a specified cancellation token.
Here, I have copied an example:
var token = new CancellationToken(true);
Task task = Task.FromCanceled(token);
Task<int> genericTask = Task.FromCanceled<int>(token);
There are many such examples online, but what is this used for? Isn't new Task sufficient?
With this question in mind, let's explore it with an example:
public static Task Test()
{
CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
return Task.FromCanceled<object>(source.Token);
}
static void Main()
{
var t = Test(); // Set a breakpoint here to monitor variables
Console.WriteLine(t.IsCanceled);
}
Using Task.FromCanceled()
allows you to construct a canceled task. I couldn't find a good example; it is beneficial if a task is canceled before it starts.
Many examples can be found here: https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken
How to Internally Cancel a Task
Previously, we discussed using CancellationToken
to pass parameters to cancel a task. But this time, we will implement a way to cancel a task without using CancellationToken
.
We can throw a System.OperationCanceledException
exception using the ThrowIfCancellationRequested()
method of the CancellationToken
. This will terminate the task, putting it in a canceled state; however, the token must still be passed initially.
Here, I will design something slightly more complex: a class that can execute multiple tasks in order.
/// <summary>
/// An asynchronous type capable of completing multiple tasks
/// </summary>
public class MyTaskClass
{
private List<Action> _actions = new List<Action>();
private CancellationTokenSource _source = new CancellationTokenSource();
private CancellationTokenSource _sourceBak = new CancellationTokenSource();
private Task _task;
/// <summary>
/// Adds a task
/// </summary>
/// <param name="action"></param>
public void AddTask(Action action)
{
_actions.Add(action);
}
/// <summary>
/// Starts executing tasks
/// </summary>
/// <returns></returns>
public Task StartAsync()
{
// _ = new Task() is ineffective in this example
_task = Task.Factory.StartNew(() =>
{
for (int i = 0; i < _actions.Count; i++)
{
int tmp = i;
Console.WriteLine($"Task {tmp} started");
if (_source.Token.IsCancellationRequested)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Task has been canceled");
Console.ForegroundColor = ConsoleColor.White;
_sourceBak.Cancel();
_sourceBak.Token.ThrowIfCancellationRequested();
}
_actions[tmp].Invoke();
}
}, _sourceBak.Token);
return _task;
}
/// <summary>
/// Cancels the task
/// </summary>
/// <returns></returns>
public Task Cancel()
{
_source.Cancel();
// This can be omitted
_task = Task.FromCanceled<object>(_source.Token);
return _task;
}
}
In the Main
method:
static void Main()
{
// Instantiate the task class
MyTaskClass myTask = new MyTaskClass();
for (int i = 0; i < 10; i++)
{
int tmp = i;
myTask.AddTask(() =>
{
Console.WriteLine(" Task 1 Start");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine(" Task 1 End");
Thread.Sleep(TimeSpan.FromSeconds(1));
});
}
// Equivalent to Task.WhenAll()
Task task = myTask.StartAsync();
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine($"Is task canceled: {task.IsCanceled}");
// Cancel the task
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Press any key to cancel the task");
Console.ForegroundColor = ConsoleColor.White;
Console.ReadKey();
var t = myTask.Cancel(); // Cancel the task
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine($"Is task canceled: 【{task.IsCanceled}】");
Console.ReadKey();
}
You can cancel the task at any stage.
Yield keyword
The iterator keyword allows data to be returned one at a time when needed, which is also quite asynchronous.
When an iterator method reaches a yield return
statement, it returns an expression
and retains its position in the code. The next time the iterator function is called, execution resumes from that position.
You can use the yield break
statement to terminate the iteration.
Official documentation: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield
Most of the online examples are with foreach
, and some classmates do not understand what this really means. Here, the author briefly explains.
We can also write an example like this:
This time there is no foreach
.
private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
private static IEnumerable<int> ForAsync()
{
int i = 0;
while (i < list.Length)
{
i++;
yield return list[i];
}
}
However, a student asked, does this return object need to implement IEnumerable<T>
? What are those documents talking about regarding iterator interfaces?
We can modify the example a bit:
private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
private static IEnumerable<int> ForAsync()
{
int i = 0;
while (i < list.Length)
{
int num = list[i];
i++;
yield return num;
}
}
Can you call this in the Main
method and see if it runs normally?
static void Main()
{
foreach (var item in ForAsync())
{
Console.WriteLine(item);
}
Console.ReadKey();
}
This shows that the object returned by yield return
does not need to implement the IEnumerable<int>
method.
In fact, yield
is syntactic sugar; you only need to call it within a loop.
static void Main()
{
foreach (var item in ForAsync())
{
Console.WriteLine(item);
}
Console.ReadKey();
}
private static IEnumerable<int> ForAsync()
{
int i = 0;
while (i < 100)
{
i++;
yield return i;
}
}
}
It will automatically generate IEnumerable<T>
without the need for you to implement IEnumerable<T>
first.
Additional Knowledge points
- There are various methods for thread synchronization: Critical Section, Mutex, Semaphores, Events, Tasks;
Task.Run()
andTask.Factory.StartNew()
wrap the Task;Task.Run()
is a simplified version ofTask.Factory.StartNew()
;- In some cases,
new Task()
is ineffective; however,Task.Run()
andTask.Factory.StartNew()
can be used;
This article is the conclusion of the basics of tasks in C#. Up to now, the C# multithreading series has completed 15 articles, and I will continue to explore more usage methods and scenarios of multithreading and tasks in the future.
If you like my author, remember to follow me~
文章评论