C#: Someone finally clarified ValueTask, IValueTaskSource, ManualResetValueTaskSourceCore!

内容纲要

Recently, the NCC group is discussing ValueTask/ValueTask<TResult>. Dashuai (the main developer of Natasha) has recently been obsessed with algorithms and high-performance computing. He pays so much attention to this thing, which shows that there is something wrong with him. Learn it, lest there is no topic 🤣.

ValueTask/ValueTask<TResult> actually appeared earlier, and I haven’t been in-depth before, so take this opportunity to learn.

When the article says ValueTask, in order to reduce the number of words, it generally includes its generic version ValueTask<TRsult>; when it mentions Task, it also includes its generic version;

1. Available versions and reference materials

According to the reference materials on the Microsoft website, the following versions of .NET programs (sets) can use ValueTask/ValueTask<TResult>.

Version category Version requirements
.NET 5.0
.NET Core 2.1, 3.0, 3.1
.NET Standard 2.1

The following is the link address of the reference materials when the author read:

[1] [https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.valuetask?view=net-5.0](https://docs.microsoft.com/zh-cn /dotnet/api/system.threading.tasks.valuetask?view=net-5.0)

[2]] [https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.valuetask-1?view=net-5.0](https://docs.microsoft.com/ zh-cn/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](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](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](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> exists under the System.Threading.Tasks namespace, and the definition of ValueTask<TResult> is as follows:

public struct ValueTask<TResult>: IEquatable<ValueTask<TResult>>
Note to the author: The IEquatable<T> interface defines the Equals method to determine whether two instances are equal.

The definition of Task is as follows:

public class Task: IAsyncResult, IDisposable

Judging from its inherited interfaces and official documents, the complexity of ValueTask<TResult> should not be high.

According to the surface understanding of the document, this type should be a simplified version of Task. Task is a reference type, so the Task object is returned from an asynchronous method or every time an asynchronous method is called, the object will be allocated in the managed heap.

Based on the comparison, we should know:

  • Task is a reference type and will allocate memory in the managed heap; ValueTask is a value type;

At present, there is only this point to remember. Let’s continue to compare the similarities and differences between the two.

Here we try to compare Task with this type and see how the code is.

        public static async ValueTask<int> GetValueTaskAsync()
        {
            await Task.CompletedTask; // Don't get me wrong here, this is just to find a random place await
            return 666;
        }

        public static async Task<int> GetTaskAsync()
        {
            await Task.CompletedTask;
            return 666;
        }

From the code point of view, the two methods used in simple code are the same (CURD is basically the case).

3. How does the compiler compile

When Task is compiled, the state machine is generated by the compiler, and a class that inherits IAsyncStateMachine is generated for each method, and there is a lot of code packaging.

According to my test, ValueTask also generates similar code.

As shown:

《C#: Someone finally clarified ValueTask, IValueTaskSource, ManualResetValueTaskSourceCore!》

Visit https://sharplab.io/#gist:ddf2a5e535a34883733196c7bf4c55b2 to read the above code (Task) online.

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

You visit this URL separately to compare the differences.

The author has taken out the differences, and readers can take a closer look:

Task:

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

ValueTask:

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

I don’t see the difference. . .

But here is the second point:

  • If the processing speed of this method is very fast, or your code is immediately available after execution, etc., using asynchronous will not be faster than synchronous, but may consume more performance resources.

4. What are the advantages of ValueTask

From the previous content, we can see that ValueTask is the same as the state machine code generated after the Task is compiled. The real difference is that ValueTask is a value type, and Task is a reference type.

From a functional point of view, ValueTask is a simple asynchronous representation, and Task has many powerful methods and various operations.

ValueTask improves performance because it does not need to allocate memory on the heap. This is where ValueTask has an advantage over Task.

To avoid memory allocation overhead, we can use ValueTask to wrap the results that need to be returned.

        public static ValueTask<int> GetValueTask()
        {
            return new ValueTask<int>(666);
        }

        public static async ValueTask<int> StartAsync()
        {
            return await GetValueTask();
        }

But at present, we have not conducted any performance testing, which is not enough to illustrate the advantages of ValueTask in improving performance. The author will continue to explain some basic knowledge. When the time is ripe, we will conduct some tests and release sample codes.

5. ValueTask creates an asynchronous task

Let’s take a look at the constructor definitions of ValueTask and ValueTask<TResult>.

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

// ValueTask<TResult>
        public ValueTask(Task<TResult> task);
        public ValueTask(TResult result);
        public ValueTask(IValueTaskSource<TResult> source, short token);

If you create a task through Task, you can use new Task(), Task.Run(), etc. to create a task, and then you can use the async/await keyword to define an asynchronous method and start an asynchronous task. So what if you use ValueTask?

In the fourth section, we already have an example, using the ValueTask(TResult result) constructor, you can use new ValueTask yourself, and then you can use the await keyword.

In addition, there are multiple constructors of ValueTask, we can continue to dig.

Convert to ValueTask via Task:

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

There is one method of IValueTaskSource parameter type as the constructor function, we will put it to the sixth section.

The ValueTask instance can only wait once! This must be remembered!

6. IValueTaskSource and custom packaging ValueTask

About IValueTaskSource

IValueTaskSource is in the System.Threading.Tasks.Sources namespace and is defined as follows:


    public interface IValueTaskSource
    {
        void GetResult(short token);

        ValueTaskSourceStatus GetStatus(short token);

        void OnCompleted(
            Action<object?> continuation,
            object? state,
            short token,
            ValueTaskSourceOnCompletedFlags flags);
    }
Method name Function
GetResult(Int16) Get the result of IValueTaskSource, only call once when the asynchronous state machine needs to get the operation result
GetStatus(Int16) Get the status of the current operation, Called by the asynchronous state machine to check the operation status
OnCompleted(Action, Object, Int16, ValueTaskSourceOnCompletedFlags) For this IValueTaskSource plans to continue the operation, developer calls it

In this namespace, there are some types related to ValueTask, please refer to [Microsoft documentation](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.sources.ivaluetasksource? view=net-5.0).

Among the above three methods, OnCompleted is used to continue the task. Readers familiar with Task should know this method, so I won’t repeat it here.

We have an example earlier:

        public static ValueTask<int> GetValueTask()
        {
            return new ValueTask<int>(666);
        }

        public static async ValueTask<int> StartAsync()
        {
            return await GetValueTask();
        }

Simplified code after conversion by the compiler:

        public static int _StartAsync()
        {
            var awaiter = GetValueTask().GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                // some inexplicable operation code
            }

            return awaiter.GetResult();
        }

Based on this code, we found that ValueTask can be state-aware, so how to express that the task has been completed? What’s the realization principle inside?

What is IValueTaskSource

IValueTaskSource is an abstraction through which we can separate the logical behavior of task/operation from the result itself (state machine).

Simplified example:

IValueTaskSource<int> someSource = // ...
short token = // ... token
var vt = new ValueTask<int>(someSource, token); // Create task
int value = await vt; // wait for the task to complete

But from this code, we can’t see how to implement IValueTaskSource, and how IValueTaskSource is used inside ValueTask. Before delving into its principle, the author consulted from other blogs, documents and other places, in order to reduce the performance overhead of Task (introduced by C# 5.0), C# 7.0 appeared ValueTask. The emergence of ValueTask is to wrap the returned result and avoid the use of heap allocation.

Therefore, you need to use Task to convert to ValueTask:

public ValueTask(Task task); // ValueTask constructor

ValueTask just wraps the return result of Task.

Later, for higher performance, IValueTaskCource was introduced, and ValueTask added a constructor.

able to passImplement IValueTaskSource:

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

In this way, the performance overhead of the conversion between ValueTask and Task can be further eliminated. ValueTask has the ability to "manage" state and no longer depends on Task.

Let’s talk about ValueTask advantages

In the coreclr draft on August 22, 2019, there is a topic "Make "async ValueTask/ValueTask" methods ammortized allocation-free", which discusses the performance impact of ValueTask and subsequent transformation plans in depth.

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

There are various performance index comparisons, and I highly recommend readers who are interested in in-depth study to take a look at this Issue.

Don’t implement IValueTaskSource all by yourself

Most people can’t complete this interface. I personally don’t understand it many times. After searching for a long time, I haven’t found a suitable code example. According to the official documentation, I found ManualResetValueTaskSourceCore, this type implements the IValueTaskSource interface and is encapsulated, so we can use ManualResetValueTaskSourceCore to wrap our own code to implement IValueTaskSource more easily.

Regarding ManualResetValueTaskSourceCore, the usage method and code examples will be given later in the article.

ValueTaskSourceOnCompletedFlags

ValueTaskSourceOnCompletedFlags is an enumeration used to represent the behavior of continuation. The enumeration description is as follows:

Enumeration Value Description
FlowExecutionContext 2 OnCompleted should capture the current ExecutionContext and use it to run the continuation.
None 0 There are any requirements in the call method of 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 to perform a continuation at any position.

ValueTaskSourceStatus

The ValueTaskSourceStatus enumeration is used to indicate the status of IValueTaskSource or IValueTaskSource. The enumeration description is as follows:

Enumeration Value Description
Canceled 3 The operation was completed due to canceled operation.
Faulted 2 The operation has completed with errors.
Pending 0 The operation has not yet completed.
Succeeded 1 The operation has completed successfully.

7. write an IValueTaskSource instance

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

If we want to design a Redis client and implement it asynchronously, if you have Socket development experience, you will understand that Socket does not send and receive one at a time. There is also no direct asynchronous interface in Socket in C#.

So here we have to implement an asynchronous Redis client.

Use IValueTaskSource to write a state machine:

    // One can operate synchronous tasks and different threads synchronously, and construct asynchronous methods through the state machine
    public class MyValueTaskSource<TRusult>: IValueTaskSource<TRusult>
    {
        // Store the returned result
        private TRusult _result;
        private ValueTaskSourceStatus status = ValueTaskSourceStatus.Pending;

        // This task is abnormal
        private Exception exception;

        #region Implement the interface, tell the caller whether the task has been completed, whether there is a result, whether there is an exception, etc.
        // Get results
        public TRusult GetResult(short token)
        {
            // If there is an exception in this task, then when the result is obtained, re-pop
            if (status == ValueTaskSourceStatus.Faulted)
                throw exception;
            // If the task is cancelled, an exception will also pop up
            else if (status == ValueTaskSourceStatus.Canceled)
                throw new TaskCanceledException("This task has been cancelled");

            return _result;
        }

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

        // achieve continuation
        public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
        {
            // No need to continue, do not implement this interface
        }

        #endregion

        #region implements a state machine, which can control whether this task has been completed and whether there is an exception

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

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

        // The task to be performed is abnormal
        public void SetException(Exception exception)
        {
            this.exception = exception;
            status = ValueTaskSourceStatus.Faulted;
        }

        #endregion

    }

Fake Socket:

    public class fake Socket
    {
        private bool IsHaveSend = false;

        // Simulate Socket to send data to the server
        public void Send(byte[] data)
        {
            new Thread(() =>
            {
                Thread.Sleep(100);
                IsHaveSend = true;
            }).Start();
        }

        // Synchronously block waiting for server response
        public byte[] Receive()
        {
            // Simulate data transmitted over the network
            byte[] data = new byte[100];

            while (!IsHaveSend)
            {
                // When the server does not send data to the client, it has been waiting empty
            }

            // It takes time to simulate network receiving data
            Thread.Sleep(new Random().Next(0, 100));
            new Random().NextBytes(data);
            IsHaveSend = false;
            return data;
        }
    }

Implement Redis client and implement

    // Redis client
    public class RedisClient
    {
        // queue
        private readonly Queue<MyValueTaskSource<string>> queue = new Queue<MyValueTaskSource<string>>();

        private readonly fake Socket _socket = new fake Socket(); // a socket client

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

        private void SendCommand(string command)
        {
            Console.WriteLine("The client sent a command:" + command);
            _socket.Send(Encoding.UTF8.GetBytes(command));
        }

        public async ValueTask<string> GetStringAsync(string key)
        {
            // Custom state machine
            MyValueTaskSource<string> source = new MyValueTaskSource<string>();
            // Create an asynchronous task
            ValueTask<string> task = new ValueTask<string>(source, 0);

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

            // Send the command to get the value
            SendCommand($"GET {key}");

            // Use await directly, only check the removal status! The first layer must complete the task before the inspection, and then it will fall into infinite waiting after await!
            // return await task;

            // To truly realize this kind of asynchrony, you must use SynchronizationContext and other complex structural logic!
            // To avoid too much code, we can use the following infinite while method!
            var awaiter = task.GetAwaiter();
            while (!awaiter.IsCompleted) {}

            // return result
            return await task;
        }
    }

This is probably the idea. But in the end it is impossible to await directly like Task! ValueTask can only await once, and await can only be the final result check!

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

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

The implementation of these codes is more complicated, what should I do? Microsoft officially gave a ManualResetValueTaskSourceCore<TResult>, with it, we can save a lot of complicated code!

ValueTask cannot be cancelled!

8. use ManualResetValueTaskSourceCore

Next, we use ManualResetValueTaskSourceCore to transform the previous code, so that we can intuitively feel what this type is for!

Transform MyValueTaskSource as follows:

    // One can operate synchronous tasks and different threads synchronously, and construct asynchronous methods through the state machine
    public class MyValueTaskSource<TRusult>: IValueTaskSource<TRusult>
    {
        private ManualResetValueTaskSourceCore<TRusult> _source = new ManualResetValueTaskSourceCore<TRusult>();

        #region Implement the interface, tell the caller whether the task has been completed, whether there is a result, whether there is an exception, etc.
        // Get results
        public TRusult GetResult(short token)
        {
            return _source.GetResult(token);
        }

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

        // achieve continuation
        public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
        {
            _source.OnCompleted(continuation, state, token, flags);
        }

        #endregion

        #region implements a state machine, which can control whether this task has been completed and whether there is an exception

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

        // The task to be performed is abnormal
        public void SetException(Exception exception)
        {
            _source.SetException(exception);
        }

        #endregion
    }

After that, we can use await directly in GetStringAsync!

        public async ValueTask<string> GetStringAsync(string key)
        {
            // Custom state machine
            MyValueTaskSource<string> source = new MyValueTaskSource<string>();
            // Create an asynchronous task
            ValueTask<string> task = new ValueTask<string>(source, 0);

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

            // Send the command to get the value
            SendCommand($"GET {key}");

            return await task;
        }

So far, ValueTask, IValueTaskSource, ManualResetValueTaskSourceCore, have you figured it out!

Someone has implemented a lot of extensions to ValueTask, so that ValueTask has the same multi-task concurrency capability as Task, such as WhenAll, WhenAny, Factory, etc., expand the library address: [https://github.com/Cysharp/ValueTaskSupplement](https://github .com/Cysharp/ValueTaskSupplement)

For time reasons, the author of this article will not give a comparison of GC and performance in concurrency and other situations. After you learn to use it, you can test it yourself.

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注