Rx - 消耗新线程上的每个项目

时间:2017-09-05 12:39:58

标签: c# multithreading system.reactive

我们假设我有这样的代码:

static void Main(string[] args)
    {
        var scheduler = NewThreadScheduler.Default;
        var enumerable = Enumerable.Range(0, 100);

        enumerable
            .ToObservable(scheduler)
            .SubscribeOn(scheduler)
            .Subscribe(item =>
            {
                Console.WriteLine("Consuming {0} on Thread: {1}", item, Thread.CurrentThread.ManagedThreadId);

                // simulate long running operation
                Thread.Sleep(1000);
            });

        Console.ReadKey();
    }

当我将IEnumerable转换为IObservable时。然后我想在新线程上使用每个项目,所以我使用了SubsribeOn(调度程序)。不幸的是,每次迭代都在同一个线程上工作,因此下一次迭代会阻塞。

结果是:

Consuming 0 on Thread: 4
Consuming 1 on Thread: 4
Consuming 2 on Thread: 4
Consuming 3 on Thread: 4
Consuming 4 on Thread: 4
....

是否有可能强迫这种行为?

1 个答案:

答案 0 :(得分:4)

您所看到的行为完全是设计上的。

Rx的基础是它的语法,它声明流被定义为零个或多个OnNext调用的序列,后跟可选的OnErrorOnCompleted

特别是,Rx语法规定这些消息中的每一个都是按顺序为给定订阅者传递的。

所以你看到的是正确的行为 - 没有OnNext处理程序的并发执行。鉴于这种刻意的约束,为每个OnNext创建一个新线程将是非常浪费的。

在幕后,如果您跟踪代码的距离足够远,您会看到NewThreadScheduler专门使用EventLoopScheduler来重新使用每个订阅者的线程。绰号NewThreadScheduler确实说明每个订阅者获得新线程的事实,而不是每个事件。

要看到这一点,请修改您的代码,以便我们有两个订阅者以不同的速度运行。你会看到每个人都有自己的线程,并以自己的速度继续前进,速度越快,速度就越慢:

var scheduler = NewThreadScheduler.Default;
var enumerable = Enumerable.Range(0, 100);

var xs = enumerable
    .ToObservable(scheduler)
    .SubscribeOn(scheduler);

xs.Subscribe(item =>
{
    Console.WriteLine("Slow consuming {0} on Thread: {1}",
        item, Thread.CurrentThread.ManagedThreadId);

    // simulate slower long running operation
    Thread.Sleep(1000);
});

xs.Subscribe(item =>
{
    Console.WriteLine("Fast consuming {0} on Thread: {1}",
        item, Thread.CurrentThread.ManagedThreadId);

    // simulate faster long running operation
    Thread.Sleep(500);
});

Console.ReadKey();

您可能会发现Rx Design Guidelines的阅读非常有帮助。

允许在订阅者中同时处理事件的愿望表明具有多个消费者的队列可能就是您所追求的 - 并且为此您可以在Rx之外查看,例如BCL ConcurrentQueue<T>。也可以将消息投射到异步调用中,并在完成时收集结果,而不会违反Rx语法约束。

e.g。这里有一些类似的代码,可以随机处理流中的每个数字一段不同的时间。您可以看到结果不按顺序排列,并且彼此不受阻碍。这不是很棒的代码,但它说明了这一点。如果异步工作是IO绑定的话,它可能真的很有用。另请注意使用Observable.Range,以避免使用Enumerable.Range().ToObservable()组合。在.NET Core 2.0上测试:

var random = new Random();

// stop the threadpool from throttling us as it grows
ThreadPool.SetMinThreads(100, 1);

Observable.Range(0, 100)
.SelectMany(x => Observable.Start(() =>
{
    Console.WriteLine($"Started {x}");
    Thread.Sleep(random.Next(1, 10) * 1000);
    return x;
}))
.Subscribe(item =>
{
    Console.WriteLine($"{item}, {Thread.CurrentThread.ManagedThreadId}");
});

Console.ReadKey();