代码之家  ›  专栏  ›  技术社区  ›  Vilx-

对于这个多线程问题,采用哪种方法?

  •  2
  • Vilx-  · 技术社区  · 15 年前

    简短问题 :

    我想生成一个单独的后台线程来处理提交到队列的工作项(比如一个线程池和一个线程)。有些工作项能够报告进度,有些则不能。我应该使用.NET无数的多线程方法中的哪一种?


    冗长的解释 (避免询问 half which doesn't make any sense ):

    我的WinForms应用程序的主窗口垂直拆分为两部分。左半部分包含带有项的TreeView。当用户双击TreeView中的项目时,该项目将在右半部分打开。几乎所有对象都有很多属性,分为几个部分(由选项卡表示)。加载这些属性需要相当长的时间,通常大约10秒,有时更多。而且每隔一段时间会添加更多的属性,所以时间会增加。

    目前,我的单线程设计使得这次用户界面没有响应。当然,这是不可取的。我想在后台一部分一部分地加载东西,一旦加载了一部分,就可以使用它。对于其他部分,我将显示带有加载动画或其他内容的占位符选项卡。此外,虽然有些部分是在一个冗长的单块操作中加载的,而其他部分则由许多较小的函数调用和计算组成,因此可以显示加载进度。对于这些部分,很高兴看到进展(特别是如果它们挂在某个地方,就会发生这种情况)。

    请注意,数据源不是线程安全的,因此我不能同时加载两个部分。

    什么方法最适合实现这种行为?有没有什么.NET课程能让我卸下肩上的工作,或者我应该放下身上的脏东西 Thread ?

    ThreadPool 执行工作项队列管理,但没有用于进度报告的工具。 BackgroundWorker 另一方面,它支持进度报告,但只针对单个工作项。可能两者都有吗?

    3 回复  |  直到 15 年前
        1
  •  2
  •   Stephen Cleary    15 年前

    .NET 4.0通过引入 Task 类型,表示单个可能的异步操作。

    对于您的场景,我建议将每个属性(或属性组)的加载拆分为单独的任务。任务包括“父”的概念,因此每个对象的加载可能是拥有属性加载任务的父任务。

    要处理取消,请使用新的统一取消框架。创建 CancellationTokenSource 为每个对象传递 CancellationToken 到父任务(将其传递到其每个子任务)。这允许取消一个对象,该对象可以在当前加载属性完成后生效(而不是等待整个对象完成)。

    为了处理并发性(或者更恰当地说, -并发),使用 OrderedTaskScheduler 来自 ParallelExtensionsExtras sample library . 每个 任务 仅表示需要计划的工作单元,并使用 订单任务时间表 ,可以确保连续执行(在线程池线程上)。

    用户界面进度更新可以通过创建用户界面更新来完成。 任务 并将其调度到UI线程。我有一个例子 on my blog ,在这里我将一些更尴尬的方法包装成 ProgressReporter 帮助程序类型。

    关于 任务 类型是它以一种自然的方式支持异常和取消;这些通常是设计一个系统来处理像您这样的问题的更困难的部分。

        2
  •  1
  •   Fredrik Leijon    15 年前

    使用线程,将工作放到线程安全集合中,并在更新UI以在正确的线程中执行时使用Invoke

        3
  •  1
  •   Scott Langham    15 年前

    听起来很棘手!

    你说你的数据源不是线程安全的。那么,这对用户意味着什么呢?如果他们到处点击,但在点击其他地方之前不要等待属性加载,他们可以点击10个需要很长时间加载的节点,然后坐在第10个节点上等待。加载必须一个接一个地运行,因为数据源访问不是线程安全的。这表明线程池不是一个好的选择,因为它将并行运行负载并破坏线程安全。如果可以中途中止加载,以防止用户在开始加载希望看到的页面之前必须等待最后9个节点加载,那将是一件好事。

    如果负载可以中止,我建议一个后台工作人员最好。如果用户切换了节点,而backgroundworker已经很忙,则设置一个事件或某个事件来表示它应该中止现有的工作,然后将新工作排队以加载当前页面。

    另外,考虑一下,让线程池中运行的线程报告进度并不太困难。为此,请将进度对象传递给类似以下类型的QueueUserWorkItem调用:

    class Progress
    {
      object _lock = new Object();
      int _current;
      bool _abort;
    
      public int Current
      {
        get { lock(_lock) { return _current; } }
        set { lock(_lock) { _current = value; } }
      }
    
      public bool Abort
      {
        get { lock(_lock) { return _abort; } }
        set { lock(_lock) { _abort = value; } }
      }
    }
    

    线程可以对此进行写入,并且UI线程可以轮询(从System.Windows.Forms.Timer事件)以读取进度并更新进度条或动画。

    另外,如果您包含一个中止属性。如果用户更改节点,用户界面可以设置它。加载方法可以在其操作的各个点检查中止值,如果设置了中止值,则返回而不完成加载。

    说实话,你所选择的并不重要。这三个选项都可以在后台线程上完成任务。如果我是你,我会从后台工作人员开始,因为它的设置非常简单,如果你决定你需要更多的东西,考虑切换到线程池或普通线程之后。

    BackgroundWorker还具有这样的优点:您可以使用它的Completed事件(在主UI线程上执行)用加载的数据更新UI。

    推荐文章