내용 보기

작성자

관리자 (IP : 106.247.248.10)

날짜

2022-08-01 09:26

제목

[C#] ValueTask<T> 사용 설명


How to use ValueTask in C#

Asynchronous programming has been in use for quite some time now. In recent years, it has been made more powerful with the introduction of the async and await keywords. You can take advantage of asynchronous programming to increase your application’s responsiveness and throughput.

The recommended return type of an asynchronous method in C# is Task. You should return Task<T> if you would like to write an asynchronous method that returns a value. If you would like to write an event handler, you can return void instead. Until C# 7.0 an asynchronous method could return Task, Task<T>, or void. Beginning with C# 7.0, an asynchronous method also can return ValueTask (available as part of the System.Threading.Tasks.Extensions package) or ValueTask<T>. This article presents a discussion of how we can work with ValueTask in C#.

Create a .NET Core console application project in Visual Studio

First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2019 is installed in your system, follow the steps outlined below to create a new .NET Core console application project in Visual Studio.

0 seconds of 8 minutes, 54 secondsVolume 0%
 
  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window shown next, specify the name and location for the new project.
  6. Click Create.

This will create a new .NET Core console application project in Visual Studio 2019. We’ll use this project to illustrate the use of ValueTask in the subsequent sections of this article.

Why should I use ValueTask?

A Task represents the state of some operation, i.e., whether the operation is completed, cancelled, and so on. An asynchronous method can return either a Task or a ValueTask.

Now, since Task is a reference type, returning a Task object from an asynchronous method implies allocating the object on the managed heap each time the method is called. Thus, one caveat in using Task is that you need to allocate memory in the managed heap every time you return a Task object from your method. If the result of the operation being performed by your method is available immediately or completes synchronously, this allocation is not needed and therefore becomes costly.

Here is exactly where ValueTask comes to the rescue. ValueTask<T> provides two major benefits. First, ValueTask<T> improves performance because it doesn’t need heap allocation, and second, it is both easy and flexible to implement. By returning ValueTask<T> instead of Task<T> from an asynchronous method when the result is immediately available, you can avoid the unnecessary overhead of allocation since “T” here represents a structure and a struct in C# is a value type (in contrast to the “T” in Task<T>, which represents a class).

Task and ValueTask represent two primary “awaitable” types in C#. Note that you cannot block on a ValueTask. If you need to block you should convert the ValueTask to a Task using the AsTask method and then block on that reference Task object.

Also note that each ValueTask can be consumed only once. Here the word “consume” implies that a ValueTask can asynchronously wait for (await) the operation to complete or take advantage of AsTask to convert a ValueTask to a Task. However, a ValueTask should be consumed only once, after which the ValueTask<T> should be ignored.

ValueTask example in C#

Suppose you have an asynchronous method that returns a Task. You might take advantage of Task.FromResult to create the Task object as shown in the code snippet given below.

public Task<int> GetCustomerIdAsync()
{
    return Task.FromResult(1);
}

The above code snippet does not create the entire async state machine magic but it allocates a Task object in the managed heap. To avoid this allocation, you might want to take advantage of a ValueTask instead as shown in the code snippet given below.

public ValueTask<int> GetCustomerIdAsync()
{
    return new ValueTask(1);
}

The following code snippet illustrates a synchronous implementation of ValueTask.

 public interface IRepository<T>
    {
        ValueTask<T> GetData();
    }

The Repository class extends the IRepository interface and implements its methods as shown below.

    public class Repository<T> : IRepository<T>
    {
        public ValueTask<T> GetData()
        {
            var value = default(T);
            return new ValueTask<T>(value);
        }
    }

Here is how you can call the GetData method from Main method.

static void Main(string[] args)
        {
            IRepository<int> repository = new Repository<int>();
            var result = repository.GetData();
            if(result.IsCompleted)
                 Console.WriteLine("Operation complete...");
            else
                Console.WriteLine("Operation incomplete...");
            Console.ReadKey();
        }

Let’s now add another method to our repository, this time an asynchronous method named GetDataAsync. Here is what the modified IRepository interface would look like.

public interface IRepository<T>
    {
        ValueTask<T> GetData();
        ValueTask<T> GetDataAsync();
    }

The GetDataAsync method is implemented by the Repository class as shown in the code snippet given below.

    public class Repository<T> : IRepository<T>
    {
        public ValueTask<T> GetData()
        {
            var value = default(T);
            return new ValueTask<T>(value);
        }
        public async ValueTask<T> GetDataAsync()
        {
            var value = default(T);
            await Task.Delay(100);
            return value;
        }
    }

When should I use ValueTask in C#?

Albeit the benefits that ValueTask provides, there are certain trade-offs to using ValueTask in lieu of Task. ValueTask is a value type with two fields, whereas Task is a reference type with a single field. Hence using a ValueTask means working with more data since a method call would return two fields of data in lieu of one. Also, if you await a method that returns a ValueTask, the state machine for that asynchronous method would be larger as well — because it would have to accommodate a struct that contains two fields in lieu of a single reference in the case of a Task.

Further, if the consumer of an asynchronous method uses Task.WhenAll or Task.WhenAny, using ValueTask<T> as a return type in an asynchronous method might become costly. This is because you would need to convert the ValueTask<T> to Task<T> using the AsTask method, which would incur an allocation that could be easily avoided if a cached Task<T> had been used in the first place.

Here is the rule of the thumb. Use Task when you have a piece of code that will always be asynchronous, i.e., when the operation will not immediately complete. Take advantage of ValueTask when the result of an asynchronous operation is already available or when you already have a cached result. Either way, you should perform the necessary performance analysis before considering ValueTask.


※참고 : https://www.sysnet.pe.kr/3/0/5698?pageno=0

출처1

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

출처2

https://www.csharpstudy.com/latest/CS7-async-return.aspx