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