Advanced development: dotnet core multipath asynchronous termination

Time:2021-8-19

Today, let’s talk about asynchronous multipath termination with a simple example. I try to write it easy to understand, but today’s content needs to have a certain programming ability.

Today’s topic comes from some recent technical research on grpc.

The topic itself has little to do with grpc. In the application, I used the relatively complex concept of full duplex data pipeline.

We know that full duplex connection is a connection between two nodes, but it is not a simple “request response” connection. Any node can send messages at any time. Conceptually, there is still a distinction between the client and the server, but this is only conceptually to distinguish who is listening for connection attempts and who is establishing connections. In fact, making a duplex API is much more complex than making a “request response” API.

Thus, another idea is extended: make a class library, build a duplex pipeline inside the library, and expose only simple content and familiar ways when supplying consumers.

    In order to prevent not providing the reprint of the original website, the original link is added here:https://www.cnblogs.com/tiger-wang/p/14297970.html

1、 Start

Suppose we have an API:

  • Client establish connection
  • There is oneSendAsyncMessages are sent from the client to the server
  • There is oneTryReceiveAsyncMessage, trying to wait for a message from the server (the server sends a message as true and returns it as false)
  • The server controls the termination of the data flow. If the server sends the last message, the client will not send any messages.

The interface code can be written as follows:

interface ITransport : IAsyncDisposable
{
    ValueTask SendAsync(TRequest request, CancellationToken cancellationToken);
    ValueTask TryReceiveAsync(CancellationToken cancellationToken);
}

Ignoring the connected part, the code doesn’t look complex.

Next, we create two loops and expose the data through the enumerator:

ITransport transport;
public async IAsyncEnumerable ReceiveAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
    while (true)
    {
        var (success, message) =
            await transport.TryReceiveAsync(cancellationToken);
        if (!success) break;
        yield return message;
    }
}

public async ValueTask SendAsync(IAsyncEnumerable data, CancellationToken cancellationToken)
{
    await foreach (var message in data.WithCancellation(cancellationToken))
    {
        await transport.SendAsync(message, cancellationToken);
    }
}

The concepts related to asynchronous iterators are used. If you don’t understand, you can read another article about asynchronous iterators【Portal】。

2、 Resolve termination flag

It seems to be done. We use a loop to receive and send, and pass an external termination flag to these two methods.

Are you really ready?

Not yet. The problem is the termination flag. We don’t take into account that the two flows are interdependent. In particular, we don’t want the producer (using sendasync’s code) to still run in any scenario of connection failure.

In fact, there will be more termination paths than we think:

  • We may have provided an external termination token for both methods, and this token may have been triggered
  • ReceiveAsyncConsumers may have passedWithCancellationA termination token was provided toGetAsyncEnumeratorAnd the token may have been triggered
  • Our send / receive code may have gone wrong
  • ReceiveAsyncThe consumer of wants to terminate the data acquisition in the middle of the data acquisition – a simple reason is that there is an error in processing the received data
  • SendAsyncAn error may have occurred with the producer in

These are just some possible examples, but there may be more.

In essence, these represent the termination of the connection, so we need to include all these scenarios in some way to allow the problem to be communicated between the sending and receiving paths. In other words, we need our ownCancellationTokenSource

Obviously, it is perfect to use the library to solve this demand. We can put these complex content in a single API that consumers can access:

public IAsyncEnumerable Duplex(IAsyncEnumerable request, CancellationToken cancellationToken = default);

This method:

  • Allow it to pass in a producer
  • It passes in an external termination token
  • There is an asynchronous response return

When using, we can do this:

await foreach (MyResponse item in client.Duplex(ProducerAsync()))
{
    // ... todo
}
async IAsyncEnumerable ProducerAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    for (int i = 0; i < 100; i++)
    {
        yield return new MyRequest(i);
        await Task.Delay(100, cancellationToken);
    }
}

In the above code, weProducerAsyncThere is not much content implemented yet. At present, only a placeholder is passed. We can enumerate it later, and the code is actually called.

go back toDuplex。 For this method, at least two different termination methods need to be considered:

  • adoptcancellationTokenIncoming external token
  • May be passed toGetAsyncEnumerator()Potential tokens for

Here, why not the more termination methods listed earlier? The combination of compilers should be considered here. What we need is not oneCancellationToken, but aCancellationTokenSource

public IAsyncEnumerable Duplex(IAsyncEnumerable request, CancellationToken cancellationToken = default) => DuplexImpl(transport, request, cancellationToken);

private async static IAsyncEnumerable DuplexImpl(ITransport transport, IAsyncEnumerable request, CancellationToken externalToken, [EnumeratorCancellation] CancellationToken enumeratorToken = default)
{
    using var allDone = CancellationTokenSource.CreateLinkedTokenSource(externalToken, enumeratorToken);
    // ... todo
}

here,DuplexImplMethod allows the enumeration to terminate, but remains separate from the external termination tag. In this way, it will not be merged at the compiler level. on the inside,CreateLinkedTokenSourceAnti inversion compiler processing.

Now, we have oneCancellationTokenSource, we may use it to terminate the loop when necessary.

using var allDone = CancellationTokenSource.CreateLinkedTokenSource(externalToken, enumeratorToken);
try
{
    // ... todo
}
finally
{
    allDone.Cancel();
}

In this way, we can deal with a scenario where the consumer does not get all the data and we want to trigger itallDone, but we quitDuplexImpl。 At this time, the iterator plays a great role. It makes the program simpler because it usesusingIn the end, everything in it will be locatedDispose/DisposeAsync

Next is the producer, that isSendAsync。 It is also duplex and has no effect on incoming messages, so it can be usedTask.RunStart running as a separate code path, and terminate the transmission if the producer has an error. In the todo part above, you can add:

var send = Task.Run(async () =>
{
    try
    {
        await foreach (var message in request.WithCancellation(allDone.Token))
        {
            await transport.SendAsync(message, allDone.Token);
        }
    }
    catch
    {
        allDone.Cancel();
        throw;
    }
}, allDone.Token);

// ... todo: receive

await send;

Here, a producer’s parallel operation is startedSendAsync。 Note that here we use a markerallDone.TokenPass the termination tag of the combination to the producer. delayawaitTo allowProducerAsyncMethod can use a termination token to meet the life cycle requirements of composite duplex operation.

In this way, the receiving code becomes:

while (true)
{
    var (success, message) = await transport.TryReceiveAsync(allDone.Token);
    if (!success) break;
    yield return message;
}

allDone.Cancel();

Finally, put this part of the code together:

private async static IAsyncEnumerable DuplexImpl(ITransport transport, IAsyncEnumerable request, CancellationToken externalToken, [EnumeratorCancellation] CancellationToken enumeratorToken = default)
{
    using var allDone = CancellationTokenSource.CreateLinkedTokenSource(externalToken, enumeratorToken);
    try
    {
        var send = Task.Run(async () =>
        {
            try
            {
                await foreach (var message in request.WithCancellation(allDone.Token))
                {
                    await transport.SendAsync(message, allDone.Token);
                }
            }
            catch
            {
                allDone.Cancel();
                throw;
            }
        }, allDone.Token);

        while (true)
        {
            var (success, message) = await transport.TryReceiveAsync(allDone.Token);
            if (!success) break;
            yield return message;
        }

        allDone.Cancel();

        await send;
    }
    finally
    {
        allDone.Cancel();
    }
}

3、 Summary

There are so many related processes. The key points here are:

  • Both external token and enumerator token are correctallDoneContribute
  • Use of sending and receiving codes in transmissionallDone.Token
  • Producer enumeration useallDone.Token
  • Exit the enumerator in any case,allDoneWill be terminated
    • If the transmission receives an error, thenallDoneTerminated
    • If the consumer terminates early, thenallDoneTerminated
  • When we receive the last message from the server,allDoneTerminated
  • If the producer or transport sends an error,allDoneTerminated

Finally, a little more aboutConfigureAwait(false)

By default,awaitContains a pairSynchronizationContext.CurrentInspection of. In addition to representing additional context switches, in the case of UI applications, it also means running on UI threadsCode that does not need to run on the UI thread。 Library code usually does not need to do this. Therefore, in the library code, it should be used in all places where await is used. configureawait (false)To bypass this check. In general application code, you should use only by defaultawaitInstead of usingConfigureAwaitUnless you know what you’re doing.

WeChat official account: Lao Wang Plus

Scanning the two-dimensional code, paying attention to the official account, can get the latest personal articles and content push.

The copyright of this article belongs to the author. Please keep this statement and the link to the original text

Recommended Today

The selector returned by ngrx store createselector performs one-step debugging of fetching logic

Test source code: import { Component } from ‘@angular/core’; import { createSelector } from ‘@ngrx/store’; export interface State { counter1: number; counter2: number; } export const selectCounter1 = (state: State) => state.counter1; export const selectCounter2 = (state: State) => state.counter2; export const selectTotal = createSelector( selectCounter1, selectCounter2, (counter1, counter2) => counter1 + counter2 ); // […]