Reactive Extensions SelectMany包含大对象

时间:2016-07-26 22:45:55

标签: c# .net system.reactive reactive-programming

我有一小段代码模拟使用大对象的流(巨大的byte[])。对于序列中的每个项目,调用异步方法以获得一些结果。问题?实际上,它会抛出OutOfMemoryException

与LINQPad兼容的代码(C#程序):

void Main()
{
    var selectMany = Enumerable.Range(1, 100)
                   .Select(i => new LargeObject(i))
                   .ToObservable()
                   .SelectMany(o => Observable.FromAsync(() => DoSomethingAsync(o)));

    selectMany
        .Subscribe(r => Console.WriteLine(r));
}


private static async Task<int> DoSomethingAsync(LargeObject lo)
{
    await Task.Delay(10000);
    return lo.Id;
}

internal class LargeObject
{
    public int Id { get; }

    public LargeObject(int id)
    {
        this.Id = id;
    }

    public byte[] Data { get; } = new byte[10000000];
}

似乎它会同时创建所有对象。我怎么能以正确的方式做到这一点?

基本思想是调用DoSomethingAsync以获得每个对象的一些结果,这就是我使用SelectMany的原因。为了简化,我刚刚介绍了一个Task.Delay,但在现实生活中它是一个可以同时处理一些项的服务,所以我想引入一些并发机制来利用它。

请注意,理论上,在时间处理少量项目不应该填满内存。实际上,我们只需要每个“大对象”来获取DoSomethingAsync方法的结果。在那之后,不再使用大对象了。

4 个答案:

答案 0 :(得分:4)

我觉得我repeating myself。与您的上一个问题和我的上一个答案类似,您需要做的是限制同时创建bigObjects™的数量。

为此,您需要将对象创建和处理组合在一起并将其放在同一个线程池中。现在问题是,我们使用异步方法允许线程在我们的异步方法运行时执行其他操作。由于您的慢速网络调用是异步的,因此您的(快速)对象创建代码将继续过快地创建大型对象。

相反,我们可以使用Rx通过将对象创建与异步调用相结合来保持运行的并发Observable的数量,并使用.Merge(maxConcurrent)来限制并发。

作为奖励,我们还可以设置查询执行的最短时间。只需Zip即可获得最小延迟。

static void Main()
{
    var selectMany = Enumerable.Range(1, 100)
                        .ToObservable()
                        .Select(i => Observable.Defer(() => Observable.Return(new LargeObject(i)))
                            .SelectMany(o => Observable.FromAsync(() => DoSomethingAsync(o)))
                            .Zip(Observable.Timer(TimeSpan.FromMilliseconds(400)), (el, _) => el)
                        ).Merge(4);

    selectMany
        .Subscribe(r => Console.WriteLine(r));

    Console.ReadLine();
}


private static async Task<int> DoSomethingAsync(LargeObject lo)
{
    await Task.Delay(10000);
    return lo.Id;
}

internal class LargeObject
{
    public int Id { get; }

    public LargeObject(int id)
    {
        this.Id = id;
        Console.WriteLine(id + "!");
    }

    public byte[] Data { get; } = new byte[10000000];
}

答案 1 :(得分:2)

  

它似乎同时创建了所有对象。

是的,因为您正在一次创建它们。

如果我简化您的代码,我可以告诉您原因:

void Main()
{
    var selectMany =
        Enumerable
            .Range(1, 5)
            .Do(x => Console.WriteLine($"{x}!"))
            .ToObservable()
            .SelectMany(i => Observable.FromAsync(() => DoSomethingAsync(i)));

    selectMany
        .Subscribe(r => Console.WriteLine(r));
}

private static async Task<int> DoSomethingAsync(int i)
{
    await Task.Delay(1);
    return i;
}

运行它会产生:

1!
2!
3!
4!
5!
4
3
5
2
1

由于Observable.FromAsync您允许源在任何结果返回之前运行完成。换句话说,您正在快速构建所有大型对象,但会慢慢处理它们。

您应该允许Rx同步运行,但是在默认调度程序上,以便不阻止主线程。然后代码将在没有任何内存问题的情况下运行,并且您的程序将在主线程上保持响应。

以下是此代码:

var selectMany =
    Observable
        .Range(1, 100, Scheduler.Default)
        .Select(i => new LargeObject(i))
        .Select(o => DoSomethingAsync(o))
        .Select(t => t.Result);

(我已有效地将Enumerable.Range(1, 100).ToObservable()替换为Observable.Range(1, 100),因为这也有助于解决一些问题。)

我已经尝试过测试其他选项,但到目前为止,允许DoSomethingAsync异步运行的任何内容都会遇到内存不足错误。

答案 2 :(得分:1)

ConcatMap支持开箱即用。我知道这个运算符在.net中不可用,但是你可以使用Concat运算符来进行相同操作,该运算符会延迟订阅每个内部源,直到前一个完成。

答案 3 :(得分:0)

您可以通过以下方式引入时间间隔延迟:

var source = Enumerable.Range(1, 100)
   .ToObservable()
   .Zip(Observable.Interval(TimeSpan.FromSeconds(1)), (i, ts) => i)
   .Select(i => new LargeObject(i))
   .SelectMany(o => Observable.FromAsync(() => DoSomethingAsync(o)));

因此,不是一次性地拉出所有100个整数,而是立即将它们转换为LargeObject,然后在所有100个上调用DoSomethingAsync,它会逐个将整数逐滴出一秒钟。

这就是TPL + Rx解决方案的样子。毋庸置疑,它不如单独使用Rx或单独使用TPL。但是,我不认为这个问题非常适合Rx:

void Main()
{
    var source = Observable.Range(1, 100);

    const int MaxParallelism = 5;
    var transformBlock = new TransformBlock<int, int>(async i => await DoSomethingAsync(new LargeObject(i)),
        new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = MaxParallelism });
    source.Subscribe(transformBlock.AsObserver());
    var selectMany = transformBlock.AsObservable();

    selectMany
        .Subscribe(r => Console.WriteLine(r));
}