【问题标题】:What is the simplest way to do background tasks in Windows.Forms?在 Windows.Forms 中执行后台任务的最简单方法是什么?
【发布时间】:2009-10-06 10:46:43
【问题描述】:

后台任务是涉及网络 I/O、磁盘 I/O 或其他可能会或可能不会通过网络发生的长时间运行的任务。它通常会与更新 GUI 的代码混在一起,这些代码需要在另一个线程上运行,即 GUI 线程。

简单意味着当打开一个 Form.cs 文件时,源代码与以前一样容易或更容易阅读。在实践中,源代码流仍然必须按照代码执行的顺序顺序读取,而不管它在哪个线程上执行。所有支撑结构都必须可重复使用并隐藏在某个地方,而不是包含在每个表单中。

谷歌搜索 MSDN:发现微软官方认可的解决方案是 System.ComponentModel.BackgroundWorker,它在第二点上(非常!)不足。

(在 System.Windows.Threading.Dispatcher 中还有一个官方认可的 Silverlight/XAML/3.5 解决方案模型。)

【问题讨论】:

  • 传统上编写线程代码是一项复杂的任务。就我个人而言,我发现后台工作人员的方式非常简单,与早期的 .Net 日子相比,您必须自定义滚动跨线程操作委托和回调。你能详细说明为什么它没有达到第二点吗?
  • 肯定比早期的 dotnet 简单,同意。我在一个团队中工作,我们需要有一个标准的设计模式来做到这一点,我们需要有非常易读的代码才能使协作取得成功。是否足够详细,或者?
  • 你因为可读性而打折Bgw?
  • 可读性和使用它所需的工作量。请记住,工作量增加了 500 倍,因为这是一个拥有几个大型软件项目的团队,并且需要为每个项目一次又一次地完成这项工作......跨度>

标签: .net winforms multithreading design-patterns


【解决方案1】:

如果你真的不喜欢 BackgroundWorker,你可以为后台操作创建自己的基类,就像我在 here 所做的那样。

【讨论】:

  • 不错的接口实现!
  • 真是太好了!
    它有一些与我需要的相反的细节: - 它对进度报告的完成方式施加了严格的 API。我宁愿让开发人员来决定哪些 UI 部分需要更新等。
    - 对于每个需要背景的“事物”,它仍然需要太多代码。它必须与单线程一样(或几乎一样)容易在后台执行,否则人们不会这样做。
    但您的方法也有主要优点,例如插入它有多容易进度表。不错。
  • 我真的看不出这个接口提供了什么BackgroundWorker 没有提供的(除了需要UI代码检查InvokeRequired,因为事件是在工作线程上引发的 /i> 而不是像BackgroundWorker 那样在创建worker的线程上引发)?
  • 在您的示例中,如果您不喜欢 BackgroundWorker,您应该使用 AsyncOperation 实例(在 UI 线程中创建)来引发 UI 线程上的事件。所以 UI 代码不必检查 InvokeRequired。
  • 当然,它并不完美,如果我再做一次,我会做的不同。我可能还会使用 SynchronizationContext.Current 来避免让我的 BackgroundActions 依赖于 WinForms。 InvokeRequired 在这个特定的应用程序中进入了我的“ProgressForm”,因此不再重复 UI 代码。后台工作者的不利影响是鼓励人们在他们的代码中编写后台操作,而抽象基类会迫使人们创建一个新类。
【解决方案2】:

为此使用BackgroundWorker

【讨论】:

    【解决方案3】:

    您仍然可以使用BackgroundWorker。它不需要作为表单上的组件存在。您可以轻松地将其包装到一个类中,然后在每个表单中重复使用。

    但是,这与在需要时简单地为后台任务设置工作人员略有不同。

    【讨论】:

    • @rosenfield:如果您将处理后台任务包装到一个单独的类中,您仍然需要创建一个实例、启动任务并以某种方式处理任何结果。这几乎就是 BackgroundWorker 提供的。
    • @fredrik:BGW 正是在这种情况下令人讨厌,因为它迫使您在每次有另一个任务需要后台处理时创建一个新的 BGW 实例并编写新的 BGW 事件处理程序,实际上污染了每个事件需要对具有大量设置等的任务进行后台处理的处理程序。
    【解决方案4】:

    你能解释一下为什么你说BackgroundWorker不够用吗?

    在大多数情况下,它需要 2-3 行额外的代码。

    【讨论】:

    • 是的(希望如此):这不仅是需要多少额外代码行的问题,而且 BackgroundWorker 的问题在于它使原始单线程代码不可读。 BackgroundWorker 迫使您将逻辑和连贯的代码划分为一堆事件处理程序和不同的方法。
    • BG-worker 从组件库继承,因为它知道与它的所有者控制同步,以进行跨线程安全调用。这就是为什么它“强迫”你进入事件处理程序方法。如果您不需要线程安全,您始终可以使用 threading.threadpool 类实现自己的线程类
    • @kb:关于“知道同步”,它是否在做其他事情/比 Invoke() 等更聪明?
    【解决方案5】:

    这是迄今为止我想出的最简单的想法。它可能完全不符合犹太教规,我编写的 Windows.Forms 应用程序几乎为零。

    它涉及一个助手,它有两个主要方法,Background() 和 Foreground()。其中任何一个都采用以 lambda 表达式形式指定的委托。 Background() 在后台线程上启动任何给定的委托并立即返回。 Foreground() 使用 Form.BeginInvoke() 将任何给定的委托“返回”到 GUI 线程并立即返回。

    下面是如何使用此设计模式的代码示例,前提是已经实现了帮助器。

    public class Form1 : Form {
        protected ProgressBar progressBar1;
        protected Button button1;
    
        protected BackgroundHelper helper = new BackgroundHelper();
    
        public void button1_Click(...) {
            // Execute code in the background.
            helper.Background(() => {
                for (int i = 0; i <= 100; i++) {
                    // Continually report progress to user.
                    helper.Foreground<int>(i, j => {
                        progressBar1.Value = j;
                    });
                    // Simulate doing I/O or whatever.
                    Thread.Sleep(25);
                }
            });
        }
    }
    

    这使代码保持整齐的顺序,在合适的位置提供共享变量,并允许跨两个线程的循环。

    为了阐明助手的作用,

    • 构造函数启动一个等待队列的后台线程。
    • Background() 和 Foreground() 都立即返回。
    • Background() 使用内部队列将代码排入队列以在后台线程中运行。
    • Foreground() 执行相同的操作,BeginInvoke 在首先创建帮助程序的 GUI 线程上。

    编辑:实施:
    http://code.google.com/p/backgrounder/

    【讨论】:

      【解决方案6】:

      例如,在您提出磁盘 I/O 问题之后,我最近(或多年来第 100 次)编写了一个异步文件扫描器类。只需实例化一个新实例并连接到事件中,它就可以按照您的意愿重复使用。

      我将功能封装在一个实现跨线程安全的类中。该类触发事件以通知调用者更新以及何时完成。

      您说“源代码仍然必须按照代码执行的顺序顺序读取”,但要意识到线程彼此并行运行。这就是分离代码结构的好处。

      这演示了将可重用代码隔离到一个单独的类中...

      Public Class FileScanner
          Public Event Scan_Complete(sender As Object)
          Public Event Scan_Update(sender As Object, filename As String)
          Public Event Scan_Error(sender As Object, ex As Exception)
          Private Delegate Sub del_ScanComplete()
      
          Sub New(syncObject As Control, path String)
              Me.SynchronizeObject = syncObject
              ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf ScanFilesAsync), path)
          End Sub
      
          Private Sub ScanFilesAsync(ByVal state As Object)
      
              ' scan files here
      
              ' call the method to raise the Complete event
              ScanComplete()
      
          End Sub
      
          Private Sub ScanComplete()
      
              If SynchronizeObject.InvokeRequired Then
                  ' we cant raise event on a different thread than our caller
                  ' so lets invoke our method on the same caller thread
                  Dim d As New del_ScanComplete(AddressOf ScanComplete)
                  SynchronizeObject.Invoke(d)
              Else
                  ' no synchronize needed, tell the caller we are done
                  RaiseEvent Complete(Me)
              End If
      
          End Sub
      
      End Class
      

      【讨论】:

      • 嗯,关于“但要意识到线程彼此并行运行。这就是为什么分离代码结构很好。”,我不同意。它不是完全并行的,因为 Invoke() 执行 PostMessage() 并且将代码序列化到这样的程度,以至于拆分只是完成了使阅读正在发生的事情变得更加困难。
      • 代码示例用于特定的后台操作(扫描文件),我正在寻找可即插即用的可重复使用的东西。
      【解决方案7】:

      在 Windows 窗体中执行后台任务并在完成后引发事件的另一种方法是使用 AsyncOperationManagerAsyncOperation 类。需要注意的是,必须在 UI 线程中创建 AsyncOperation 才能正常工作。否则 UI 代码必须检查 InvokeRequired。

      public class BackgroundTask{
      
        private AsyncOperation _asyncOperation;
        public EventHandler Done;
      
        public BackgroundTask(){
          _asyncOperation = AsyncOperationManager.CreateOperation();
        }
      
        public void DoAsync(object userState) {
          System.Threading.ThreadPool.QueueUserWorkItem( ExecuteDo, userState);
        }
      
        private void ExecuteDo(object state) {
          // Do your work here
          // Raise event after finish
          _asyncOperation.PostOperationCompleted( Finished, EventArgs.Empty );
        }
      
        private void LookUpFinished( object eventargs ) {
          OnDone( ( EventArgs) eventargs );
        }
      
        private void OnDone( LookUpEventArgs e ) {
          EventHandler localEvent = Done;
          if ( localEvent!= null ) {
            localEvent(this,e);
          } 
        }
      }
      

      这是如何使用它的代码:

      public class MyForm : Form {
      
        public MyForm() {
          InitializeComponent();
        }
      
        protected override OnShown(EventArgs e) {
          BackgroundTask task = new BackgroundTask();
          task.Done += SignalTaskDone;
        }
      
        private void SignalTaskDone(object sender, EventArgs e){
          MessageBox.Show(this, "Task finished");
        }
      }
      

      【讨论】:

      • 在实际使用中,你仍然会使用上面的代码将任何代码分成小块,因为这基本上是一个基于回调/事件的异步设计模式,就像 BGW 一样。 (不幸的是,因此同样不合适。)
      【解决方案8】:

      正如 here 颇有说服力的论证,微软已经看到了 BackgroundWorker 中的缺陷,您可以使用带有 async 关键字的 Task.Run 获得更好、更清晰的代码。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-09-25
        • 2011-01-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-09-04
        • 1970-01-01
        相关资源
        最近更新 更多