簡體   English   中英

從多個 IAsyncEnumerable 流並行接收數據

[英]Parallel receiving data from several IAsyncEnumerable streams

我有一個案例需要從多個 IAsyncEnumerable 源接收數據。 為了提高性能,它應該以並行方式執行。

我已經使用AsyncAwaitBestPracticesSystem.Threading.Tasks.DataflowSystem.Linq.Async nuget 包編寫了這樣的代碼來實現這個目標:

public static async IAsyncEnumerable<T> ExecuteSimultaneouslyAsync<T>(
        this IEnumerable<IAsyncEnumerable<T>> sources,
        int outputQueueCapacity = 1,
        TaskScheduler scheduler = null)
    {
        var sourcesCount = sources.Count();

        var channel = outputQueueCapacity > 0 
            ? Channel.CreateBounded<T>(sourcesCount)
            : Channel.CreateUnbounded<T>();

        sources.AsyncParallelForEach(
                async body => 
                {
                    await foreach (var item in body)
                    {
                        await channel.Writer.WaitToWriteAsync();
                        await channel.Writer.WriteAsync(item);
                    }
                },
                maxDegreeOfParallelism: sourcesCount,
                scheduler: scheduler)
            .ContinueWith(_ => channel.Writer.Complete())
            .SafeFireAndForget();

        while (await channel.Reader.WaitToReadAsync())
            yield return await channel.Reader.ReadAsync();
    }

public static async Task AsyncParallelForEach<T>(
    this IEnumerable<T> source,
    Func<T, Task> body,
    int maxDegreeOfParallelism = DataflowBlockOptions.Unbounded,
    TaskScheduler scheduler = null)
{
    var options = new ExecutionDataflowBlockOptions
    {
        MaxDegreeOfParallelism = maxDegreeOfParallelism
    };

    if (scheduler != null)
        options.TaskScheduler = scheduler;

    var block = new ActionBlock<T>(body, options);

    foreach (var item in source)
        block.Post(item);

    block.Complete();

    await block.Completion;
}

此代碼工作正常,直到兩個或更多源引發異常。 在某些情況下,它會導致無法處理第二個異常並破壞應用程序的情況。

所以我想知道是否有更好的方法以並行方式使用來自多個 IAsyncEnumerable 源的數據?

無論是功能性管道還是 CSP 管道,在異常情況下保持管道運行都是極其困難的。 在大多數情況下,即使在個別消息失敗的情況下,管道也需要繼續工作。 失敗的消息並不意味着整個管道都失敗了。

這就是為什么使用面向鐵路的編程將消息錯誤包裝到Result<TOk,TError>包裝器中並“重定向”或忽略錯誤消息的原因。 這樣的 class 使編程 Dataflow、Channels 和IAsyncEnumerable管道變得容易得多。

在 F# 中,使用有區別的聯合,可以定義一個Result 類型

type Result<'T,'TError> =
    | Ok of ResultValue:'T
    | Error of ErrorValue:'TError

DU 還沒有在 C# 中,因此已經提出了各種替代方案,一些使用來自IResult<>基礎的 inheritance,一些使用允許詳盡模式匹配的類/記錄,這是IResult<>技術不可用的。

假設這里的Result<>是:

public record Result<T>(T? result, Exception? error)
{
    public bool IsOk => error == null;
    public static Result<T> Ok(T result) => new(result, default);
    public static Result<T> Fail(Exception exception) =>
        new(default, exception);

    public static implicit operator Result<T> (T value) 
        =>  Result<T>.Ok(value);
    public static implicit operator Result<T>(Exception err) 
        => Result<T>.Fail(err);
}

第一步是創建一個CopyAsync助手,它將所有數據從輸入IAsyncEnumerable<Result<T>>復制到 output ChannelWriter<Result<T>>

public static async Task CopyToAsync<T>(
           this IAsyncEnumerable<Result<T>> input, 
           ChannelWriter<Result<T>> output,
           CancellationToken token=default)
{
    try
    {
        await foreach(var msg in input.WithCancellationToken(token).ConfigureAwait(false))
        {
            await output.WriteAsync(msg).ConfigureAwait(false);
        }
    }
    catch(Exception exc)
    {
        await output.WriteAsync(Result.Fail(exc)).ConfigureAwait(false);
    }
}

這樣,即使拋出異常,也會發出失敗消息而不是中止管道。

這樣,您可以通過將輸入消息復制到 output 通道來合並多個源:

public static ChannelReader<Result<T>> Merge(
        this IEnumerable<IAsyncEnumerable<Result<T>> inputs,
        CancellationToken token=default)
{
    var channel=Channel.CreateBounded<Result<T>>(1);

    var tasks = inputs.Select(inp=>CopyToAsync(channel.Writer,token));

    _ = Task.WhenAll(tasks)
            .ContinueWith(t=>channel.Writer.TryComplete(t.Exception));

    return channel.Reader;
}

使用 BoundedCapacity=1 可以維持下行通道或消費者的背壓行為。

您可以通過Channel.ReadAllAsync(CancellationToken)讀取 ChannelReader 中的所有消息:

IEnumerable<IAsyncEnumerable<Result<T>>> sources = ...;
var merged=sources.Merge();
await foreach(var msg in merged.ReadAllAsync())
{
    //Pattern magic to get Good results only
    if(msg is ({} value,null)
    {
        //Work with value
    }
}

您可以通過返回IAsyncEnumerable<>來避免暴露通道:

public static IAsyncEnumerable<Result<T>> MergeAsync(
        this IEnumerable<IAsyncEnumerable<Result<T>> inputs,
        CancellationToken token=default)
{
    return inputs.Merge(token).ReadAllAsync(token);
}

您可以使用System.Linq.Async使用 LINQ 方法在 IAsyncEnumerable<> 上工作,例如將IAsyncEnumerable<T>轉換為IAsyncEnumerable<Result<T>>

source.Select(msg=>Result.Ok(msg))

或者在處理失敗消息之前過濾它們:

source.Where(msg=>msg.IsOk)

您可以創建一個將Func<T1,Task<T2>>應用於輸入並將結果或錯誤作為結果傳播的方法:

public async Task<Result<T2>> ApplyAsync<T1,T2>(this Result<T1> msg,
                                               Func<T1,Task<T2>> func)
{            
    if (msg is (_, { } err))
    {
        return err;
    }
    try
    {
        var result = await func(msg.result).ConfigureAwait(false);
        return result;
    }
    catch(Exception exc)
    {
        return exc;
    }
}

這有點……在 F# 中更容易

受此答案的啟發,我決定更新自己的代碼(請參閱相關依賴項)

public record Result<T>(T Data = default, Exception error = null)
{
    public bool IsOk => error == null;
    public static Result<T> Ok(T result) => new(result, default);
    public static Result<T> Fail(Exception exception) =>
        new(default, exception);

    public static implicit operator Result<T>(T value)
        => Ok(value);

    public static implicit operator Result<T>(Exception err)
        => Fail(err);
}

public static async ValueTask AsyncParallelForEach<T>(
    this IEnumerable<T> source,
    Func<T, Task> body,
    int maxDegreeOfParallelism = DataflowBlockOptions.Unbounded,
    TaskScheduler scheduler = null)
{
    var options = new ExecutionDataflowBlockOptions
    {
        MaxDegreeOfParallelism = maxDegreeOfParallelism
    };

    if (scheduler != null)
        options.TaskScheduler = scheduler;

    var block = new ActionBlock<T>(body, options);

    foreach (var item in source)
        block.Post(item);

    block.Complete();

    await block.Completion;
}

public static async IAsyncEnumerable<Result<T>> ExecuteParallelAsync<T>(
    this IEnumerable<IAsyncEnumerable<T>> sources,
    int outputQueueCapacity = 1,
    TaskScheduler scheduler = null)
{
    var sourcesCount = sources.Count();

    var channel = outputQueueCapacity > 0
        ? Channel.CreateBounded<Result<T>>(sourcesCount)
        : Channel.CreateUnbounded<Result<T>>();

    sources.AsyncParallelForEach(
            async body =>
            {
                try
                {
                    await foreach (var item in body)
                    {
                        if (await channel.Writer.WaitToWriteAsync().ConfigureAwait(false))
                            await channel.Writer.WriteAsync(item).ConfigureAwait(false);
                    }
                }
                catch (Exception ex)
                {
                    if (await channel.Writer.WaitToWriteAsync().ConfigureAwait(false))
                        await channel.Writer.WriteAsync(Result<T>.Fail(ex)).ConfigureAwait(false);
                }
            },
            maxDegreeOfParallelism: sourcesCount,
            scheduler: scheduler)
        .AsTask()
        .ContinueWith(_ => channel.Writer.Complete())
        .SafeFireAndForget();

    while (await channel.Reader.WaitToReadAsync().ConfigureAwait(false))
        yield return await channel.Reader.ReadAsync().ConfigureAwait(false);
}

也許這個代碼比原始答案中的代碼更容易理解。

注意。 您需要使用 c# 9 或以上語言版本才能使用記錄此外,如果您使用的是 .net 框架 4x(我必須這樣做),您必須執行本文中描述的一些技巧。 簡而言之,您必須在項目的某處編寫以下代碼:

namespace System.Runtime.CompilerServices
{
    using System.ComponentModel;
    /// <summary>
    /// Reserved to be used by the compiler for tracking metadata.
    /// This class should not be used by developers in source code.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    internal static class IsExternalInit { }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM