Multithreaded User Interfaces

本文介绍如何在WinForms应用程序中使用BackgroundWorker组件实现多线程操作,包括启动后台线程进行长时间运行的任务、报告进度更新、完成及取消任务等关键步骤。
 

1.       Issues of a single-threaded application:

  •   Cannot response to UI requests in client area until long-running working function is returned.
    • User cannot see the progress until long-running working function is returned.

2.       To avoid these issues, this application needs a way to free the UI thread to do UI work and handle the long-running function in the background. For this, it needs another thread of execution. [Asynchronous Operations] Use the BackgroundWorker component from the System.ComponentModel namespace.

3.       How to implement safe, asynchronous, long-running operations with progress reports in WinForm[Multi-threaded application]:

Tips:

a)       Direct manipulation of controls from the worker thread is forbidden, which means UI controls cannot call from worker thread. we can pass object argument or use shared data.

b)        

BackgroundWorker Work Flow

 

    •    Initiating a Worker Thread:

calling BackgroundWorker's RunWorkerAsync method:

void calcButton_Click(object sender, EventArgs e) {

              ...

     // Initiate asynchronous pi calculation on worker thread         this.backgroundWorker.RunWorkerAsync(
             (int)this.decimalPlacesNumericUpDown.Value);

 

    •   Executing from the Worker Thread:

DoWork is BackgroundWorker's default event which let you handle to process your long-running operation on a worker thread from the thread pool.

        System.ComponentModel.BackgroundWorker backgroundWorker;

         ...

         void InitializeComponent() {

              ...

              this.backgroundWorker = new System.ComponentModel.BackgroundWorker();

               ...

              // backgroundWorker

              this.backgroundWorker.DoWork += this.backgroundWorker_DoWork;

              ...

              }

          // AsyncCalcPiForm.cs

          partial class AsyncCalcPiForm : Form {

         ...

         // Executed on a worker thread from the thread pool

          void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {

              ...

   }

}

How to pass the object from UI to worker thread

  partial class AsyncCalcPiForm : Form {

  ...

  void calcButton_Click(object sender, EventArgs e) {

    ...

    // Begin calculating pi asynchronously

    this.backgroundWorker.RunWorkerAsync(

       (int)this.decimalPlacesNumericUpDown.Value);

  }

 

  void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {

    CalcPi((int)e.Argument);

  }

 

    • Reporting Progress

Reporting progress from the worker thread back to the UI thread.

1st, set WorkerReportsProgress property to true.

2nd, to report progress from the worker thread, you call the BackgroundWorker object's ReportProgress method. To pass an object from worker thread to UI…

class AsyncCalcPiForm : Form {

   ...

   class CalcPiUserState {

     public readonly string Pi;

     public readonly int TotalDigits;

     public readonly int DigitsSoFar;

 

    public CalcPiUserState(

       string pi, int totalDigits, int digitsSoFar) {

       this.Pi = pi;  

       this.TotalDigits = totalDigits;

       this.DigitsSoFar = digitsSoFar;

     }

   }

 

   void CalcPi(int digits) {

     StringBuilder pi = new StringBuilder("3", digits + 2);

 

    // Report initial progress.  use  obeject  argument, not shared data

    this.backgroundWorker.ReportProgress(0,

       new CalcPiUserState(pi.ToString(), digits, 0));

 

    if( digits > 0 ) {

      pi.Append(".");

 

      for( int i = 0; i < digits; i += 9 ) {

        ...

 

        // Report continuing progress

        this.backgroundWorker.ReportProgress(0,

          new CalcPiUserState(pi.ToString(), digits, i + digitCount));

       }

     }

   }

}

 

3rd, UI can access them and respond accordingly by handling BackgroundWorker's ProgressChanged event.

// AsyncCalcPiForm.Designer.cs

partial class AsyncCalcPiForm {

  ...

  System.ComponentModel.BackgroundWorker backgroundWorker;

  ...

  void InitializeComponent() {

    ...

    this.backgroundWorker =

      new System.ComponentModel.BackgroundWorker();

    ...

    // backgroundWorker

    this.backgroundWorker.ProgressChanged +=

      backgroundWorker_ProgressChanged;

    ...

  }

 }

 

 // AsyncCalcPiForm.cs

 partial class AsyncCalcPiForm : Form {

   ...

   void ShowProgress(string pi, int totalDigits, int digitsSoFar) {

      ...

   }

 

   void backgroundWorker_ProgressChanged(

     object sender, ProgressChangedEventArgs e) {

 

     // Show progress

     CalcPiUserState progress = (CalcPiUserState)e.UserState;

     ShowProgress(

        progress.Pi, progress.TotalDigits, progress.DigitsSoFar);

   }

}

 

    • Completion

When a BackgroundWorker-managed worker thread completes, BackgroundWorker fires the RunWorkerCompleted event. This allows us to refactor our ShowProgress method and let RunWorkerCompleted reset the status strip progress bar state:

void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {

  // Track start time

  DateTime start = DateTime.Now;

 

  CalcPi((int)e.Argument);

 

  // Return elapsed time

  DateTime end = DateTime.Now;

  TimeSpan elapsed = end - start;

  e.Result = elapsed;

}

 

 

void backgroundWorker_RunWorkerCompleted(

   object sender, RunWorkerCompletedEventArgs e) {

   // Was there an error?

   if( e.Error != null ) {

     this.resultsTextBox.Text = e.Error.Message;

     return;

  }

  ...

   

 // Show elapsed time

   TimeSpan elapsed = (TimeSpan)e.Result;

   MessageBox.Show("Elapsed: " + elapsed.ToString());

 

  // Reset progress UI

  this.calcToolStripStatusLabel.Text = "Ready";

  this.calcToolStripProgressBar.Visible = false;

 

}

 

    • Cancellation

We use the CancellationPending property of BackgroundWorker to find out whether we've already canceled the pi calculation. CancelAsync is actually only a request, so the worker thread needs to watch for it by checking the BackgroundWorker component's CancellationPending property.

void calcButton_Click(object sender, EventArgs e) {

   // Don't process if cancel request pending

   // (Should not be called, because we disabled the button...)

   if( this.backgroundWorker.CancellationPending ) return;

 

   // If worker thread currently executing, cancel it

   if( this.backgroundWorker.IsBusy ) {

     this.calcButton.Enabled = false;

     this.backgroundWorker.CancelAsync();

     return;

   }

   // Set calculating UI

   this.calcButton.Text = "Cancel";

   this.calcToolStripProgressBar.Visible = true;

   this.calcToolStripStatusLabel.Text = "Calculating...";

 

   // Begin calculating pi asynchronously

   this.backgroundWorker.RunWorkerAsync(

     (int)this.decimalPlacesNumericUpDown.Value);

}

 

void CalcPi(int digits) {

   StringBuilder pi = new StringBuilder("3", digits + 2);

 

   // Report initial progress

   this.backgroundWorker.ReportProgress(0,

     new CalcPiUserState(pi.ToString(), digits, 0));

 

   if( digits > 0 ) {

     pi.Append(".");

 

     for( int i = 0; i < digits; i += 9 ) {

       ...

       // Report continuing progress

       this.backgroundWorker.ReportProgress(0,

         new CalcPiUserState(pi.ToString(), digits, i + digitCount));

 

       // Check for cancellation

       if( this.backgroundWorker.CancellationPending ) return;

     }

   }

}

 

void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {

   ...

   CalcPi((int)e.Argument);

 

   // Indicate cancellation

   if( this.backgroundWorker.CancellationPending ) {

      e.Cancel = true;

   }

   ...

 }

 void backgroundWorker_RunWorkerCompleted(

   object sender, RunWorkerCompletedEventArgs e) {

   ...

   // Was the worker thread canceled?

   if( e.Cancelled ) {

      this.resultsTextBox.Text = "Canceled";

      return;

   }

   ...

}

 

    • Shared Data

By passing copies or ownership of data around, we ensure that no two threads need to share access to any one piece of data. We have another choice: suppose we decide that we prefer shared access to an object. For proper concurrent access to shared data, you must synchronize access to the data that is, make sure that one thread waits patiently while another thread works on the data. To synchronize access to shared data, C# provides the lock block:

Shared access to data between threads makes it very easy to get into race conditions, in which one thread is racing to read data that is only partially up-to-date before another thread has finished updating it.

 

SharedCalcPiUserState state = new SharedCalcPiUserState();

object stateLock = new object();

void CalcPi(int digits) {

  ...

  // Synchronize access to shared data

  // on the worker thread

  lock( stateLock ) {

    this.state.Pi = pi.ToString();

    this.state.TotalDigits = digits;

    this.state.DigitsSoFar = i + digitCount;

    this.backgroundWorker.ReportProgress(0);

  }

  ...

}

 

void backgroundWorker_ProgressChanged(

  object sender, ProgressChangedEventArgs e) {

 

  // Synchronize access to shared data

  // on the UI thread

  lock( stateLock ) {

     ShowProgress(

        this.state.Pi, this.state.TotalDigits, this.state.DigitsSoFar);

  }

}

Now that your data has been properly protected against race conditions, you must watch out for another problem known as a deadlock. A deadlock occurs when each of two threads has locked a resource and both subsequently wait for the resource held by the other thread, causing each thread to stop dead, waiting forever. When two threads are deadlocked, each of them waits for the other to complete its work before continuing, thereby ensuring that neither actually progresses.

Multithreaded programming with shared data is hard. By passing copies or ownership of data around, we ensure that no two threads need to share access to any one piece of data. If you don't have shared data, there's no need to synchronize access to it. But if you find that you need access to shared datamaybe because the overhead of copying the data is too great a burden in space or timethen you need to read up on multithreading and shared data synchronization, topics that are beyond the scope of this book.

 

本项目采用C++编程语言结合ROS框架构建了完整的双机械臂控制系统,实现了Gazebo仿真环境下的协同运动模拟,并完成了两台实体UR10工业机器人的联动控制。该毕业设计在答辩环节获得98分的优异成绩,所有程序代码均通过系统性调试验证,保证可直接部署运行。 系统架构包含三个核心模块:基于ROS通信架构的双臂协调控制器、Gazebo物理引擎下的动力学仿真环境、以及真实UR10机器人的硬件接口层。在仿真验证阶段,开发了双臂碰撞检测算法和轨迹规划模块,通过ROS控制包实现了末端执行器的同步轨迹跟踪。硬件集成方面,建立了基于TCP/IP协议的实时通信链路,解决了双机数据同步和运动指令分发等关键技术问题。 本资源适用于自动化、机械电子、人工智能等专业方向的课程实践,可作为高年级课程设计、毕业课题的重要参考案例。系统采用模块化设计理念,控制核心与硬件接口分离架构便于功能扩展,具备工程实践能力的学习者可在现有框架基础上进行二次开发,例如集成视觉感知模块或优化运动规划算法。 项目文档详细记录了环境配置流程、参数调试方法和实验验证数据,特别说明了双机协同作业时的时序同步解决方案。所有功能模块均提供完整的API接口说明,便于使用者快速理解系统架构并进行定制化修改。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值