Pipelines - .NET中的新IO API指引(三) 边看边记
Pipelines - .NET中的新IO API指引 作者 marcgravell 原文
此系列前两篇网上已有的译文
Pipelines - .NET中的新IO API指引(一)
Pipelines - .NET中的新IO API指引(二)
关于System.IO.Pipelines的一篇说明
System.IO.Pipelines: .NET高性能IO
本篇不是翻译,边看边译边记而已。
System.IO.Pipelines 是对IO的统一抽象,文件、com口、网络等等,重点在于让调用者注意力集中在读、写缓冲区上,典型的就是 IDuplexPipe中的Input Output。
可以理解为将IO类抽象为读、写两个缓冲区。
目前官方实现还处于preview状态,作者使用Socket和NetworkStream 实现了一个 Pipelines.Sockets.Unofficial
作者在前两篇中提到使用System.IO.Pipelines 改造StackExchange.Redis,在本篇中作者采用了改造现有的SimplSockets库来说明System.IO.Pipelines的使用。
文章中的代码(SimplPipelines,KestrelServer )
## SimplSockets说明
+ 可以单纯的发送(Send),也可以完成请求/响应处理(SendRecieve)
+ 同步Api
+ 提供简单的帧协议封装消息数据
+ 使用byte[]
+ 服务端可以向所有客户端广播消息
+ 有心跳检测等等
## 作者的改造说明
### 对缓冲区数据进行处理的一些方案及选型
1. 使用byte[]拷贝出来,作为独立的数据副本使用,简单易用但成本高(分配和复制)
2. 使用 ReadOnlySequence<byte> ,零拷贝,快速但有限制。一旦在管道上执行Advance操作,数据将被回收。在有严格控制的服务端处理场景(数据不会逃离请求上下文)下可以使用,言下之意使用要求比较高。
3. 作为2的扩展方案,将数据载荷的解析处理代码移至类库中(处理ReadOnlySequence<byte>),只需将解构完成的数据发布出来,也许需要一些自定义的structs 映射(map)一下。这里说的应该是直接将内存映射为Struct?
4. 通过返回Memory<byte> 获取一份数据拷贝,也许需要从ArrayPool<byte>.Share 池中返回一个大数组;但是这样对调用者要求较高,需要返回池。并且从Memory<T> 获取一个T[]属于高级和不安全的操作。不安全,有风险。( not all Memory<T> is based on T[])
5. 一个妥协方案,返回一个提供Memory<T>(Span<T>)的东西,并且使用一些明确的显而易见的Api给用户,这样用户就知道应该如何处理返回结果。比如IDisposable/using这种,在Dispose()被调用时将资源归还给池。
作者认为,设计一个通用的消息传递Api时,方案5更为合理,调用方可以保存一段时间的数据并且不会干扰到管道的正常工作,也可以很好的利用ArrayPool。如果调用者没有使用using也不会有什么大麻烦,只是效率会降低一些,就像使用方案1一样。
但是方案的选择需要充分考虑你的实际场景,比如在StackExchange.Redis 客户端中使用的是方案3;在不允许数据离开请求上下文时使用方案2.。
一旦选定方案,以后基本不可能再更改。
public interface IMemoryOwner<T> : IDisposable
{
Memory<T> Memory { get; }
}
private sealed class ArrayPoolOwner<T>:IMemoryOwner<T>{
private readonly int _length;
private T[] _oversized;
internal ArrayPoolOwner(T[] oversized,int length){
_length=length;
_oversized=oversized;
}
public Memory<T> Memory=>new Memory<T>(GetArray(),,_length);
private T[] GetArray()=>Interlocked.CompareExchange(ref _oversized,null,null)
?? throw new ObjectDisposedException(ToString());
public void Dispose(){
var arr=Interlocked.Exchange(ref _oversized,null);
if(arr!=null) ArrayPool<T>.Shared.Return(arr);
}
}
+ 从ArrayPool借出的数组比你需要的要大,你给定的大小在ArrayPool看来属于下限(不可小于你给定的大小),见:ArrayPool<T>.Shared.Rent(int minimumLength);
+ 归还时数组默认不清空,因此你借出的数组内可能会有垃圾数据;如果需要清空,在归还时使用 ArrayPool<T>.Shared.Return(arr,true) ;
增加 IMemoryOwner<T> RentOwned(int length),T[] Rent(int minimumLength) 及借出时清空数组,归还时清空数组的选项。
void DoSomething(IMemoryOwner<byte> data){
using(data){
// ... other things here ...
DoTheThing(data.Memory);
}
// ... more things here ...
}
通过ArrayPool的借、还机制避免频繁分配。
+ 不要把data.Memory 单独取出乱用,using完了就不要再操作它了(这种错误比较基础)
+ 有人会用MemoryMarshal搞出数组使用,作者认为可以实现一个 MemoryManager<T>(ArrayPoolOwner<T> : MemoryManager<T>, since MemoryManager<T> : IMemoryOwner<T>)让.Span如同.Memory一样失败。
---- 作者也挺纠结(周道)的 :)。
public static IMemoryOwner<T> Lease<T>(this ReadOnlySequence<T> source)
{
if (source.IsEmpty) return Empty<T>();
int len = checked((int)source.Length);
var arr = ArrayPool<T>.Shared.Rent(len);//借出
source.CopyTo(arr);
return new ArrayPoolOwner<T>(arr, len);//dispose时归还
}
### 基本API
服务端和客户端虽然不同但代码有许多重叠的地方,比如都需要某种线程安全机制的写入,需要某种读循环来处理接收的数据,因此可以共用一个基类。
基类中使用IDuplexPipe(包括input,output两个管道)作为管道。
public abstract class SimplPipeline : IDisposable
{
private IDuplexPipe _pipe;
protected SimplPipeline(IDuplexPipe pipe)
=> _pipe = pipe;
public void Dispose() => Close();
public void Close() {/* burn the pipe*/}
}
+ 有许多移动的部分
+ 与“pipelines”有些重复
+ 不一定时同步的
+ 调用方可以单纯的传入一段内存数据(ReadOnlyMember<byte>),或者是一个(IMemoryOwner<byte>)由Api写入后进行清理。
+ 先假设读、写分开(暂不考虑响应)
protected async ValueTask WriteAsync(IMemoryOwner<byte> payload, int messageId)//调用方不再使用payload,需要我们清理
{
using (payload)
{
await WriteAsync(payload.Memory, messageId);
}
}
protected ValueTask WriteAsync(ReadOnlyMemory<byte> payload, int messageId);//调用方自己清理
messageId标识一条消息,写入消息头部, 用于之后处理响应回复信息。
返回值使用ValueTask因为写入管道通常是同步的,只有管道执行Flush时才可能是异步的(大多数情况下也是同步的,除非在管道被备份时)。
### 写入与错误
首先需要保证单次写操作,lock在此不合适,因为它不能与异步操作很好的协同。考虑flush有可能是异步的,导致后续(continuation )部分可能会在另外的线程上。这里使用与异步兼容的SemaphoreSlim。
下面是一条指南:**一般来说, 应用程序代码应针对可读性进行优化;库代码应针对性能进行优化。**
以下为机翻原文
> 您可能同意也可能不同意这一点, 但这是我编写代码的一般指南。我的意思是,类库代码往往有一个单一的重点目的, 往往由一个人的经验可能是 "深刻的, 但不一定是 广泛的" 维护;你的大脑专注于那个领域, 用奇怪的长度来优化代码是可以的。相反,应用程序代码往往涉及更多不同概念的管道-"宽但不一定深" (深度隐藏在各种库 中)。应用程序代码通常具有更复杂和不可预知的交互, 因此重点应放在可维护和 "明显正确" 上。
基本上, 我在这里的观点是, 我倾向于把很多注意力集中在通常不会放入应用程序代码中的优化上, 因为我从经验和广泛的基准测试中知道它们真的很重要。所以。。。我要做一些看起来很奇怪的事情, 我希望你和我一起踏上这段旅程。
private readonly SemaphoreSlim _singleWriter= new SemaphoreSlim();
protected async ValueTask WriteAsync(ReadOnlyMemory<byte> payload, int messageId)
{
await _singleWriter.WaitAsync();
try
{
WriteFrameHeader(writer, payload.Length, messageId);
await writer.WriteAsync(payload);
}
finally
{
_singleWriter.Release();
}
}
通过两个问题进行重构
- 单次写入是否没有竞争?(无人争用)
- Flush是否为同步
protected ValueTask WriteAsync(ReadOnlyMemory<byte> payload, int messageId)
{
// try to get the conch; if not, switch to async
//writer已经被占用,异步
if (!_singleWriter.Wait())
return WriteAsyncSlowPath(payload, messageId);
bool release = true;
try
{
WriteFrameHeader(writer, payload.Length, messageId);
var write = writer.WriteAsync(payload);
if (write.IsCompletedSuccessfully) return default;
release = false;
return AwaitFlushAndRelease(write);
}
finally
{
if (release) _singleWriter.Release();
}
}
async ValueTask AwaitFlushAndRelease(ValueTask<FlushResult> flush)
{
try { await flush; }
finally { _singleWriter.Release(); }
}
三个地方
1. _singleWriter.Wait(0) 意味着writer处于空闲状态,没有其他人在调用
2. write.IsCompletedSuccessfully 意味着writer同步flush
3. 辅助方法 AwaitFlushAndRelease 处理异步flush情况
-------------------------------------------------------------------------------------
### 协议头处理
协议头由两个int组成,小端,第一个是长度,第二个是messageId,共8字节。
void WriteFrameHeader(PipeWriter writer, int length, int messageId)
{
var span = writer.GetSpan();
BinaryPrimitives.WriteInt32LittleEndian(
span, length);
BinaryPrimitives.WriteInt32LittleEndian(
span.Slice(), messageId);
writer.Advance();
}
public class SimplPipelineClient : SimplPipeline
{
public async Task<IMemoryOwner<byte>> SendReceiveAsync(ReadOnlyMemory<byte> message)
{
var tcs = new TaskCompletionSource<IMemoryOwner<byte>>();
int messageId;
lock (_awaitingResponses)
{
messageId = ++_nextMessageId;
if (messageId == ) messageId = ;
_awaitingResponses.Add(messageId, tcs);
}
await WriteAsync(message, messageId);
return await tcs.Task;
}
public async Task<IMemoryOwner<byte>> SendReceiveAsync(IMemoryOwner<byte> message)
{
using (message)
{
return await SendReceiveAsync(message.Memory);
}
}
}
- _awaitingResponses 是个字典,保存已经发送的消息,用于将来处理对某条(messageId)消息的回复。
protected async Task StartReceiveLoopAsync(CancellationToken cancellationToken = default)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var readResult = await reader.ReadAsync(cancellationToken);
if (readResult.IsCanceled) break;
var buffer = readResult.Buffer;
var makingProgress = false;
while (TryParseFrame(ref buffer, out var payload, out var messageId))
{
makingProgress = true;
await OnReceiveAsync(payload, messageId);
}
reader.AdvanceTo(buffer.Start, buffer.End);
if (!makingProgress && readResult.IsCompleted) break;
}
try { reader.Complete(); } catch { }
}
catch (Exception ex)
{
try { reader.Complete(ex); } catch { }
}
}
protected abstract ValueTask OnReceiveAsync(ReadOnlySequence<byte> payload, int messageId);
- TryParseFrame 读出缓冲区数据,根据帧格式解析出实际数据、id等
- OnRecieveAsync 处理数据,比如对于回复/响应的处理等
- reader中的数据读出后尽快Advance一下,通知管道你的读取进度;
private bool TryParseFrame(
ref ReadOnlySequence<byte> input,
out ReadOnlySequence<byte> payload, out int messageId)
{
if (input.Length < )
{ // not enough data for the header
payload = default;
messageId = default;
return false;
}
int length;
if (input.First.Length >= )
{ // already 8 bytes in the first segment
length = ParseFrameHeader(
input.First.Span, out messageId);
}
else
{ // copy 8 bytes into a local span
Span<byte> local = stackalloc byte[];
input.Slice(, ).CopyTo(local);
length = ParseFrameHeader(
local, out messageId);
}
// do we have the "length" bytes?
if (input.Length < length + )
{
payload = default;
return false;
}
// success!
payload = input.Slice(, length);
input = input.Slice(payload.End);
return true;
}
代码很简单,主要演示一些用法;
辅助方法
static int ParseFrameHeader(
ReadOnlySpan<byte> input, out int messageId)
{
var length = BinaryPrimitives
.ReadInt32LittleEndian(input);
messageId = BinaryPrimitives
.ReadInt32LittleEndian(input.Slice());
return length;
}
OnReceiveAsync
protected override ValueTask OnReceiveAsync(
ReadOnlySequence<byte> payload, int messageId)
{
if (messageId != )
{ // request/response
TaskCompletionSource<IMemoryOwner<byte>> tcs;
lock (_awaitingResponses)
{
if (_awaitingResponses.TryGetValue(messageId, out tcs))
{
_awaitingResponses.Remove(messageId);
}
}
tcs?.TrySetResult(payload.Lease());
}
else
{ // unsolicited
MessageReceived?.Invoke(payload.Lease());
}
return default;
}
Pipelines - .NET中的新IO API指引(三) 边看边记的更多相关文章
- Pipelines - .NET中的新IO API指引(一)
https://zhuanlan.zhihu.com/p/39223648 原文:Pipelines - a guided tour of the new IO API in .NET, part 1 ...
- JDK8中的新时间API:Duration Period和ChronoUnit介绍
目录 简介 Duration Period ChronoUnit 简介 在JDK8中,引入了三个非常有用的时间相关的API:Duration,Period和ChronoUnit. 他们都是用来对时间进 ...
- Java日期时间API系列7-----Jdk8中java.time包中的新的日期时间API类的特点
1.不变性 新的日期/时间API中,所有的类都是不可变的,这对多线程环境有好处. 比如:LocalDateTime 2.关注点分离 新的API将人可读的日期时间和机器时间(unix timestamp ...
- Java日期时间API系列13-----Jdk8中java.time包中的新的日期时间API类,时间类转换,Date转LocalDateTime,LocalDateTime转Date等
从前面的系列博客中可以看出Jdk8中java.time包中的新的日期时间API类设计的很好,但Date由于使用仍非常广泛,这就涉及到Date转LocalDateTime,LocalDateTime转D ...
- Java日期时间API系列19-----Jdk8中java.time包中的新的日期时间API类,ZonedDateTime与ZoneId和LocalDateTime的关系,ZonedDateTime格式化和时区转换等。
通过Java日期时间API系列6-----Jdk8中java.time包中的新的日期时间API类中时间范围示意图:可以很清晰的看出ZonedDateTime相当于LocalDateTime+ZoneI ...
- Java日期时间API系列8-----Jdk8中java.time包中的新的日期时间API类的LocalDate源码分析
目录 0.前言 1.TemporalAccessor源码 2.Temporal源码 3.TemporalAdjuster源码 4.ChronoLocalDate源码 5.LocalDate源码 6.总 ...
- Java日期时间API系列11-----Jdk8中java.time包中的新的日期时间API类,使用java8日期时间API重写农历LunarDate
通过Java日期时间API系列7-----Jdk8中java.time包中的新的日期时间API类的优点,java8具有很多优点,现在网上查到的农历转换工具类都是基于jdk7及以前的类写的,下面使用ja ...
- Java日期时间API系列12-----Jdk8中java.time包中的新的日期时间API类,日期格式化,常用日期格式大全
通过Java日期时间API系列10-----Jdk8中java.time包中的新的日期时间API类的DateTimeFormatter, 可以看出java8的DateTimeFormatter完美解决 ...
- iOS7开发中的新特性
iOS7到现在已经发布了有一段时间了.相信你现在已经了解了它那些开创性的视觉设计,已经了解了它的新的API,比如说SpirteKit,UIKit Dynamics以及TextKit,作为开发者 ...
随机推荐
- 第八章 高级搜索树 (a1)伸展树:逐层伸展
- C/C++ 宏
0. #define MALLOC(n, type) \ ( (type *) malloc((n)* sizeof(type))) 1. 宏可以像函数一样被定义,例如: #define min ...
- 配置python学习环境遇到的问题:[Decode error - output not utf-8]
因为前阵子学习monkeyrunner的时候,碰到了很多关于.py的脚本,其实我是一知半解的,也没打算去学习一下.将就着看看吧,后来无意中看到自动化测试工程师都要求会脚本语言的时候,刺激了我,想了想, ...
- obstacle
obstacle - 必应词典 美[ˈɑbstək(ə)l]英[ˈɒbstək(ə)l] n.障碍:障碍物:阻碍:绊脚石 网络妨碍:干扰:妨害
- Django的cookie学习
为什么要有cookie,因为http是无状态的,每次请求都是独立的,但是我们还需要保持状态,所以就有了cookie cookie就是保存在客户端浏览器上的键值对,别人可以利用他来做登陆 rep = r ...
- java的PDF纵横向打印
PDF默认是纵向打印的,通过rotate()来让其改变为横向打印,一般在打印A4 12*21纸以及发票的时候会用横向打印.横向打印时页面会出现行转列以及列转行的情况,因此在设置页面大小的时候一定要宽度 ...
- Luogu 3953[NOIP2017] 逛公园 堆优化dijkstra + 记忆化搜索
题解 首先肯定是要求出单源最短路的,我用了堆优化dijikstra ,复杂度 mlogm,值得拥有!(只不过我在定义优先队列时把greater 打成了 less调了好久 然后我们就求出了$i$到源点的 ...
- POJ3687.Labeling Balls 拓扑排序
Labeling Balls Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 13201 Accepted: 3811 Descr ...
- json.dumps错误:'utf8' codec can't decode byte解决方案-乾颐堂
一次在使用json.dumps()过程中,出现错误提示: ERROR:"UnicodeDecodeError: 'utf8' codec can't decode byte 0xe1 in ...
- UDP接收数据
http://blog.csdn.net/xingzheouc/article/details/49946191 http://blog.csdn.net/robertkun/article/deta ...