我有一小段代码模拟使用大对象的流(巨大的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方法的结果。在那之后,不再使用大对象了。
答案 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));
}