Making an IObservable that uses async/await return completed tasks in original order

后端 未结 2 1463
难免孤独
难免孤独 2021-02-01 09:56

Suppose you have a list of 100 urls and you want to download them, parse the response and push the results through an IObservable:

public IObservable

        
相关标签:
2条回答
  • 2021-02-01 10:29

    Give this a try:

    urls.ToObservable()
        .Select(url => Observable.FromAsync(async () => {
            var bytes = await this.DownloadImage(url);
            var image = await this.ParseImage(bytes);
            return image;        
        }))
        .Merge(6 /*at a time*/);
    

    What are we doing here?

    For each URL, we're creating a Cold Observable (i.e. one that won't do anything at all, until somebody calls Subscribe). FromAsync returns an Observable that, when you Subscribe to it, runs the async block you gave it. So, we're Selecting the URL into an object that will do the work for us, but only if we ask it later.

    Then, our result is an IObservable<IObservable<Image>> - a stream of Future results. We want to flatten that stream, into just a stream of results, so we use Merge(int). The merge operator will subscribe to n items at a time, and as they come back, we'll subscribe to more. Even if url list is very large, the items that Merge are buffering are only a URL and a Func object (i.e. the description of what to do), so relatively small.

    0 讨论(0)
  • 2021-02-01 10:42

    One way to achieve what you want is to use the Observable.StartAsync method, a SemaphoreSlim, and the Concat operator. The Observable.StartAsync will create hot observables (started immediately), the SemaphoreSlim will throttle the downloading/parsing of the images, and the Concat will collect the images in the original order.

    public IObservable<ImageSource> GetImages(IEnumerable<string> urls, int maxConcurrency)
    {
        return Observable.Using(() => new SemaphoreSlim(maxConcurrency), semaphore =>
            urls
                .ToObservable()
                .Select(url => Observable.StartAsync(async cancellationToken =>
                {
                    await semaphore.WaitAsync(cancellationToken);
                    try
                    {
                        var bytes = await this.DownloadImage(url);
                        var image = await this.ParseImage(bytes);
                        return image;
                    }
                    finally { semaphore.Release(); }
                }))
                .Concat());
    }
    

    You could consider passing the cancellationToken argument to the DownloadImage and ParseImage methods, in order to avoid having fire-and-forget operations running in the background, in case that the resulting IObservable<ImageSource> terminates prematurely for any reason (error or unsubscription).

    0 讨论(0)
提交回复
热议问题