[英]Parallel receiving data from several IAsyncEnumerable streams
我有一個案例需要從多個 IAsyncEnumerable 源接收數據。 為了提高性能,它應該以並行方式執行。
我已經使用AsyncAwaitBestPractices 、System.Threading.Tasks.Dataflow和System.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.