简短问题:
我想生成一个后台线程来处理提交到队列的工作项(例如具有一个线程的线程池)。有些工作项目能够报告进度,有些则不能。我应该使用 .NET 众多多线程方法中的哪一种?
长解释(以避免询问一半没有任何意义):
我的winforms应用程序的主窗口被垂直分成两半。左半部分包含一个包含项目的树视图。当用户双击树视图中的某个项目时,该项目将在右半部分打开。几乎所有对象都有很多属性,分为几个部分(由选项卡表示)。这些属性的加载需要相当长的时间,通常在 10 秒左右,有时甚至更长。而且每隔一段时间就会添加更多的属性,因此时间会增加。
目前我的单线程设计使得 UI 这次没有响应。这自然是不可取的。我想在后台逐部分加载内容,并且一旦加载了某个部分,就可以使用它。对于其他部分,我会显示一个带有加载动画或其他内容的占位符选项卡。此外,虽然某些部分是在单个冗长的整体操作中加载的,但其他部分则由许多较小的函数调用和计算组成,因此可以显示加载进度。对于这些部分,很高兴看到进展(特别是如果它们挂在某个地方,这种情况就会发生)。
请注意,数据源不是线程安全的,因此我无法同时加载两个部分。
什么方法最适合实现这种行为?是否有一些 .NET 类可以减轻我肩上的一些工作,或者我应该直接开始使用 Thread ?
ThreadPool 负责工作项队列管理,但没有进度报告功能。另一方面,BackgroundWorker 支持进度报告,但它适用于单个工作项。也许两者兼而有之?
Short question:
I would like to spawn a single background thread that would process work items submitted to a queue (like a threadpool with one thread). Some of the work items are capable of reporting progress, some are not. Which one of the .NET's myriad of multithreading approaches should I use?
Long explanation (to avoid asking about the half which doesn't make any sense):
The main window of my winforms application is split vertically into two halves. The left half contains a treeview with items. When the user double-clicks an item in the treeview, the item is opened on the right half. Almost all objects have a lot of properties, split into several sections (represented by tabs). The loading of these properties takes quite a lot of time, typically around 10s, sometimes more. And more properties are added every once in a while, so the time increases.
Currently my single-threaded design makes the UI non-responsive for this time. Naturally this is undesirable. I'd like to load things part-by-part in background and as soon as a part is loaded make it available for use. For other parts I would display a placeholder tab with a loading animation or something. Also, while some parts are loaded in a single lengthy monolithic operation, others consist of lots of smaller function calls and calculations, and could thus display loading progress. For these parts it would be nice to see the progress (especially if they hang somewhere, which happens).
Note that the data source is not thread-safe, so I cannot load two parts simultaneously.
What approach would be best to implement this behavior? Is there some .NET class that would lift some work off my shoulders, or should I just get down and dirty with Thread
?
A ThreadPool
does work item queue management, but there are no facilities for progress reporting. BackgroundWorker
on the other hand supports progress reports, but it's meant for a single work item. Is there perhaps a combination of both?
发布评论
评论(3)
.NET 4.0 通过引入
Task
类型(代表单个可能的异步操作)为多线程带来了许多改进。对于您的场景,我建议将每个属性(或属性组)的加载分成单独的任务。任务包括“父”的概念,因此每个对象的加载可以是拥有属性加载任务的父任务。
要处理取消,请使用新的统一取消框架。为每个对象创建一个
CancellationTokenSource
并将其CancellationToken
传递给父任务(父任务将其传递给每个子任务)。这允许取消对象,这可以在当前加载的属性完成后生效(而不是等到整个对象完成)。要处理并发(或更准确地说,非并发),请使用 ParallelExtensionsExtras 示例库。每个
Task
仅代表需要调度的一个工作单元,通过使用OrderedTaskScheduler
,您可以确保顺序执行(在 ThreadPool 线程上)。UI 进度更新可以通过创建 UI 更新任务并将其调度到 UI 线程来完成。我有一个例子 在我的博客上,我将一些更尴尬的方法包装到
ProgressReporter
辅助类型中。Task
类型的一个好处是它以自然的方式传播异常和取消;这些通常是设计一个系统来处理像您这样的问题时更困难的部分。.NET 4.0 brings a lot of improvements to multithreading by introducing the
Task
type, which represents a single possibly-asynchronous operation.For your scenario, I'd recommend splitting up the loading of each property (or property group) into separate tasks. Tasks include the notion of a "parent", so the loading of each object could be a parent task owning the property-loading tasks.
To handle cancellation, use the new unified cancellation framework. Create a
CancellationTokenSource
for each object and pass itsCancellationToken
to the parent task (which passes it to each of its child tasks). This allows an object to be cancelled, which can take effect after the currently-loading property is done (instead of waiting until the whole object is done).To handle concurrency (or more properly, non-concurrency), use the
OrderedTaskScheduler
from the ParallelExtensionsExtras sample library. EachTask
only represents a unit of work that needs to be scheduled, and by usingOrderedTaskScheduler
, you ensure sequential execution (on a ThreadPool thread).UI progress updates can be done by creating a UI update
Task
and scheduling it to the UI thread. I have an example of this on my blog, where I wrap some of the more awkward methods into aProgressReporter
helper type.One nice thing about the
Task
type is that it propogates exceptions and cancellation in a natural manner; those are often the more difficult parts of designing a system to handle a problem like yours.使用线程,将您的工作放入线程安全集合中,并在更新 ui 时使用调用以在正确的线程中执行此操作
Use a thread, drop your work in a thread safe collection and use invoke when you update your ui to do it in the right thread
听起来很棘手!
你说你的数据源不是线程安全的。那么,这对用户意味着什么。如果他们到处点击,但在点击其他地方之前不等待属性加载,他们可能会点击需要很长时间加载的 10 个节点,然后等待第 10 个节点。由于数据源访问不是线程安全的,因此加载必须一个接一个地运行。这表明线程池不是一个好的选择,因为它会并行运行负载并破坏线程安全。如果加载可以中途中止,以防止用户必须等待最后 9 个节点加载才能开始加载他们想要查看的页面,那就太好了。
如果可以中止加载,我建议最好使用BackgroundWorker。如果用户切换节点,并且BackgroundWorker已经很忙,请设置一个事件或其他东西来表明它应该中止现有工作,然后将新工作排队以加载当前页面。
另外,请考虑一下,让线程池中运行的线程报告进度并不是太棘手。为此,将进度对象传递给类型如下的 QueueUserWorkItem 调用:
线程可以写入此内容,并且 ui 线程可以轮询(从 System.Windows.Forms.Timer 事件)以读取进度并更新进度条或动画。
另外,如果您包含 Abort 属性。如果用户更改节点,ui 可以设置它。 load 方法可以在整个操作过程中的各个点检查中止值,如果已设置,则返回而不完成加载。
老实说,你选择哪个并不重要。所有三个选项都可以在后台线程上完成工作。如果我是你,我会开始使用 BackgroundWorker,因为它的设置非常简单,如果你决定需要更多东西,请考虑稍后切换到 ThreadPool 或普通线程。
BackgroundWorker 的另一个优点是您可以使用它的完成事件(在主 ui 线程上执行)来使用已加载的数据更新 ui。
Sounds tricky!
You say your data source is not thread-safe. So, what does this mean for the user. If they're clicking around all over the place, but don't wait for properties to load up before clicking somewhere else, they could click on 10 nodes which take a long time to load, and then sit waiting on the 10th one. The load's have to run one after the other as the data source access is not thread safe. This indicates a ThreadPool wouldn't be a good choice as it would run loads in parallel and break the thread safety. It would be good if a load could be aborted part way through to prevent the user having to wait for the last 9 nodes to load up before the page they want to see starts loading.
If loads can be aborted, I'd suggest a BackgroundWorker would be best. If the user switches node, and the BackgroundWorker is already busy, set an event or something to signal it should abort existing work, and then queue up the new work to load the current page.
Also, consider, it isn't too tricky to make a thread running in a thread pool report progress. To do this pass a progress object to the QueueUserWorkItem call of a type something like this:
The thread can write to this, and the ui thread can Poll (from a System.Windows.Forms.Timer event) to read the progress and update a progress bar or animation.
Also, if you include an Abort property. The ui can set it if the user changes node. The load method can at various points throughout its operation check the abort value, and if it's set, return without completing the load.
To be quite honest, which you choose doesn't fantastically matter. All three options get stuff done on a background thread. If I were you, I'd get started with the BackgroundWorker as it's pretty simple to setup, and if you decide you need something more, consider switching to the ThreadPool or plain Thread afterwards.
The BackgroundWorker also has the advantage that you can use it's completed event (which is executed on the main ui thread) to update the ui with the data that was loaded.