使用调度器

A scheduler controls when a subscription starts and when notifications are published. It consists of three components. It is first a data structure. When you schedule for tasks to be completed, they are put into the scheduler for queueing based on priority or other criteria. It also offers an execution context which denotes where the task is executed (e.g., in the thread pool, current thread, or in another app domain). Lastly, it has a clock which provides a notion of time for itself (by accessing the Now property of a scheduler). Tasks being scheduled on a particular scheduler will adhere to the time denoted by that clock only.

调度器控制订阅的启动时机和通知的发布时机。它由三个核心组件构成:
1. 数据结构:当任务被调度执行时,调度器会根据优先级或其他标准将其存入队列进行管理。
2. 执行上下文:决定任务在何处执行(例如线程池、当前线程,或其他应用程序域)。
3. 时钟机制:通过调度器的 Now 属性提供时间概念,所有调度到该调度器的任务均遵循此时钟的时间基准。

调度器确保任务在其指定的上下文和时间体系下有序执行。

Schedulers also introduce the notion of virtual time (denoted by the VirtualScheduler type), which does not correlate with real time that is used in our daily life. For example, a sequence that is specified to take 100 years to complete can be scheduled to complete in virtual time in a mere 5 minutes. This will be covered in the Testing and Debugging Observable Sequences topic.

调度器还引入了虚拟时间的概念(通过 VirtualScheduler 类型实现),这种时间与现实中使用的时钟时间无关。例如,一个需要 100 年才能完成的序列任务,在虚拟时间中可被调度为仅用 5 分钟即可完成。这一特性将在《测试与调试可观察序列》专题中详细探讨。

Scheduler Types    调度器类型

The various Scheduler types provided by Rx all implement the IScheduler interface. Each of these can be created and returned by using static properties of the Scheduler type. The ImmediateScheduler (by accessing the static Immediate property) will start the specified action immediately. The CurrentThreadScheduler (by accessing the static CurrentThread property) will schedule actions to be performed on the thread that makes the original call. The action is not executed immediately, but is placed in a queue and only executed after the current action is complete. The DispatcherScheduler (by accessing the static Dispatcher property) wills schedule actions on the current Dispatcher, which is beneficial to Silverlight developers who use Rx. Specified actions are then delegated to the Dispatcher.BeginInvoke() method in Silverlight. NewThreadScheduler (by accessing the static NewThread property) schedules actions on a new thread, and is optimal for scheduling long running or blocking actions. TaskPoolScheduler (by accessing the static TaskPool property) schedules actions on a specific Task Factory. ThreadPoolScheduler (by accessing the static ThreadPool property) schedules actions on the thread pool. Both pool schedulers are optimized for short-running actions.

Rx 提供的各类调度器均实现了 IScheduler 接口,可通过 Scheduler 类型的静态属性创建并返回。具体类型包括:

  1. ImmediateScheduler(通过 Scheduler.Immediate 静态属性获取)

    • 立即执行:指定的操作会直接启动,无延迟。

  2. CurrentThreadScheduler(通过 Scheduler.CurrentThread 静态属性获取)

    • 当前线程队列:将操作调度到发起调用的原始线程上执行。

    • 非即时执行:操作不会立即运行,而是进入队列,等待当前操作完成后按序执行

  3. DispatcherScheduler(通过 Scheduler.Dispatcher 静态属性获取)

    • 界面线程调度:在 Silverlight 等场景中,将操作调度到当前 Dispatcher 线程(UI 线程)。

    • 实现方式:底层通过 Silverlight 的 Dispatcher.BeginInvoke() 方法委托操作。

  4. NewThreadScheduler(通过 Scheduler.NewThread 静态属性获取)

    • 新线程调度:在新线程上执行操作,适合处理需长时间运行或可能阻塞线程的操作

  5. TaskPoolScheduler(通过 Scheduler.TaskPool 静态属性获取)

    • 任务池调度:使用特定的 TaskFactory 调度操作,针对短时任务优化

  6. ThreadPoolScheduler(通过 Scheduler.ThreadPool 静态属性获取)

    • 线程池调度:将操作调度到系统线程池中执行,同样针对短时任务优化

Using Schedulers    使用调度器

You may have already used schedulers in your Rx code without explicitly stating the type of schedulers to be used. This is because all Observable operators that deal with concurrency have multiple overloads. If you do not use the overload which takes a scheduler as an argument, Rx will pick a default scheduler by using the principle of least concurrency. This means that the scheduler which introduces the least amount of concurrency that satisfies the needs of the operator is chosen.  For example, for operators returning an observable with a finite and small number of messages, Rx calls Immediate.  For operators returning a potentially large or infinite number of messages, CurrentThread is called. For operators which use timers, ThreadPool is used.

你可能已在 Rx 代码中使用过调度器,却并未显式指定其类型。这是因为所有涉及并发的 Observable 操作符都提供多个重载方法。若未使用接受调度器参数的重载,Rx 将基于「最少并发原则」自动选择默认调度器。该原则的核心是:在满足操作符需求的前提下,选择引入最少并发量的调度器。

默认调度器的选择逻辑

  1. 有限且少量消息的操作符

    • 例如返回有限数据流的操作符(如 ReturnRange

    • 默认使用 Immediate 调度器(立即同步执行)。

  2. 潜在大量或无限消息的操作符

    • 例如 Interval、持续事件流操作符

    • 默认使用 CurrentThread 调度器(在当前线程队列中顺序执行)。

  3. 涉及定时器的操作符

    • 例如 TimerDelay

    • 默认使用 ThreadPool 调度器(通过线程池异步处理定时任务)。

Because Rx uses the least concurrency scheduler, you can pick a different scheduler if you want to introduce concurrency for performance purpose, or when you have a thread-affinity issue.  An example of the former is that when you do not want to block a particular thread, in this case, you should use ThreadPool.  An example of the latter is that when you want a timer to run on the UI, in this case, you should use Dispatcher. To specify a particular scheduler, you can use those operator overloads that take a scheduler, e.g., 

由于 Rx 默认采用最少并发原则的调度器,开发者可根据需求主动选择其他调度器:

  • 性能优化:若需引入并发提升效率(如避免阻塞关键线程),可选用 ThreadPool 调度器。

  • 线程亲和性:若需在特定线程执行(如 UI 线程处理定时任务),可选用 Dispatcher 调度器。

通过调用操作符的重载方法并传入调度器参数来使用指定调度器。例如:

Timer(TimeSpan.FromSeconds(10), Scheduler.DispatcherScheduler())

In the following example, the source observable sequence is producing values at a frantic pace. The default overload of the Timer operator would place OnNext messages on the ThreadPool.

在以下示例中,源可观察序列正以极高频率生成值。若使用 Timer 操作符的默认重载方法(不指定调度器),其发出的 OnNext 消息将被默认调度到线程池中处理。

Observable.Timer(Timespan.FromSeconds(0.01))
          .Subscribe(…);

This will queue up on the observer quickly. We can improve this code by using the ObserveOn operator, which allows you to specify the context that you want to use to send pushed notifications (OnNext) to observers. By default, the ObserveOn operator ensures that OnNext will be called as many times as possible on the current thread. You can use its overloads and redirect the OnNext outputs to a different context. In addition, you can use the SubscribeOn operator to return a proxy observable that delegates actions to a specific scheduler. For example, for a UI-intensive application, you can delegate all background operations to be performed on a scheduler running in the background by using SubscribeOn and passing to it a ThreadPoolScheduler. To receive notifications being pushed out and access any UI element, you can pass an instance of the DispatcherScheduler to the ObserveOn operator.

这将在观察者上快速堆积起来。我们可以通过使用`ObserveOn`操作符来改进这段代码,该操作符允许您指定希望用于向观察者发送推送通知(`OnNext`)的上下文。默认情况下,`ObserveOn`操作符会确保`OnNext`在当前线程上尽可能被多次调用。您可以使用其重载方法,将`OnNext`的输出重定向到不同的上下文。

此外,您可以使用`SubscribeOn`操作符返回一个代理可观察序列,该序列将操作委托给特定的调度器执行。例如,对于UI密集型的应用程序,通过使用`SubscribeOn`并向其传递`ThreadPoolScheduler`,您可以将所有后台操作委托给在后台运行的调度器来执行。若需要接收推送的通知并访问任何UI元素,则可以将`DispatcherScheduler`的实例传递给`ObserveOn`操作符。

The following example will schedule any OnNext notifications on the current Dispatcher, so that any value pushed out is sent on the UI thread. This is especially beneficial to Silverlight developers who use Rx.

以下示例会将所有`OnNext`通知调度到当前的`Dispatcher`,从而确保任何推送出的值都将在UI线程上发送。这对于使用Rx的Silverlight开发者尤为有益。

Observable.Timer(Timespan.FromSeconds(0.01))
          .ObserveOn(Scheduler.DispatcherScheduler)
          .Subscribe(…);

Instead of using the ObserveOn operator to change the execution context on which the observable sequence produces messages, we can create concurrency in the right place to begin with. As operators parameterize introduction of concurrency by providing a scheduler argument overload, passing the right scheduler will lead to fewer places where the ObserveOn operator has to be used. For example, we can unblock the observer and subscribe to the UI thread directly by changing the scheduler used by the source, as in the following example. In this code, by using the Timer overload which takes a scheduler, and providing the Scheduler.Dispatcher instance, all values pushed out from this observable sequence will originate on the UI thread.

我们无需使用`ObserveOn`操作符来改变可观察序列产生消息的执行上下文,而是可以在源头正确的位置创建并发。由于许多操作符通过提供接受调度器参数的重载方法,将并发的引入参数化,因此传递合适的调度器能减少需要使用`ObserveOn`操作符的场景。例如,通过修改源头使用的调度器,我们可以解除观察者的阻塞并直接订阅到UI线程,如下例所示。在这段代码中,通过使用接受调度器的`Timer`重载方法,并传入`Scheduler.Dispatcher`实例,所有从该可观察序列推送出的值都将源自UI线程。

Observable.Timer(Timespan.FromSeconds(0.01), Scheduler.DispatcherScheduler)
          .Subscribe(…);

You should also note that by using the ObserveOn operator, an action is scheduled for each message that comes through the original observable sequence. This potentially changes timing information as well as puts additional stress on the system. If you have a query that composes various observable sequences running on many different execution contexts, and you are doing filtering in the query, it is best to place ObserveOn later in the query. This is because a query will potentially filter out a lot of messages, and placing the ObserveOn operator earlier in the query would do extra work on messages that would be filtered out anyway. Calling the ObserveOn operator at the end of the query will create the least performance impact.

此外还需注意的是,通过使用`ObserveOn`操作符,系统会为原始可观察序列传递的每条消息调度一个操作。这不仅可能改变时序信息,还会给系统带来额外负担。如果您的查询组合了运行在不同执行上下文中的多个可观察序列,并且查询中进行了过滤操作,最佳实践是将`ObserveOn`放置在查询的靠后位置。这是因为查询可能会过滤掉大量消息,若过早使用`ObserveOn`操作符,会导致对那些最终被过滤的消息进行不必要的处理。在查询末尾调用`ObserveOn`操作符将对性能的影响降至最小。

Another advantage of specifying a scheduler type explicitly is that you can introduce concurrency for performance purpose, as illustrated by the following code.

显式指定调度器类型的另一优势在于,您可以为提升性能而引入并发机制,如下列代码所示。

seq.GroupBy(...)
        .Select(x=>x.ObserveOn(Scheduler.NewThread))
        .Select(x=>expensive(x))  // perform operations that are expensive on resources

 

posted @ 2025-05-25 21:22  菜鸟吊思  阅读(20)  评论(0)    收藏  举报