簡體   English   中英

如何使用Reactive限制消耗順序?

[英]How to limit consuming sequence with Reactive?

我們有一個應用程序,其中我們有一個物化的項目數組,我們將通過Reactive管道進行處理。 它看起來有點像這樣

EventLoopScheduler eventLoop = new EventLoopScheduler();
IScheduler concurrency = new TaskPoolScheduler(
    new TaskFactory(
        new LimitedConcurrencyLevelTaskScheduler(threadCount)));
IEnumerable<int> numbers = Enumerable.Range(1, itemCount);

// 1. transform on single thread
IConnectableObservable<byte[]> source = 
    numbers.Select(Transform).ToObservable(eventLoop).Publish();

// 2. naive parallelization, restricts parallelization to Work 
// only; chunk up sequence into smaller sequences and process
// in parallel, merging results
IObservable<int> final = source.
    Buffer(10).
    Select(
        batch =>
        batch.
        ToObservable(concurrency).
        Buffer(10).
        Select(
            concurrentBatch =>
            concurrentBatch.
            Select(Work).
            ToArray().
            ToObservable(eventLoop)).
        Merge()).
    Merge();

final.Subscribe();

source.Connect();
Await(final).Wait();

如果你真的很想玩這個,那么替身方法就像

private async static Task Await(IObservable<int> final)
{
    await final.LastOrDefaultAsync();
}

private static byte[] Transform(int number)
{
    if (number == itemCount)
    {
        Console.WriteLine("numbers exhausted.");
    }
    byte[] buffer = new byte[1000000];
    Buffer.BlockCopy(bloat, 0, buffer, 0, bloat.Length);
    return buffer;
}

private static int Work(byte[] buffer)
{
    Console.WriteLine("t {0}.", Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(50);
    return 1;
}

一點解釋。 Range(1, itemCount)模擬從數據源實現的原始輸入。 Transform模擬每個輸入必須經歷的濃縮過程,並導致更大的內存占用。 Work是一個“漫長”的過程,對轉換后的輸入進行操作。

理想情況下,我們希望最小化系統同時保存的轉換輸入的數量,同時通過並行化Work最大化吞吐量。 內存中已轉換輸入的數量應為批量大小( 10以上)乘以並發工作線程( threadCount )。

因此,對於5個線程,我們應該在任何給定時間保留50個Transform項目; 如果這里的變換是一個1MB字節的緩沖區,那么我們預計在整個運行過程中內存消耗大約為50MB。

我發現的是完全不同的。 也就是說Reactive正在急切地消耗所有numbers ,並且預先對它們進行Transform (由numbers exhausted.消息證明),導致前面的大量內存峰值(1000 itemCount為@ 1GB)。

我的基本問題是:有沒有辦法實現我所需要的(即最小化消耗,受多線程批處理限制)?

更新:抱歉逆轉詹姆斯; 起初,我不認為paulpdaniels和Enigmativity的Work(Transform)應用(這與我們實際實現的性質有關,這比上面提供的簡單場景更復雜),然而,經過一些進一步的實驗,我可能能夠應用相同的原則:即延遲轉換直到批處理執行。

你的代碼犯了一些錯誤,導致你得出所有的結論。

首先,你做到了這一點:

IEnumerable<int> numbers = Enumerable.Range(1, itemCount);

您已經使用了Enumerable.Range ,這意味着當您調用數字時。選擇numbers.Select(Transform)您將以單個線程可以接受的速度刻錄所有numbers Rx甚至沒有機會做任何工作,因為到目前為止你的管道完全可以枚舉。

下一個問題在您的訂閱中:

final.Subscribe();

source.Connect();
Await(final).Wait();

因為你調用final.Subscribe()final.Subscribe() Await(final).Wait(); 您正在為final observable創建兩個單獨的訂閱。

由於中間有source.Connect() ,因此第二個訂閱可能會丟失值。

所以,讓我們嘗試刪除所有正在發生的事情,看看我們是否可以解決問題。

如果你談到這個:

IObservable<int> final =
    Observable
        .Range(1, itemCount)
        .Select(n => Transform(n))
        .Select(bs => Work(bs));

事情很順利。 最后這些數字已經耗盡,在我的機器上處理20個項目大約需要1秒鍾。

但這是按順序處理所有事情。 並且“ Work步驟為“ Transform提供反壓,以降低其消耗數字的速度。

讓我們添加並發性。

IObservable<int> final =
    Observable
        .Range(1, itemCount)
        .Select(n => Transform(n))
        .SelectMany(bs => Observable.Start(() => Work(bs)));

這在0.284秒內處理20個項目,並且在處理5個項目之后數字自行耗盡。 這些數字不再有任何背壓。 基本上,調度程序將所有工作交給Observable.Start以便立即為下一個數字做好准備。

讓我們減少並發性。

IObservable<int> final =
    Observable
        .Range(1, itemCount)
        .Select(n => Transform(n))
        .SelectMany(bs => Observable.Start(() => Work(bs), concurrency));

現在,20個項目在0.5秒內得到處理。 在數字耗盡之前只有兩個被處理。 這是有道理的,因為我們將並發性限制為兩個線程。 但仍然沒有對數字消費的背壓,所以他們很快就被嚼起來了。

說完所有這些之后,我嘗試用適當的背壓構建一個查詢,但我找不到方法。 關鍵在於, Transform(...)執行速度遠遠快於Work(...)因此它的完成速度要快得多。

那么對我來說顯而易見的舉動是這樣的:

IObservable<int> final =
    Observable
        .Range(1, itemCount)
        .SelectMany(n => Observable.Start(() => Work(Transform(n)), concurrency));

這直到最后才完成數字,並且它將處理限制為兩個線程。 除了我必須一起做Work(Transform(...))之外,它似乎對你想要的東西做了正確的事情。

事實上,你想要限制你正在做的工作量,這表明你應該提取數據,而不是把它推向你。 我會忘記在這種情況下使用Rx,從根本上說,你所描述的並不是一個被動的應用程序。 此外,Rx最適合連續處理物品; 它使用順序事件流。

為什么不保持數據源可枚舉,並使用PLinqParallel.ForEachDataFlow 所有這些聽起來更適合您的問題。

正如@JamesWorld所說,你很可能想要使用PLinq來執行這項任務,這實際上取決於你是否真的對真實場景中的數據作出反應 ,或者只是迭代它。

如果選擇使用Reactive路由,則可以使用Merge來控制並行化的級別:

var source = numbers
  .Select(n => 
          Observable.Defer(() => Observable.Start(() => Work(Transform(n)), concurrency)))
  //Maximum concurrency
  .Merge(10)
  //Schedule all the output back onto the event loop scheduler
  .ObserveOn(eventLoop);

上面的代碼將首先消耗所有數字(遺憾的是無法避免這種情況),但是,通過將處理包裝在Defer並使用限制並行化的Merge跟蹤,一次只能處理x個項目。 Start()將調度程序作為它用於執行提供的方法的第二個參數。 最后,由於您基本上只是將Transform的值推送到Work我在Start方法中組合它們。

作為旁注,您可以await一個Observable ,它將等同於您擁有的代碼,即:

await source; //== await source.LastAsync();

暫無
暫無

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

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