簡體   English   中英

如何停止傳播異步 stream (IAsyncEnumerable)

[英]How to stop propagating an asynchronous stream (IAsyncEnumerable)

我有一個接受IAsyncEnumerable作為參數的方法,並且還返回一個IAsyncEnumerable 它為輸入 stream 中的每個項目調用 web 方法,並將結果傳播到 output ZF27ED544CFAFD5C529。 我的問題是,如果我的方法的調用者已停止枚舉 output stream,如何通知我,這樣我就可以停止枚舉我的方法內部的輸入 ZF7B44CFAFD5C52223D5498196C8A2E7B? 似乎我應該能夠收到通知,因為調用者默認處理從我的方法獲取的IAsyncEnumerator 是否有任何內置機制為編譯器生成的異步方法生成這樣的通知? 如果不是,最容易實施的替代方案是什么?

例子。 web 方法驗證 url 是否有效。 提供的 url 有一個永無止境的 stream,但是當發現超過 2 個無效 url 時調用者停止枚舉結果:

var invalidCount = 0;
await foreach (var result in ValidateUrls(GetMockUrls()))
{
    Console.WriteLine($"Url {result.Url} is "
        + (result.IsValid ? "OK" : "Invalid!"));
    if (!result.IsValid) invalidCount++;
    if (invalidCount > 2) break;
}
Console.WriteLine($"--Async enumeration finished--");
await Task.Delay(2000);

網址的生成器。 每 300 毫秒生成一個 url。

private static async IAsyncEnumerable<string> GetMockUrls()
{
    int index = 0;
    while (true)
    {
        await Task.Delay(300);
        yield return $"https://mock.com/{++index:0000}";
    }
}

url 的驗證器。 有一個要求是輸入 stream 被急切地枚舉,因此兩個異步工作流並行運行。 第一個工作流程將 url 插入隊列中,第二個工作流程逐個挑選 url 並驗證它們。 BufferBlock用作異步隊列。

private static async IAsyncEnumerable<(string Url, bool IsValid)> ValidateUrls(
    this IAsyncEnumerable<string> urls)
{
    var buffer = new System.Threading.Tasks.Dataflow.BufferBlock<string>();
    _ = Task.Run(async () =>
    {
        await foreach (var url in urls)
        {
            Console.WriteLine($"Url {url} received");
            await buffer.SendAsync(url);
        }
        buffer.Complete();
    });

    while (await buffer.OutputAvailableAsync() && buffer.TryReceive(out var url))
    {
        yield return (url, await MockValidateUrl(url));
    }
}

澄清:隊列是強制性的,刪除它不是一種選擇。 它是這個問題的一個重要組成部分。

單個 url 的驗證器。 驗證過程平均持續 300 毫秒。

private static Random _random = new Random();
private static async Task<bool> MockValidateUrl(string url)
{
    await Task.Delay(_random.Next(100, 600));
    return _random.Next(0, 2) != 0;
}

Output:

Url https://mock.com/0001 received
Url https://mock.com/0001 is Invalid!
Url https://mock.com/0002 received
Url https://mock.com/0003 received
Url https://mock.com/0002 is OK
Url https://mock.com/0004 received
Url https://mock.com/0003 is Invalid!
Url https://mock.com/0005 received
Url https://mock.com/0004 is OK
Url https://mock.com/0005 is OK
Url https://mock.com/0006 received
Url https://mock.com/0006 is Invalid!
--Async enumeration finished--
Url https://mock.com/0007 received
Url https://mock.com/0008 received
Url https://mock.com/0009 received
Url https://mock.com/0010 received
Url https://mock.com/0011 received
Url https://mock.com/0012 received
...

問題是在調用者/客戶端完成異步枚舉后仍然會生成和接收 url。 我想解決這個問題,以便在--Async enumeration finished--之后控制台中不再出現消息。

編輯

使用適當的示例,討論會更容易。 驗證 URL 並不昂貴。 如果您需要點擊例如 100 個 URL 並選擇前 3 個響應怎么辦?

在這種情況下,工作人員和緩沖區都有意義。

編輯 2

其中一條評論增加了額外的復雜性——任務是同時執行的,結果需要在它們到達時發出。


對於初學者, ValidateUrl可以重寫為迭代器方法:

private static async IAsyncEnumerable<(string Url, bool IsValid)> ValidateUrls(
    this IAsyncEnumerable<string> urls)
{
    await foreach (var url in urls)
    {
        Console.WriteLine($"Url {url} received");
        var isValid=await MockValidateUrl(url);
        yield return (url, isValid);
    }
}

不需要工作任務,因為所有方法都是異步的。 除非消費者要求結果,否則迭代器方法不會繼續。 即使MockValidateUrl做了一些昂貴的事情,它也可以使用Task.Run本身或包裝在Task.Run中。 不過,這會產生相當多的任務。

為了完整起見,您可以添加CancellationTokenConfigureAwait(false)

public static async IAsyncEnumerable<(string Url, bool IsValid)> ValidateUrls(
       IAsyncEnumerable<string> urls, 
       [EnumeratorCancellation]CancellationToken token=default)
{
    await foreach(var url in urls.WithCancellation(token).ConfigureAwait(false))
    {
        var isValid=await MockValidateUrl(url).ConfigureAwait(false);
        yield return (url,isValid);
    }
}

無論如何,只要調用者停止迭代, ValidateUrls就會停止。

緩沖

緩沖是一個問題——無論它是如何編程的,工作人員在緩沖區填滿之前都不會停止。 緩沖區的大小是工作人員在意識到需要停止之前將 go 進行多少次迭代。 這對於 Channel 來說是一個很好的案例(是的,再次:):

public static IAsyncEnumerable<(string Url, bool IsValid)> ValidateUrls(
        IAsyncEnumerable<string> urls,CancellationToken token=default)
{
    var channel=Channel.CreateBounded<(string Url, bool IsValid)>(2);
    var writer=channel.Writer;
    _ = Task.Run(async ()=>{
                await foreach(var url in urls.WithCancellation(token))
                {
                    var isValid=await MockValidateUrl(url);
                    await writer.WriteAsync((url,isValid));
                }
        },token)
        .ContinueWith(t=>writer.Complete(t.Exception));        
    return channel.Reader.ReadAllAsync(token);
}

不過,最好傳遞 ChannelReaders 而不是 IAsyncEnumerables。 至少,在有人嘗試從 ChannelReader 讀取之前,不會構造異步枚舉器。 將管道構建為擴展方法也更容易:

public static ChannelReader<(string Url, bool IsValid)> ValidateUrls(
        this ChannelReader<string> urls,int capacity,CancellationToken token=default)
{
    var channel=Channel.CreateBounded<(string Url, bool IsValid)>(capacity);
    var writer=channel.Writer;
    _ = Task.Run(async ()=>{
                await foreach(var url in urls.ReadAllAsync(token))
                {
                    var isValid=await MockValidateUrl(url);
                    await writer.WriteAsync((url,isValid));
                }
        },token)
        .ContinueWith(t=>writer.Complete(t.Exception));        
    return channel.Reader;
}

這種語法允許以流暢的方式構建管道。 假設我們有這個幫助方法來將 IEnumerables 轉換為通道(或 IAsyncEnumerables):

public static ChannelReader<T> AsChannel(
         IEnumerable<T> items)
{
    var channel=Channel.CreateUnbounded();        
    var writer=channel.Writer;
    foreach(var item in items)
    {
        channel.TryWrite(item);
    }
    return channel.Reader;
}

我們可以寫:

var pipeline=urlList.AsChannel()     //takes a list and writes it to a channel
                    .ValidateUrls();

await foreach(var (url,isValid) in pipeline.ReadAllAsync())
{
   //Use the items here
}

立即傳播的並發調用

使用通道很容易,盡管此時工作人員需要立即觸發所有任務。 本質上,我們需要多個工人。 這不是僅使用 IAsyncEnumerable 就可以完成的。

首先,如果我們想使用例如 5 個並發任務來處理我們可以編寫的輸入

    var tasks = Enumerable.Range(0,5).
                  .Select(_ => Task.Run(async ()=>{
                                 /// 
                             },token));
    _ = Task.WhenAll(tasks)(t=>writer.Complete(t.Exception));        

代替:

    _ = Task.Run(async ()=>{
        /// 
        },token)
        .ContinueWith(t=>writer.Complete(t.Exception));        

使用大量工人就足夠了。 我不確定 IAsyncEnumerable 是否可以被多個工作人員使用,我也不想知道。

提前取消

如果客戶端消耗所有結果,則上述所有工作。 但是,要在前 5 個結果之后停止處理,我們需要 CancellationToken:

var cts=new CancellationTokenSource();

var pipeline=urlList.AsChannel()     //takes a list and writes it to a channel
                    .ValidateUrls(cts.Token);

int i=0;
await foreach(var (url,isValid) in pipeline.ReadAllAsync())
{
    //Break after 3 iterations
    if(i++>2)
    {
        break;
    }
    ....
}

cts.Cancel();

此代碼本身可以在接收 ChannelReader 的方法中提取,在本例中為 CancellationTokenSource:

static async LastStep(this ChannelReader<(string Url, bool IsValid)> input,CancellationTokenSource cts)
    {
    int i=0;
    await foreach(var (url,isValid) in pipeline.ReadAllAsync())
    {
        //Break after 3 iterations
        if(i++>2)
        {
            break;
        }
        ....
    }

    cts.Cancel();        
}

管道變為:

var cts=new CancellationTokenSource();

var pipeline=urlList.AsChannel()     
                    .ValidateUrls(cts.Token)
                    .LastStep(cts);

我想我應該回答我自己的問題,因為我現在有一個足夠簡單的通用解決方案。

更新:我正在刪除我之前的答案,因為我發現了一個更簡單的解決方案。 其實很簡單。 我所要做的就是將ValidateUrls迭代器的產生部分包含在一個try-finally塊中。 finally塊將在每種情況下執行,無論是由調用者正常完成枚舉,還是由break或異常異常執行。 所以這就是我可以通過在finally上取消CancellationTokenSource來獲得我正在尋找的通知的方式:

private static async IAsyncEnumerable<(string Url, bool IsValid)> ValidateUrls(
    this IAsyncEnumerable<string> urls)
{
    var buffer = new System.Threading.Tasks.Dataflow.BufferBlock<string>();
    var completionCTS = new CancellationTokenSource();
    _ = Task.Run(async () =>
    {
        await foreach (var url in urls)
        {
            if (completionCTS.IsCancellationRequested) break;
            Console.WriteLine($"Url {url} received");
            await buffer.SendAsync(url);
        }
        buffer.Complete();
    });

    try
    {
        while (await buffer.OutputAvailableAsync() && buffer.TryReceive(out var url))
        {
            yield return (url, await MockValidateUrl(url));
        }
    }
    finally // This runs when the caller completes the enumeration
    {
        completionCTS.Cancel();
    }
}

我可能應該注意到不支持取消的異步迭代器不是一個好習慣。 沒有它,調用者就沒有簡單的方法來停止消耗一個值和下一個值之間的等待。 因此,我的方法的更好簽名應該是:

private static async IAsyncEnumerable<(string Url, bool IsValid)> ValidateUrls(
    this IAsyncEnumerable<string> urls,
    [EnumeratorCancellation]CancellationToken cancellationToken = default)
{

然后可以將令牌傳遞給產生循環的等待方法OutputAvailableAsyncMockValidateUrl

從調用者的角度來看,令牌可以直接傳遞,也可以通過鏈接擴展方法WithCancellation來傳遞。

await foreach (var result in ValidateUrls(GetMockUrls()).WithCancellation(token))

暫無
暫無

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

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