当前位置:网站首页>One task is not enough, but another valuetask. I'm really confused!

One task is not enough, but another valuetask. I'm really confused!

2020-11-10 10:45:03 One line code agriculture

One : background

1. Tell a story

A few days ago, I used MemoryStream When I found ReadAsync Method has one more return ValueTask overloaded , what the fuck , One Task Enough to learn , Another one ValueTask, dizzy , Method signature is as follows :


    public class MemoryStream : Stream
    {
        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default(CancellationToken))
        {
        }
    }

Since it's new , I'm more curious , Look at this. ValueTask It's something , Flip through the source code to see the class definition :


    public readonly struct ValueTask<TResult> : IEquatable<ValueTask<TResult>>
    {
    }

It turned out to be a The value type Task, Countless optimization experiences tell me , Value types save more space than reference types , If you don't believe it, you can use windbg Check it out , Respectively in List Pour in 1000 individual Task and 1000 individual ValueTask, Look at the amount of space taken up .


0:000> !clrstack -l
OS Thread Id: 0x44cc (0)
        Child SP               IP Call Site
0000004DA3B7E630 00007ffaf84329a6 ConsoleApp2.Program.Main(System.String[]) [E:\net5\ConsoleApp1\ConsoleApp2\Program.cs @ 17]
    LOCALS:
        0x0000004DA3B7E6E8 = 0x000001932896ac78
        0x0000004DA3B7E6E0 = 0x000001932897e700
0:000> !objsize 0x000001932896ac78
sizeof(000001932896AC78) = 80056 (0x138b8) bytes (System.Collections.Generic.List`1[[System.Threading.Tasks.Task`1[[System.Int32, System.Private.CoreLib]], System.Private.CoreLib]])
0:000> !objsize 0x000001932897e700
sizeof(000001932897E700) = 16056 (0x3eb8) bytes (System.Collections.Generic.List`1[[System.Threading.Tasks.ValueTask`1[[System.Int32, System.Private.CoreLib]], System.Private.CoreLib]])

The code above shows , 1000 individual Task Need to occupy 80056 byte,1000 individual ValueTask Need to occupy 16056 byte, The difference is about 5 times , The space utilization rate has been greatly improved , In addition to this , ValueTask What else do you want to solve ?

Two :ValueTask Principle analysis

1. from MemoryStream In search of answers

You can think about it , since MemoryStream One more ReadAsync Expand , It must be extant ReadAsync Can't satisfy some business , What business can't be satisfied with ? The answer can only be found in the method source code , The simplified code is as follows :


public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
    {
        return Task.FromCanceled<int>(cancellationToken);
    }

    int num = Read(buffer, offset, count);
    Task<int> lastReadTask = _lastReadTask;
    return (lastReadTask != null && lastReadTask.Result == num) ? lastReadTask : (_lastReadTask = Task.FromResult(num));
}

After reading this code , I don't know if you have any doubts ? Anyway, I have doubts .

2. My doubts

1) asynchronous It's packed cpu Intensive operation

C# The introduction of asynchrony is essentially used to solve IO intensive Scene , Using the powerful intervention of the disk drive, the calling thread is released , Improve thread utilization and throughput , And it's exactly here ReadAsync Medium Read It's actually a simple pure memory operation , That is to say CPU intensive Scene , In fact, there is no effect of asynchronous processing at this time , Seriously, it's asynchronous for the sake of asynchrony , Maybe it is to unify the asynchronous programming model .

2) CPU Intensive processing speed is tens of thousands of miles

Pure memory operations are quite fast ,1s It can be executed tens of thousands of times , What's the problem ? This is a big problem , You can see clearly , This ReadAsync Back to a Task object , This means that tens of millions of Task object , The consequences may be GC It's spasmodic , Seriously affect the performance of the program .

3. Language team solutions

Maybe based on the two points I just talked about , Especially the second point , The language team gave ValueTask This solution , After all, it's a value type , No memory will be allocated on the managed heap , and GC It doesn't matter , Some friends will say , a verbal statement without any proof ,Talk is cheap. Show me the code .

3、 ... and :Task and ValueTask stay MemoryStream The demo on

1. Task<int> Of ReadAsync demonstration

For the sake of explanation , I'm going to pour a paragraph into MemoryStream In the middle , And then use ReadAsync One byte One byte Read out , The purpose is to make while More cycles , Generate more Task object , The code is as follows :


    class Program
    {
        static void Main(string[] args)
        {
            var content = GetContent().Result;

            Console.WriteLine(content);

            Console.ReadKey();
        }

        public static async Task<string> GetContent()
        {
            string str = "  Generally speaking : Students don't care about the position of the paper on the table ( He doesn't usually put the paper right ), Always calculate in the blank , out of order . however , I once saw a student number them in order on the draft paper . He told me , The advantage of this is : Whether it's exam or homework , At the final inspection , According to the number , He'll be able to find the previous calculation very quickly , You can save about two or three minutes . This habit , May follow him all his life , He can have countless two or three minutes in his life , And there are likely to be several critical two or three minutes .";

            using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(str)))
            {
                byte[] bytes = new byte[1024];

                ms.Seek(0, SeekOrigin.Begin);
                int cursor = 0;
                var offset = 0;
                int count = 1;

                while ((offset = await ms.ReadAsync(bytes, cursor, count)) != 0)
                {
                    cursor += offset;
                }

                return Encoding.UTF8.GetString(bytes, 0, cursor);
            }
        }
    }

There is no problem with the output , Next use windbg Take a look at how many are generated on the managed heap Task...


0:000> !dumpheap -type Task -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffaf2404650        1           24 System.Threading.Tasks.Task+<>c
00007ffaf24042b0        1           40 System.Threading.Tasks.TaskFactory
00007ffaf23e3848        1           64 System.Threading.Tasks.Task
00007ffaf23e49d0        1           72 System.Threading.Tasks.Task`1[[System.String, System.Private.CoreLib]]
00007ffaf23e9658        2          144 System.Threading.Tasks.Task`1[[System.Int32, System.Private.CoreLib]]
Total 6 objects

From the top of the heap , I went to ,Task<int> Why there are only two ?, 了 , I'm not right ??? impossible , Take a look at the source code .


public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
        int num = Read(buffer, offset, count);
        Task<int> lastReadTask = _lastReadTask;
        return (lastReadTask != null && lastReadTask.Result == num) ? lastReadTask : (_lastReadTask = Task.FromResult(num));
}

I don't know if you understand the last code above ,MemoryStream It was used _lastReadTask Played a little trick , as long as num The same return is a Task, If it is different, a new one will be generated Task object , Obviously, this is optimized for specific scenarios , For universality , I'm definitely going to bypass this technique , The way to do it is every time num It's ok if the numbers are different , take while The code is as follows :


        while ((offset = await ms.ReadAsync(bytes, cursor, count++ % 2 == 0 ? 1 : 2)) != 0)
        {
            cursor += offset;
        }

And then use windbg to glance at :


0:000> !dumpheap -type Task -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffaf7f04650        1           24 System.Threading.Tasks.Task+<>c
00007ffaf7f042b0        1           40 System.Threading.Tasks.TaskFactory
00007ffaf7ee3848        1           64 System.Threading.Tasks.Task
00007ffaf7ee49d0        1           72 System.Threading.Tasks.Task`1[[System.String, System.Private.CoreLib]]
00007ffaf7ee9658      371        26712 System.Threading.Tasks.Task`1[[System.Int32, System.Private.CoreLib]]
Total 375 objects

You can see from the last line of code that Count=371, ha-ha , It's a million level , Then here Task You can imagine how terrible it is .

2. ValueTask<int> Of ReadAsync demonstration

The harmfulness of the previous example is also clear to us , In this scenario, the solution is naturally C# New from the team ReadAsync Method , The code is as follows :


   class Program
   {
        static void Main(string[] args)
        {
            var content = GetContent().Result;

            Console.WriteLine(content);

            Console.ReadKey();
        }

        public static async Task<string> GetContent()
        {
            string str = "  Generally speaking : Students don't care about the position of the paper on the table ( He doesn't usually put the paper right ), Always calculate in the blank , out of order . however , I once saw a student number them in order on the draft paper . He told me , The advantage of this is : Whether it's exam or homework , At the final inspection , According to the number , He'll be able to find the previous calculation very quickly , You can save about two or three minutes . This habit , May follow him all his life , He can have countless two or three minutes in his life , And there are likely to be several critical two or three minutes .";

            using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(str)))
            {
                byte[] bytes = new byte[1024];

                Memory<byte> memory = new Memory<byte>(bytes);

                ms.Seek(0, SeekOrigin.Begin);
                int cursor = 0;
                var offset = 0;
                var count = 1;

                while ((offset = await ms.ReadAsync(memory.Slice(cursor, count++ % 2 == 0 ? 1 : 2))) != 0)
                {
                    cursor += offset;
                }

                return Encoding.UTF8.GetString(bytes, 0, cursor);
            }
        }
    }

Very happy , use ValueTask It also achieves the same function , And not yet GC Any trouble , Don't believe it , use windbg Under verification :


0:000> !dumpheap -type Task -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffaf23f7bf0        1           24 System.Threading.Tasks.Task+<>c
00007ffaf23f7850        1           40 System.Threading.Tasks.TaskFactory
00007ffaf23c3848        1           64 System.Threading.Tasks.Task
00007ffaf23c49d0        1           72 System.Threading.Tasks.Task`1[[System.String, System.Private.CoreLib]]
Total 4 objects

0:000> !dumpheap -type ValueTask -stat
Statistics:
              MT    Count    TotalSize Class Name
Total 0 objects

You can see , There's no trace on the managed heap , It's perfect .

Four : ValueTask Is it really perfect ?

If it's perfect , I believe the underlying framework will be changed to ValueTask, And the reality is not , Also explains ValueTask It's just the preferred solution in some scenarios , If you understand the two cases above , You should understand ValueTask Especially suitable for those CPU intensive Asynchronous task , Because it's a false asynchrony , When you await When , In fact, the results have come out , After all, they are pure memory operations , Don't deal with underlying drives , Naturally, it's quite fast .

struct There are many restrictions in multithreading mode , If it's not used properly , There will be too many potential problems and uncertainties , You can think about why lock Most locks use reference types , Not the value type , It's the same thing , So it's destined to be a high-level game , Believe in 95% None of your friends will use it in project development , Use Task Just fine , Basically cure all kinds of diseases

5、 ... and : summary

from ValueTask It can be seen from the problem to be solved that C# The language team has been obsessed with performance optimization in high concurrency scenarios , And in the existing class library 99% The method is still to use Task, So ordinary players still use it honestly Task Well , In reality, I haven't encountered any performance bottleneck on this , High energy or left to high-level players !

More high quality dry goods : See my GitHub: dotnetfly

版权声明
本文为[One line code agriculture]所创,转载请带上原文链接,感谢