当前位置: 首页 > news >正文

美容加盟网站建设wordpress问候插件

美容加盟网站建设,wordpress问候插件,佛山网站制作在线,临潼网站建设原文#xff1a;Pipelines - a guided tour of the new IO API in .NET, part 2作者#xff1a;marcgravell在上一章#xff0c;我们讨论了以往的StreamAPI中存在的一些问题#xff0c;并且介绍了Pipe,PipeWriter,PipeReader 等API#xff0c;研究如何写出一个Pipe 并且从… 原文Pipelines - a guided tour of the new IO API in .NET, part 2作者marcgravell在上一章我们讨论了以往的StreamAPI中存在的一些问题并且介绍了Pipe,PipeWriter,PipeReader 等API研究如何写出一个Pipe 并且从中消费数据我们也讨论了FlushAsync() 和ReadAsync() 是如何协同保证两端的工作从而解决“空”和“满”的场景——在没有数据时挂起reader并在数据到来时恢复它在写入快过读取(即pipe满载)时挂起writer并在reader追上后恢复它并且我们也在线程模型的层面上探讨了什么是“挂起”。在这章我们将会研究pipelines的内存模型数据实际上存在于哪里我们也会开始着手研究如何在现实场景中使用pipeline以满足真实需求。内存模型我的数据在哪里在上一章我们讲了pipe如何管理所有的缓冲区允许writer通过 GetMemory()和GetSpan()请求缓冲区随后通过ReadAsync()中的 .Buffer 将提交后的数据暴露给reader——reader取得的数据是一个 ReadOnlySequencebyte即全部数据其中的一些片段。那么其中究竟发生了什么每一个Pipe实例都有一个引用指向MemoryPoolbyte——一个System.Memory中的新东西顾名思义它创建了一个内存池。在创建Pipe的时候你可以在选项中指定一个专门的 MemoryPoolbyte但是在默认情况下(我猜也是大多数情况下)——应该是使用一个应用级别共享的 (MemoryPoolbyte.Shared) 内存池。MemoryPoolbyte 的概念是非常开放的。其默认的实现是简单地使用ArrayPoolbyte.Shared(应用级别的数组池)在需要的时候租借数组并在使用完后归还。这个 ArrayPoolT 使用了 WeakReference来实现所以池化的数组在内存有压力时是可以回收的但是当你请求GetMemory(someSize) 或者 GetSpan(someSize)时它并不是简单地向内存池请求“someSize”相反它在内部追踪了一个“片段(segment)”一个新“片段”将是默认情况下可以通过配置改变someSize和2048字节中的最大值这样在请求一个大小可观的内存时就意味着我们的系统不会充满着许多小数组而后者会对GC造成显著碰撞。当你在writer中 Advance(bytesWritten)它移动一个表达当前已使用多少片段的内部计数器更新reader的“备读(available to be read)”链的末端如果我们刚刚对一个空片段的第一个字节进行了写入这意味着将会向链中增加一个新片段否则它意味着当前链的结尾标志被增加后移这就是我们从 ReadAsync()中获取到的“备读”链而当我们在reader中 AdvanceTo ——在整个片段都被消费后pipe会将这些片段送回内存池。在那里它们可以被多次复用。并且作为上述两点导致的直接结果我们可以看到在大多数情况下(即使在writer中多次调用Advance )我们最终会在reader中发现一个单独的片段而如果是在片段边界处或reader落后于writer数据开始累积的情况下会有多个片段。只有使用默认池才能我们不用在每次调用GetMemory() / GetSpan()时都要分配内存我们不需要每次GetMemory() / GetSpan()都产生一个单独的数组——通常我们只是获得同样的“片段”中的某个不同的范围只使用少量的大缓冲数组它们不需要大量的类库代码就可以自动回收当不再需要时它们可以被GC回收这也解释了为什么可以在GetMemory() / GetSpan()中请求少量空间再在之后检查其大小我们可以访问当前段的剩下未使用的部分。这意味着一个大小为2048的片段在之前的写入中用掉了200字节——即使我们只请求5字节我们也可以看到我们还剩下1848字节可供使用或者更多——记住从ArrayPool.Shared 中获取到的数组也是一个“至少这么大”的操作。零复制缓冲区在此还有需要注意的地方是我们获取数据缓冲的时候没有进行任何数据的复制。writer申请一个缓冲区然后第一次写入数据到需要的位置。这就成了writer和reader之间的缓冲区无需复制数据。而如果reader当前无法处理完所有的数据它能够通过显示声明其“未被消费”的方式将数据放回pipe。这样无需为reader维护一个单独的数据积压处backlog而种情况这在使用Stream的协议处理代码中是非常常见的。正是这种功能间的组合使得pipeline代码在内存层面显得非常友好。你可以用Stream做到所有的这些但是却需要大量令人痛苦的易出错的代码去实现甚至需要更多如果你想做好的话——并且你几乎必须去为每个场景单独地实现它。Pipelines让良好的内存处理变为默认的简单的途径——落入成功之中译注即如自由落体一般实现成功的代码更多奇特的内存池你并不受限于使用我们之前讨论的内存池你可以实现你自己的自定义内存池默认内存池的优点在于它很简单。尤其是在我们是否100%完美地返回每个片段并不重要的情况下——如果我们以某种方式丢弃某个pipe最坏的情况会是GC将在某个时刻回收掉被丢弃的片段。它们不会回到池中但那没关系。但是你可以做很多有趣的东西。想象一下比如一个 MemoryPoolbyte承载巨量的内存——通过一些非常大的数组得到的托管内存或是通过 Marshal.AllocHGlobal 获得的非托管内存注意 Memory 和 Span 并不受限于数组——它们需要的不过是某种连续内存按需使用这些巨大的内存块。这有很大的潜在场景但是它会使片段的可靠回收变得更加重要。大多数系统不应该这么做但是提供这样的灵活性是好的。在真实系统中有用的pipes我们在第一部分中用的例子是一个读写均在同一代码的单独Pipe。很明显这不是个真实场景除非我们是在试图模拟一个echo服务器所以我们在更真实的场景中可以做什么呢首先我们需要把我们的pipelines连接到什么东西上。我们通常并不想单独地使用pipe相反我们希望可以有一个结合一个普遍的系统或API使用的pipe。所以来让我们开始看看接下来会是什么样子吧。在这里我们需要注意发布于.NET Core 2.1的pipelines不包括任何终端实现。这意味着 Pipe 虽然存在但是在框架内没有提供任何的与现有系统的实际连接——就像提供了抽象的 Stream 基类却没有 FileStream,NetworkStream等。是的这听起来让人感到失望但是这只是因为时间所限不要慌现在在进行一些关于它们应该以哪种优先级实现的“活跃的”讨论。并且现在有一些社区贡献来补足那些最为明显的缺陷。一旦我们处于那些场景我们可能会问“将pipelines连接到另一个数据后端需要什么”也许将一个pipe连接到一个 Stream会是一个不错的开头。我知道你在想“但是Marc你在上一章你不遗余力地再说 Stream 有多么糟糕”。我没有改变我的看法它不一定是完美的——对于那些特定场景的Stream实现比如NetworkStream或FileStream我们可以有一个专门的基于pipelines的终端直接与那些服务以最小的中转代价进行通讯但是这是一个有用的起步它使我们可以立即访问到巨量的API——任何可以通过Stream暴露数据或任何通过封装的streams作为中间层的API加密、压缩等它将所有老旧的StreamAPI隐藏在一个明确清晰的表层下它带来了几乎所有我们之前提到过的优点所以让我们开始吧我们首先要思考的是这里的方向是什么就像刚才提到的一样Stream是模糊不清的——可能只读只写或可读可写。来假设我们想解决的是最通常的问题一个可读可写表现为双工行为的stream——这可以让我们访问如sockets(通过NetworkStream)之类的东西。这意味着我们实际上将会需要两个pipe——一个用来输入一个用来输出。Pipelines通过明确地声明IDuplexPipe接口来帮助我们指明道路。这是一个非常简单的接口数据传输给IDuplexPipe就像传输给两个pipe的端点一样——一个标记为in一个标记为outinterface IDuplexPipe{PipeReader Input { get; }PipeWriter Output { get; }}我们接下来想要做的是创建一个类来实现 IDuplexPipe不过其内部使用了两个Pipe实例一个Pipe会是输出缓冲区从消费者的角度来看它将会在调用者写入Output时被填充——并且我们将会用一个循环来消费这个Pipe并且将数据推入底层Stream(被用来写入网络或者其它任何stream可以写入的)一个Pipe将会是输入缓冲区从消费者的角度来看我们将有一个循环来从底层Stream读取数据并将其推入Pipe它将会在调用者从Input中读取时排出这个方法可以立即解决普遍影响着那些使用Stream的人一大堆的问题我们现在有了input/output缓冲区用于从读/写调用中解耦stream访问而不用添加BufferedStream或是其它类似的防止数据碎片的功能对于写入代码来说,并且这将会使我们在处理数据时很方便去接收更多数据特别是对于读取代码来说这样我们不用在请求更多数据时保持暂停如果调用代码的写入快过stream的Write可以处理的程度背压特性将会展现出来对调用代码进行节流这样我们不会被充满未发送数据的巨大缓冲区所终结如果stream的Read超过了消费这些数据的调用代码背压特性也会在这里出场对我们的stream读取循环进行节流这样我们不会被充满未处理数据的巨大缓冲区所终结读取和写入代码都会受益于我们之前所讨论的内存池的所有优点调用代码从来不用担心数据的后备存储未完成帧等——pipe去解决它那么它看起来会是什么样基本上我们需要做的就是这样class StreamDuplexPipe : IDuplexPipe{Stream _stream;Pipe _readPipe, _writePipe;public PipeReader Input _readPipe.Reader;public PipeWriter Output _writePipe.Writer;// ... more here}注意我们有两个不同的pipe调用者获取每个pipe的一个端点——然后我们的代码将会操作每个pipe的另一个端点。对pipe进行泵送(Pumping)那么我们与stream交互的代码是什么样的呢像之前说过的那样我们需要两个方法。首先——很简单——一个循环从_stream中读取数据并且将其推入_readPipe然后被调用代码所消费这个方法的核心类似这样while (true){// note well usually get *much* more than we ask forvar buffer _readPipe.Writer.GetMemory(1);int bytes await _stream.ReadAsync(buffer);_readPipe.Writer.Advance(bytes);if (bytes 0) break; // source EOFvar flush await _readPipe.Writer.FlushAsync();if (flush.IsCompleted || flush.IsCanceled) break;}这个循环向pipie请求一个缓冲区然后用 netcoreapp2.1 中Stream.ReadAsync 的新重载接收一个 Memorybyte 来填充缓冲区——我们一会儿讨论如果你现在没有一个能接收 Memorybyte的API该怎么办。当读取完成后它使用Advance向pipe提交这个数量的字节然后它在pipe上调用 FlushAsync() 来如果需要的话唤醒reader或者在背压减轻时暂停写循环。注意我们还需要检查Pipe的 FlushAsync()的结果——它可以告诉我们pipe的消费者已经告知其已经读取完了所有想要的数据Iscompleted或者pipe本身被关闭IsCanceled。注意在这两种情况下我们都希望确保在此循环退出时告诉管道这样我们就不会最终在没有数据到来时永远在调用端等待下去有时会发生意外有时在调用 _stream.ReadAsync 或其它方法可能会有异常抛出所以最好是利用try/finallyException error null;try{// our loop from the previous sample}catch(Exception ex) { error ex; }finally { _readPipe.Writer.Complete(error); }如果你愿意的话你可以使用两个 Complete ——一个在try末尾成功时一个在catch中失败时。我们需要的第二个方法会比较复杂。我们需要一个循环来从_writePipe中消费数据然后将其推入_stream。核心代码会像这样while (true){var read await _writePipe.Reader.ReadAsync();var buffer read.Buffer;if (buffer.IsCanceled) break;if (buffer.IsEmpty read.IsCompleted) break;// write everything we got to the streamforeach (var segment in buffer){await _stream.WriteAsync(segment);}_writePipe.AdvanceTo(buffer.End);await _stream.FlushAsync();    }这会等待一些数据可能在多个缓冲区里然后进行一些退出判断检查像之前一样我们可以在IsCanceled时放弃但是下一个检查会比较微妙我们不希望只因为producer表示它们已经写入了所有想要的数据Iscompleted就停止写入不然我们也许会丢失它们末尾几段数据——我们需要继续直到我们已经写入了它们所有的数据直到buffer.IsEmpty。这是个简化后的例子因为我们一直写入所有数据——我们之后会看到更复杂的例子。一旦我们有了数据我们按顺序将每个非连续缓冲区写入stream中——因为Stream一次只能写入一个缓冲区同样我使用的是netcoreapp2.1中的重载接受ReadOnlyMemorybyte参数但是我们不限于此。一旦它写完了缓冲区它告诉pipe我们已经消费完了所有数据然后刷新(flush)底层的Stream。在“真实”代码中我们也许希望更积极地优化从而减少刷新底层stream直到我们知道再也不会有可读取的数据那么也许在_writePipe.Reader.ReadAsync()之外我们可以使用_writePipe.Reader.TryRead(...)。这个方法的工作方式类似于ReadAsync()但是会保证同步返回——这可以用来测试“在我忙的时候writer是否附加了什么”。但是上面的内容已经讲述了这一点。另外像之前一样我们也许需要添加一个 try/finally这样在我们退出时总是会调用_writePipe.Reader.Complete()。我们可以使用 PipeScheduler 来启动这两个泵(pumps)这会确保它们在预期环境中运行然后我们的循环开始泵送数据。我们要添加一些格外的内容我们可能需要一种机制来 Close()/Dispose() 底层stream等——但是像你所看到的将 IDuplexPipe 连接到没有pipeline设计的源并不需要是一项艰巨的任务。这是我之前做的...我已经将上面的内容简化了一些说真的不是太多以便让它适合讨论但是你可能仍然不应该从这里复制粘贴代码来尝试让它工作。我并没有声称它们是适用于所有情况的完美解决方案但是作为StackExchange.Redis 2.0版工作的一部分我们实现了一系列pipelines的绑定放在nuget上——毫无创意地命名为 Pipelines.Sockets.Unofficialnuget,github(https://github.com/mgravell/Pipelines.Sockets.Unofficial)它包括了将双工的Stream转换为 IDuplexPipe 就像上面说的将只读Stream转换为PipeReader将只写Stream转换为PipeWriter将 IDuplexPipe 转换为双工的Stream将PipeReader转换为只读Stream将PipeWriter转换为只写Stream将Socket直接转换成IDuplexPipe不经过NetworkStream前六个在 StreamConnection的静态方法中最后一个在SocketConnection里。StackExchange.Redis 牵涉着大量Socket工作所以我们对如何将pipeline连接到socket上非常感兴趣对于没有TLS的redis连接我们可以直接将我们的Socket连接到pipelineSocket ⇔ SocketConnection对于需要TLS的redis连接比如云redis提供商我们可以这样连接Socket ⇔ NetworkStream ⇔ SslStream ⇔ StreamConnection所有这两种配置都是一个Socket在其中一端一个IDuplexPipe在另一端它开始展示我们如何将pipeline作为更复杂系统的一部分。也许更重要的是它为我们在未来实施改变提供了空间。将来有可能的例子Tim Seaward一直在折腾Leto它提供了不需要 SslStream 直接用IDuplexPipe实现TLS的能力并且不需要stream逆变器在 Tim SeawardDavid Fowler 和Ben Adams之间有一系列直接实现pipelines而不用托管sockets的实验性/正在进行的网络层工作包括libuvRIORegisterd IO和最近的magma——它将整个TCP栈推入用户代码从而减少系统调用。看这个空间如何发展将会非常有趣但是我当前的API不会使用 Span 或者 Memory当在写将数据从pipe中泵送到其它系统比如一个Socket时很有可能你会遇到不接收 Span或者 Memory的API。不要慌这没有大碍你依然可以有很多种变通方案使其变得更……传统。在你有一个 Memory 或者 ReadOnlyMemory时第一个技巧是MemoryMarshal.TryGetArray(...)。它接收一个memory并且尝试获取一个ArraySegment 它用一个T[]vector和一个int偏移/计数对描述相同的数据。显然这只有在这块内存是基于一个vector时才能用而情况并非总是如此所以这可能会在异种的内存池上失败。我们第二个解决办法时MemoryMarshal.GetReference(...)它接受一个span然后返回一个原始数据起点的引用实际上是一个“托管指针”又叫做 ref T。一旦我们有了一个 ref T我们可以用unsafe语法来获得一个这个数据的非托管指针在这种情况下会有用Spanbyte span ...fixed(byte* ptr MemoryMarshal.GetReference(span)){// ...}即使span的长度是零你依然可以这么做其会返回一个第0项将会存在的位置而且甚至在使用defaultspan即根本没有实际后备内存的时候也可以这么使用。后面这个有一点需要注意因为ref T通常不被认为会是null但是在这里它却是了。实际上只要你不去尝试对这种空引用进行解引用不会有什么问题。如果你使用fixed将其转换为一个非托管指针你会得到一个空零指针这相对来说更合理并且在一些P/Invoke场景中会有用MemoryMarshal 本质上是unsafe 代码的同义词即使你调用的那段代码并没有使用unsafe 关键字。使用它是完全有效的但是如果不恰当地使用它它可能会坑到你——所以小心就是了。Pipe的应用端代码是什么样的OK我们有了IDuplexPipe并且我们也看到了如何将两个pipe的“业务端”连接到你选择的后端数据服务。现在我们在应用代码中如何使用它按照我们上一章的例子我们将从 IDuplexPipe.Output 中把PipeWriter传递给我们的出站代码从 IDuplexPipe.Input 中把 PipeReader 传递给我们的入站代码。出站代码相当简单并且通常是需要直接从基于Stream的代码移植成基于PipeWriter的代码。关键的区别还是那样即你不再手动控制缓冲区。下面是一个典型的实现ValueTaskbool Write(SomeMessageType message, PipeWriter writer){// (this may be multiple GetSpan/Advance calls, or a loop,// depending on what makes sense for the message/protocol)var span writer.GetSpan(...);// TODO: ... actually write the messageint bytesWritten ... // from writingwriter.Advance(bytesWritten);return FlushAsync(writer);}private static async ValueTaskbool FlushAsync(PipeWriter writer){// apply back-pressure etcvar flush await writer.FlushAsync();// tell the calling code whether any more messages// should be writtenreturn !(flush.IsCanceled || flush.IsCompleted);}Write 的第一部分是我们的业务代码我们需要把数据从writer写入到缓冲区通常这会多次调用 GetSpan(...) 和 Advance()。当我们写完了数据我们可以flush它从而保证启动泵送并且应用背压控制。对于那些非常大的消息体我们也可以在中间点flush但是对于大多数场景一个消息flush一次足够了。如果你好奇为什么我将FlushAsync 分割到不同的代码中那是因为我想await FlushAsync的结果来检查退出条件所以它需要在一个async 方法里在这里最有效率的访问内存方式是通过 Spanbyte APISpanbyte 是一个 ref struct 类型因此我们不能在异步方法中将 Spanbyte 作为局部变量使用。一个实用的办法是简单地分割代码这样一个方法做 Spanbyte 工作一个方法做async方面的工作。发散一下异步代码、同步热路径和异步机制开销async / await 中引入的机制译注指ValueTaskmachinery应该是和async状态机关联的词但是我并不知道怎么翻译合适只好翻译成机制了非常棒但是它仍然会是一个会产生惊人栈开销的工作——你可以从 sharplab.io 中看到——看看OurCode.FlushAsync 中生成的机制——和整个 struct FlushAsyncd__0。现在这些代码并不是很糟糕——它非常努力地尝试在同步路径上避免内存分配——但是没有必要。这里有两种方法可以显著地改善它一个是压根不去 await 通常如果 await 是在方法中地最后一行并且我们不需要去处理结果不去 await ——只要去除async然后return这个task——完成或者未完成。在这里我们没办法这样做因为我们需要去检查返回的状态但是我们可以通过检查这个task是否已经完成来对成功的结果进行优化通过 .IsCompletedSuccessfully ——如果它已经结束但是有错误我们仍然需要使用await来让异常可以正确表现出来。如果它是成功完成的我们可以请求到.Result。所以我们也可以将FlushAsync 写成这样private static ValueTaskbool Flush(PipeWriter writer){bool GetResult(FlushResult flush)// tell the calling code whether any more messages// should be written !(flush.IsCanceled || flush.IsCompleted);async ValueTaskbool Awaited(ValueTaskFlushResult incomplete) GetResult(await incomplete);// apply back-pressure etcvar flushTask writer.FlushAsync();return flushTask.IsCompletedSuccessfully? new ValueTaskbool(GetResult(flushTask.Result)): Awaited(flushTask);}这在大多数情况同步完成下完全避免了async/await 机制——如我们再次在 sharplab.io中看到的一样。我要强调如果代码是经常或仅仅进行真正的异步行为时这样做是完全没有必要的它只对于那些结果通常或仅仅会同步地产生时才有帮助。(译注对于ValueTask的hot path场景的使用这里有个视频讲过一些以及其它一些.NET中新的优化性能的方法 Adam Sitnik - State of the .NET Performance)那么Reader呢就像我们多次看到的一样reader总是稍微复杂一些——我们无从得知一个单独的“读”操作是否会准确包含一个入站消息我们也许需要开启循环直到我们获取到了所有所需的数据并且我们也许需要推回一些多余的数据。因此让我们假设我们想要消费某种单一的消息async ValueTaskSomeMessageType GetNextMessage(PipeReader reader,CancellationToken cancellationToken default){while (true){var read await reader.ReadAsync(cancellationToken);if (read.IsCanceled) ThrowCanceled();// can we find a complete frame?var buffer read.Buffer;if (TryParseFrame(buffer,out SomeMessageType nextMessage,out SequencePosition consumedTo)){reader.AdvanceTo(consumedTo);return nextMessage;}reader.AdvanceTo(buffer.Start, buffer.End);if (read.IsCompleted) ThrowEOF();        }}这里我们从pipe中获取了一些数据进行退出检查比如取消。然后我们尝试辨识一个消息这是什么意思取决于你具体的代码——它可以是从缓冲区中寻找某些特定的值比如一个ASCII行尾然后把所有到这里的数据当作一个消息丢弃行尾解析一个定义良好的二进制帧头获取其内容长度通过检查获取这样长度的数据然后处理或者其它你需要的如果我们能够辨识到一个消息我们可以告诉pipe令其丢弃我们已经消费过的数据——通过 AdvanceTo(consumedTo)在这里使用我们自己的帧解析代码告诉我们消费了多少。如果我们没能辨识出一个消息我们要做的第一件事就是告诉pipe我们什么也没消费尽管我们尝试读取了所有数据——通过 reader.AdvanceTo(buffer.Start, buffer.End)。在这里会有两种可能我们还没有获得足够的数据pipe已经死亡我们再也不会获得足够的数据我们通过read.IsCompleted 检查了这些在第二种情况时报告错误否则我们继续循环等待更多数据。那么剩下的就是我们的帧解析——我们已经把复杂的IO管理降低成了简单的操作比如如果我们的消息是以行标记分隔private static bool TryParseFrame(ReadOnlySequencebyte buffer,out SomeMessageType nextMessage,out SequencePosition consumedTo){// find the end-of-line markervar eol buffer.PositionOf((byte)\n);if (eol null){nextMessage default;consumedTo default;return false;}// read past the line-endingconsumedTo buffer.GetPosition(1, eol.Value);// consume the datavar payload buffer.Slice(0, eol.Value);nextMessage ReadSomeMessageType(payload);return true;}这里PositionOf 尝试获取第一个行标记的位置。如果一个也找不到我们就放弃否则我们将consumedTo 设为”行标记1“即我们会消费行标记然后我们分割我们的缓冲区来创建一个子集表示不包括行标记的内容这样我们就可以解析了。最终我们报告成功并且庆祝我们可以简单地解析Linux风格的行尾。这里的重点是什么用这些和大多数最简单最简朴的Stream版本没有任何nice的特性非常相似的最少量的代码我们的应用现在有了一个reader和writer利用广泛的能力确保高效和有效的处理。你可以用Stream来做所有的这些事但是这样真的、真的很难去做好做可靠。通过将所有的这些特性集成进框架许多代码都可以受益于这一单独的实现。并且它也给了那些直接在pipeline API上开发并且对自定义pipeline端点和修饰感兴趣的人更多的未来空间。总结在这节我们研究了pipeline使用的内存模型和其如何帮助我们避免分配内存然后我们研究了怎样才可以将pipeline与现有的API和系统如Stream进行交互——并且我们介绍了 Pipelines.Sockets.Unofficial 这样的可用的工具库。我们研究了在不支持 span/memory 代码的API上集成它们的可用选项最终我们展示了和pipeline交互的真正的调用代码是什么样子的并且简单地介绍了如何优化那些通常是同步的async代码——展示了我们的应用代码会是什么样子。在最后一部分我们将会研究如何在开发现实中的库比如StackExchange.Redis将我们学到的这些知识点联系起来——讨论我们在代码里需要解决哪些复杂点而pipeline又如何将它们变得简单。相关文章System.IO.Pipelines: .NET高性能IOPipelines - .NET中的新IO API指引(一)原文地址: https://zhuanlan.zhihu.com/p/39969692.NET社区新闻深度好文欢迎访问公众号文章汇总 http://www.csharpkit.com
http://www.zqtcl.cn/news/684749/

相关文章:

  • 网页设计教学网站江西省建设监督网站
  • 网站建设与发布需要什么提供网站制作公司哪家好
  • 西宁市城市道路建设规划网站探测器 东莞网站建设
  • 旅游村庄网站建设方案小程序制作价格
  • 网站地图制作软件机械加工网免费铺货
  • 网站上线有什么线上活动可以做龙华建网站多少钱
  • 门户网站系统开发建设电脑优化
  • 公司建网站多少钱一个月服务佳的广州网站建设
  • 怎么创建网站建设徐州网站建设要多少钱
  • 微网站功能列表菜市场做建筑设计图库的网站设计
  • 制作网站支付方式定制网站开发哪里好
  • 常州网络网站建设行情软件app网站大全下载
  • 出台网站集约化建设通知彩票网站开发的
  • 怎样创建个人的网站怎么学做网站
  • 小江高端网站建设网站建设中可能升级
  • 网站建设的原则有哪些内容wordpress的底部版权
  • 一个网站建立团队大概要多少钱大连专业网站建设
  • 宁波网站公司相册插件wordpress
  • 科技网站域名大型网站开发团队
  • 温岭建设规划局网站注册新公司网上怎么核名
  • dede网站移动端怎么做golang 网站开发 开源
  • 织梦网站导航固定沈阳男科医院在线咨询免费
  • 四川华鸿建设有限公司网站网站建设需求文案
  • 汕头东莞网站建设怎么制作微信小程序app
  • 网站建设预算方案模板怎么做网络推广网站
  • 顺义网站开发wordpress内容主题模板下载
  • 永康做网站的化工网站模板免费下载
  • 潍坊高密网站建设如何做网页推广
  • 杭州房产网站建设wordpress 替换谷歌字体
  • 陕西省建设工程质量安全监督总站网站开发公司与施工单位工程造价鉴定报告