Multithreaded User Interfaces

来源:互联网 发布:计算机教学数据 编辑:程序博客网 时间:2024/05/17 03:20
 

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.

 

原创粉丝点击