C#: Finally, Someone Explained ValueTask, IValueTaskSource, and ManualResetValueTaskSourceCore Clearly!

2020年12月3日 128点热度 0人点赞 10条评论
内容目录

Recently, there was a discussion in the NCC group about ValueTask/ValueTask<TResult>. Da Shua (Natasha, the main developer) has been focused on algorithms and high-performance computing lately. His attention to this topic suggests it has potential, so I will secretly learn about it to keep up with the conversation 🤣.

ValueTask/ValueTask<TResult> has been around for a while, and I haven't delved into it deeply before, so I intend to take this opportunity to study it thoroughly.

When referring to ValueTask in this article, it generally includes its generic version ValueTask<TResult>; similarly, when mentioning Task, it encompasses its generic version as well.

1. Available Versions and References

According to Microsoft’s official documentation, the following versions of .NET programs can use ValueTask/ValueTask<TResult>.

| Version Category | Version Requirement |
| :---------------- | :------------------- |
| .NET | 5.0 |
| .NET Core | 2.1, 3.0, 3.1 |
| .NET Standard | 2.1 |

Below are the reference links used in this article:

【1】 https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask?view=net-5.0

【2】 https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1?view=net-5.0

【3】 https://www.infoworld.com/article/3565433/how-to-use-valuetask-in-csharp.html

【4】 https://tooslowexception.com/implementing-custom-ivaluetasksource-async-without-allocations/

【5】 https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html

【6】 https://qiita.com/skitoy4321/items/31a97e03665bd7bcc8ca

【7】 https://neuecc.medium.com/valuetasksupplement-an-extensions-to-valuetask-4c247bc613ea

【8】https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Threading/Tasks/Sources/ManualResetValueTaskSourceCore.cs

2. ValueTask<TResult> and Task

ValueTask<TResult> is located in the System.Threading.Tasks namespace, and its definition is as follows:

public struct ValueTask&lt;TResult&gt; : IEquatable&lt;ValueTask&lt;TResult&gt;&gt;
Note from the author: The IEquatable<T> interface defines the Equals method to determine if two instances are equal.

Whereas, the definition of Task is as follows:

public class Task : IAsyncResult, IDisposable

From their inherited interfaces and the official documentation, ValueTask<TResult> should not be very complex.

Based on the superficial understanding of the documentation, this type appears to be a simplified version of Task. Since Task is a reference type, returning a Task object from an asynchronous method or invoking an asynchronous method each time would allocate that object on the managed heap.

By comparison, we should know:

  • Task is a reference type and allocates memory on the managed heap; ValueTask is a value type;

Currently, this is the only point to remember, so let’s continue comparing the similarities and differences between the two.

Let's try using this type to compare with Task and see how the code looks.

        public static async ValueTask&lt;int&gt; GetValueTaskAsync()
        {
            await Task.CompletedTask; // Don't get confused; I'm just awaiting on a place randomly
            return 666;
        }

        public static async Task&lt;int&gt; GetTaskAsync()
        {
            await Task.CompletedTask;
            return 666;
        }

From the code, both methods use the same approach in simple code (CURD is basically like this).

3. How the Compiler Compiles

When compiling Task, the compiler generates a state machine, creating a class for each method that inherits from IAsyncStateMachine, resulting in a significant amount of code wrapping.

I have tested that ValueTask also generates similar code.

For example:

Compiled Task

Visit https://sharplab.io/#gist:ddf2a5e535a34883733196c7bf4c55b2 to read the compiled code for Task online.

Visit https://sharplab.io/#gist:7129478fc630a87c08ced38e7fd14cc0 to read the ValueTask example code online.

You can visit the URLs to compare the differences.

I have extracted the differing parts; readers can take a close look:

Task:

    [AsyncStateMachine(typeof(&lt;GetTaskAsync&gt;d__0))]
    [DebuggerStepThrough]
    public static Task&lt;int&gt; GetTaskAsync()
    {
        &lt;GetTaskAsync&gt;d__0 stateMachine = new &lt;GetTaskAsync&gt;d__0();
        stateMachine.&lt;&gt;t__builder = AsyncTaskMethodBuilder&lt;int&gt;.Create();
        stateMachine.&lt;&gt;1__state = -1;
        AsyncTaskMethodBuilder&lt;int&gt; &lt;&gt;t__builder = stateMachine.&lt;&gt;t__builder;
        &lt;&gt;t__builder.Start(ref stateMachine);
        return stateMachine.&lt;&gt;t__builder.Task;
    }

ValueTask:

    [AsyncStateMachine(typeof(&lt;GetValueTaskAsync&gt;d__0))]
    [DebuggerStepThrough]
    public static ValueTask&lt;int&gt; GetValueTaskAsync()
    {
        &lt;GetValueTaskAsync&gt;d__0 stateMachine = new &lt;GetValueTaskAsync&gt;d__0();
        stateMachine.&lt;&gt;t__builder = AsyncValueTaskMethodBuilder&lt;int&gt;.Create();
        stateMachine.&lt;&gt;1__state = -1;
        AsyncValueTaskMethodBuilder&lt;int&gt; &lt;&gt;t__builder = stateMachine.&lt;&gt;t__builder;
        &lt;&gt;t__builder.Start(ref stateMachine);
        return stateMachine.&lt;&gt;t__builder.Task;
    }

I can't see any significant differences...

However, this brings up another point:

  • If the processing speed of this method is very quick, or if your code is available immediately after execution, using async may not be faster than synchronous, and might actually consume more performance resources.

4. What Are the Advantages of ValueTask

From the previous content, we can conclude that the compiled state machine code for both ValueTask and Task is identical. Therefore, the true difference lies in the fact that ValueTask is a value type, whereas Task is a reference type.

Functionally, ValueTask is a simpler asynchronous representation, while Task provides many powerful methods with various intricate manipulations.

ValueTask improves performance because it does not require heap allocation for memory, which is its advantage over Task.

To avoid the overhead of memory allocation, we can use ValueTask to wrap the results we need to return.

        public static ValueTask&lt;int&gt; GetValueTask()
        {
            return new ValueTask&lt;int&gt;(666);
        }

        public static async ValueTask&lt;int&gt; StartAsync()
        {
            return await GetValueTask();
        }

However, at present, we have not conducted any performance testing to sufficiently demonstrate the advantages of ValueTask in enhancing performance. I will continue to explain some foundational knowledge and plan to conduct tests later to provide example code.

5. Creating Asynchronous Tasks with ValueTask

Let's look at the constructor definitions for ValueTask and ValueTask<TResult>.

// ValueTask
        public ValueTask(Task task);
        public ValueTask(IValueTaskSource source, short token);

// ValueTask&lt;TResult&gt;
        public ValueTask(Task&lt;TResult&gt; task);
        public ValueTask(TResult result);
        public ValueTask(IValueTaskSource&lt;TResult&gt; source, short token);

If a Task is created using new Task() or Task.Run(), then the async/await keywords can be used to define an asynchronous method and start an asynchronous task. So how do we create using ValueTask?

As we mentioned in the fourth section, we already have an example using the ValueTask(TResult result) constructor, where we can create a new ValueTask and then utilize the await keyword.

Moreover, there are multiple constructors for ValueTask that we can explore further.

Converting a Task to ValueTask:

        public static async ValueTask&lt;int&gt; StartAsync()
        {
            Task&lt;int&gt; task = Task.Run&lt;int&gt;(() =&gt; 666);
            return await new ValueTask&lt;int&gt;(task);
        }

The remaining method with IValueTaskSource as a parameter type for the constructor will be discussed in section 6.

A ValueTask instance can only be awaited once! This must be remembered!

6. IValueTaskSource and Custom Wrapping of ValueTask

About IValueTaskSource

IValueTaskSource is located in the System.Threading.Tasks.Sources namespace, and its definition is as follows:

    public interface IValueTaskSource
    {
        void GetResult(short token);

        ValueTaskSourceStatus GetStatus(short token);

        void OnCompleted(
            Action&lt;object?&gt; continuation, 
            object? state, 
            short token, 
            ValueTaskSourceOnCompletedFlags flags);
    }

| Method Name | Purpose |
| -------------------------------------------------- | ------------------------------------------------------------ |
| GetResult(Int16) | Gets the result of the IValueTaskSource, should be called once when the asynchronous state machine needs to retrieve the operation's result |
| GetStatus(Int16) | Gets the current status of the operation, to be called by the asynchronous state machine to check the operation's status |
| OnCompleted(Action, Object, Int16, ValueTaskSourceOnCompletedFlags) | Plans the continuation operation for this IValueTaskSource, to be invoked by the developer |

In this namespace, there are also several types related to ValueTask, which can be referenced from Microsoft Documentation.

Among these three methods, OnCompleted is used to continue the task. Readers familiar with Task should already be clear on this point, so I won't elaborate further.

In the previous example:

        public static ValueTask&lt;int&gt; GetValueTask()
        {
            return new ValueTask&lt;int&gt;(666);
        }

        public static async ValueTask&lt;int&gt; StartAsync()
        {
            return await GetValueTask();
        }

The simplified code after compiler transformation is:

        public static int _StartAsync()
        {
            var awaiter = GetValueTask().GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                // Some mysterious operation codes
            }

            return awaiter.GetResult();
        }

Based on this code, we observe that ValueTask can have state awareness; now the question is how does it express that the task has been completed? What implementation principles lie behind this?

What is IValueTaskSource

IValueTaskSource is an abstraction that allows us to separate the logical behaviors and results of tasks/operations from their representation (state machine).

Simplified Example:

IValueTaskSource&lt;int&gt; someSource = // ...
short token =                      // ... token
var vt = new ValueTask&lt;int&gt;(someSource, token);  // Create task
int value = await vt;						     // Wait for task completion

However, from this piece of code, we cannot see how IValueTaskSource is implemented and how ValueTask internally utilizes IValueTaskSource. Before diving into the underlying principles, the author researched blogs, documents, and found that to reduce the performance overhead of Task (introduced in C# 5.0), ValueTask was introduced in C# 7.0. The purpose of ValueTask is to wrap the return result and avoid heap allocation.

Thus, to convert a Task to a ValueTask, we use:

public ValueTask(Task task);		// ValueTask constructor

ValueTask merely wraps the return result of a Task.

Later, for higher performance, IValueTaskSource was introduced, and ValueTask gained an additional constructor.

It can be implemented through IValueTaskSource:

public ValueTask(IValueTaskSource source, short token);    // ValueTask constructor

This further eliminates the performance overhead of converting ValueTask to Task. ValueTask gains state "management" capabilities and no longer relies on Task.

Advantages of ValueTask

In the coreclr draft of 2019-8-22, there was a theme "Make 'async ValueTask/ValueTask' methods amortized allocation-free", which deeply discussed the performance impact of ValueTask and subsequent improvement plans.

Issue link: https://github.com/dotnet/coreclr/pull/26310

There are various performance metrics comparisons within that issue. The author highly recommends interested readers to take a look at this issue for further study.

Do Not Implement IValueTaskSource Yourself

Most people cannot complete this interface; many times, I personally did not understand it well, searched for a long time, and did not find suitable code examples. According to the official documentation, I discovered ManualResetValueTaskSourceCore, which implements the IValueTaskSource interface and is encapsulated. Therefore, we can use ManualResetValueTaskSourceCore to wrap our own code and more easily implement IValueTaskSource.

Regarding ManualResetValueTaskSourceCore, methods and code examples will be provided later in the article.

ValueTaskSourceOnCompletedFlags

ValueTaskSourceOnCompletedFlags is an enumeration used to indicate continuation behavior, with the following description:

| Enum | Value | Description |
|---------------------------|-------|------------------------------------------------------------|
| FlowExecutionContext | 2 | OnCompleted should capture the current ExecutionContext and use it to run the continuation. |
| None | 0 | No specific requirements on how to call the continuation. |
| UseSchedulingContext | 1 | OnCompleted should capture the current scheduling context (SynchronizationContext) and use it when adding the continuation to the execution queue. If this flag is not set, the implementation can choose any location to execute the continuation. |

ValueTaskSourceStatus

ValueTaskSourceStatus enumeration is used to indicate the status of IValueTaskSource or IValueTaskSource, with the following description:

| Enum | Value | Description |
|------------|-------|----------------------------|
| Canceled | 3 | The operation completed due to a cancellation operation. |
| Faulted | 2 | The operation has completed but with an error. |
| Pending | 0 | The operation has not yet completed. |
| Succeeded | 1 | The operation has successfully completed. |

Implementing IValueTaskSource Instance

Full code: https://github.com/whuanle/RedisClientLearn/issues/1

Suppose we want to design a Redis client and implement it asynchronously. If you have experience with socket development, you will understand that socket communication is not just a one-send-one-receive model. C# sockets do not have direct asynchronous interfaces.

Therefore, we need to implement an asynchronous Redis client.

Using IValueTaskSource to write the state machine:

// A state machine that can construct asynchronous methods through synchronous tasks and synchronize operations across different threads
public class MyValueTaskSource&lt;TRusult&gt; : IValueTaskSource&lt;TRusult&gt;
{
    // Store the return result
    private TRusult _result;
    private ValueTaskSourceStatus status = ValueTaskSourceStatus.Pending;

    // Exception for this task
    private Exception exception;

    #region Implementing the interface, informing the caller whether the task is completed, whether there is a result, whether there is an exception, etc.
    // Get result
    public TRusult GetResult(short token)
    {
        // If this task has an exception, rethrow it when getting the result
        if (status == ValueTaskSourceStatus.Faulted)
            throw exception;
        // If the task was canceled, throw a cancellation exception
        else if (status == ValueTaskSourceStatus.Canceled)
            throw new TaskCanceledException(&quot;This task has been canceled&quot;);

        return _result;
    }

    // Get status, token is not used in this example
    public ValueTaskSourceStatus GetStatus(short token)
    {
        return status;
    }

    // Implement continuation
    public void OnCompleted(Action&lt;object&gt; continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
    {
        // No need for continuation, do not implement this interface
    }

    #endregion

    #region Implementing the state machine, able to control whether this task has completed and whether there is an exception

    // Complete the task and provide the result
    public void SetResult(TRusult result)
    {
        status = ValueTaskSourceStatus.Succeeded;  // This task has completed
        _result = result;
    }

    // Cancel the task
    public void Cancel()
    {
        status = ValueTaskSourceStatus.Canceled;
    }

    // Exception occurred during task execution
    public void SetException(Exception exception)
    {
        this.exception = exception;
        status = ValueTaskSourceStatus.Faulted;
    }

    #endregion

}

Fake Socket:

public class 假的Socket
{
    private bool IsHaveSend = false;

    // Simulate socket sending data to the server
    public void Send(byte[] data)
    {
        new Thread(() =&gt;
        {
            Thread.Sleep(100);
            IsHaveSend = true;
        }).Start();
    }

    // Synchronize blocking waiting for server response
    public byte[] Receive()
    {
        // Simulated network transport data
        byte[] data = new byte[100];

        while (!IsHaveSend)
        {
            // Empty wait as long as the server has not sent data to the client
        }

        // Simulate network data reception delay
        Thread.Sleep(new Random().Next(0, 100));
        new Random().NextBytes(data);
        IsHaveSend = false;
        return data;
    }
}

Implementing the Redis client:

// Redis client
public class RedisClient
{
    // Queue
    private readonly Queue&lt;MyValueTaskSource&lt;string&gt;&gt; queue = new Queue&lt;MyValueTaskSource&lt;string&gt;&gt;();

    private readonly 假的Socket _socket = new 假的Socket();  // A socket client

    public RedisClient(string connectStr)
    {
        new Thread(() =&gt;
        {
            while (true)
            {
                byte[] data = _socket.Receive();
                // Take a state machine from the queue
                if (queue.TryDequeue(out MyValueTaskSource&lt;string&gt; source))
                {
                    // Set the result of this state machine
                    source.SetResult(Encoding.UTF8.GetString(data));
                }
            }
        }).Start();
    }

    private void SendCommand(string command)
    {
        Console.WriteLine(&quot;Client sent a command:&quot; + command);
        _socket.Send(Encoding.UTF8.GetBytes(command));
    }

    public async ValueTask&lt;string&gt; GetStringAsync(string key)
    {
        // Custom state machine
        MyValueTaskSource&lt;string&gt; source = new MyValueTaskSource&lt;string&gt;();
        // Create asynchronous task
        ValueTask&lt;string&gt; task = new ValueTask&lt;string&gt;(source, 0);

        // Enqueue
        queue.Enqueue(source);

        // Send command to get value
        SendCommand($&quot;GET {key}&quot;);

        // Directly using await will only check the removal status! One layer must complete the task before the await, otherwise it will fall into an infinite wait!
        // return await task;

        // To truly realize this async feature, complex structural logic with `SynchronizationContext` etc. must be used!
        // To avoid excessive code, we can use the following infinite while method!
        var awaiter = task.GetAwaiter();
        while (!awaiter.IsCompleted) { }

        // Return the result
        return await task;
    }
}

The general idea is like this. However, in the end, it is not possible to directly await like Task! ValueTask can only be awaited once, and await can only be for the final result check!

If we use TaskCompletionSource to write a Task state machine, we can directly await.

If you want to truly implement a ValueTask that can be awaited, you must implement SynchronizationContext, TaskScheduler, etc., when writing IValueTaskSource.

.

Implementing this code is quite complex. What should we do? Microsoft has officially provided a ManualResetValueTaskSourceCore<TResult>, which can help us save a lot of complicated code!

ValueTask is non-cancellable!

8. Using ManualResetValueTaskSourceCore

Next, we will modify the previous code using ManualResetValueTaskSourceCore, so we can intuitively understand what this type is for!

Revamping MyValueTaskSource as follows:

    // A class that can build asynchronous methods through state machines from synchronous tasks and operations across different threads
    public class MyValueTaskSource&lt;TRusult&gt; : IValueTaskSource&lt;TRusult&gt;
    {
        private ManualResetValueTaskSourceCore&lt;TRusult&gt; _source = new ManualResetValueTaskSourceCore&lt;TRusult&gt;();

        #region Implementing the interface to inform the caller whether the task has been completed, and whether there are results or exceptions, etc.
        // Get result
        public TRusult GetResult(short token)
        {
            return _source.GetResult(token);
        }

        // Get status; in this example, the token is not needed
        public ValueTaskSourceStatus GetStatus(short token)
        {
            return _source.GetStatus(token);
        }

        // Implement continuation
        public void OnCompleted(Action&lt;object&gt; continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
        {
            _source.OnCompleted(continuation, state, token, flags);
        }

        #endregion

        #region Implementing a state machine to control whether this task has completed and if there are any exceptions

        // Complete the task and provide the result
        public void SetResult(TRusult result)
        {
            _source.SetResult(result);
        }

        // An exception occurred during the execution of the task
        public void SetException(Exception exception)
        {
            _source.SetException(exception);
        }

        #endregion
    }

After that, we can directly use await in GetStringAsync!

        public async ValueTask&lt;string&gt; GetStringAsync(string key)
        {
            // Custom state machine
            MyValueTaskSource&lt;string&gt; source = new MyValueTaskSource&lt;string&gt;();
            // Create asynchronous task
            ValueTask&lt;string&gt; task = new ValueTask&lt;string&gt;(source, 0);

            // Enqueue to the queue
            queue.Enqueue(source);

            // Send command to get value
            SendCommand($&quot;GET {key}&quot;);

            return await task;
        }

By this point, you should have understood ValueTask, IValueTaskSource, and ManualResetValueTaskSourceCore!

Someone has implemented a large number of extensions for ValueTask, allowing it to have the same concurrent capabilities as Task, such as WhenAll, WhenAny, Factory, etc. The extension library can be found at: https://github.com/Cysharp/ValueTaskSupplement

Due to time constraints, this article does not cover the GC and performance comparisons in concurrent and other scenarios. Once you've learned to use them, you can conduct your own tests.

痴者工良

高级程序员劝退师

文章评论