Is it worth to wrap TcpClient WriteAsync and ReadAsync into MailboxProcessor?

243 Views Asked by At

Summary of questions:

  1. Are Stream.WriteAsync and Stream.ReadAsync not blocking each other and can run on 2 cpu cores fully in parallel?
  2. Is there a way to notify wait for external source to notify in async{} ?
  3. What is performance impact of MailboxProcessor as a wrapper for TcpClient ?

Some explanation:

As I understand from source code of Stream.cs async operations done synchronously and not completely in parallel (if you read something, it blocks other read attempts until finished and also blocks write attemts, same in other way - write blocks read and write attempts), the only benefit that it doesn't block thread where you call writeAsync/readAsync

So I have wrapped TcpClient into MailboxProcessor that accepts

type AgentRequest<'a> =
  | Write of 'a
  | Read  of int * AsyncReplyChannel<'a>

Purpose of it was to reconnect client if connection failed, and during this time we don't want to attempt to read or write anything. This is also possible with thread synchronization techniques, but that require lot more code.

I didn't get to position when I can benchmark the solution, but is there any benchmarks of MailboxProcessor internals to see what impact will it make to tcp connection (if performance impact is small in terms of whole request/response time)

Also this is created to send requests to server that guarantees order of responses to be same as requests received But I can't rely on Write requestA -> Read responseA to read the correct response: tread1: Write requestA Read responseA tread2: Write requestB Read responseB

Queue: ['write requestA'; 'write requestB'; 'read responseB'; 'read responseA'] And this will lead that responseA will be returned to thread2 and responseB to thread1.

Good thing that request-response are correlated by id set in request. So there are some solutions for this protocol that store Dictionary<id, TaskCompletionSource>. This makes it possible to wait till Task is finished (TaskCompletionSource set result) and then proceed with result. Result set by separate single thread that keep reading responses from tcp stream and mapping them by id.

In F# it's much beautiful to use Async instead of Task, so the way I see how to map responses to correct requests is to store Dictionary<id, response -> unit> Or I can have only 1 message for MailboxProcessor like Send request and do not separate write request from read response.

Is there any other way similar to TaskCompletionSource but for Async, so I could have:

// instead of
async {
  do! send request
  sharedDictionary.Add(request.id, fun resp -> () (*do something with this response*) )
  read() }

// do something like
async {
  do! send request
  let! response = read request.id
  (*do something with this response*) }
0

There are 0 best solutions below