.
Anmeldung | Registrieren | Hilfe | Posteingang
Suchen
Home Foren News Member Offers Termine Developer Blogs Knowledge Base

Navigation

Navigationslinks überspringen.
Knowledge Base reduzierenKnowledge Base
Tutorials reduzierenTutorials
Webentwicklung
Cliententwicklung
Datenbankentwicklung
IT Professional
Sharepoint
Sprachspezifisch reduzierenSprachspezifisch
C#
Visual Basic
C++
XAML
SQL
JavaScript
Erfahrungsberichte reduzierenErfahrungsberichte
Entwicklersoftware
Bücher
FAQ Grundlagen

Verknüpfungen

  • Knowledge Base durchsuchen
  • Hilfe zur Knowledge Base
  • RSS Feed
  • Twitter

Controls aus einem anderen Thread setzen (SynchronizationContext, CancellationToken, Bonus: Tooltips)

Version 2 vom 7.9.2010
Änderungen:

  • Die Übergabe der Daten an die Controls findet jetzt in der Form1-Klasse statt (SRP).
  • Eine Referenz der Form1-Klasse wird nicht mehr an die Workerklasse übergeben –> geringere Kopplung und Wegfall der Referenzproperties für die involvierten Controls.
  • Das Messaging zwischen den Klassen wird über Events abgewickelt.
  • Extra Methoden für das Handling des SynchronizationContext wurden entfernt und durch Lambdas ersetzt –> schlankerer Code.

Dieser KB-Artikel befasst sich mit der Frage, “wie behandelt man Windows-Forms Controls aus einem anderen Thread heraus”.

Jeder Entwickler hat wohl schon folgende Exception gesehen:

System.InvalidOperationException was unhandled by user code
  Message=Cross-thread operation not valid: Control 'Form1' accessed from a thread other than the thread it was created on.

Es gibt verschiedene Wege für die Lösung des Problems; ich möchte hier den SynchronizationContext demonstrieren. Dazu habe ich ein kleines Demoprogramm geschrieben, das sowohl einen Thread als auch einen Task benutzt.

SyncContextDemo

Form1.cs

Erschrecken sie jetzt nicht bei der Menge Code! Das meiste dient nur den Tooltips, die sie erhalten wenn sie mit der Maus über eines der Controls fahren. Die gesamte Methode InitToolTips können sie auch auslassen. Allerdings verpassen sie damit nützliche Hintergrundinformationen.

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

namespace UsingSynchronizationContext
{
   public partial class Form1 : Form
   {
      private CancellationTokenSource cancellationTokenSource;
      private readonly WorkerClass worker;
      private readonly SynchronizationContext syncContext;

      public Form1()
      {
         InitializeComponent();
         FormBorderStyle = FormBorderStyle.Fixed3D;
         MaximizeBox = false;
         SizeGripStyle = SizeGripStyle.Hide;
         worker = new WorkerClass();
         worker.FlashThreadEvent += ProcessFlashThreadEvent;
         worker.FlashTaskEvent += ProcessFlashTaskEvent;
         cancellationTokenSource = new CancellationTokenSource();
         syncContext = SynchronizationContext.Current;

         InitToolTips();
         SetInformationLabel();
      }

      private void SetInformationLabel()
      {
         InformationLabel.Text = "Hinweis: Fahre mit der Maus über die"
                                 + Environment.NewLine
                                 + " Buttons, um Informationen zu erhalten.";
      }

      private void InitToolTips()
      {
         var buttonThreadStartToolTip = new ToolTip
                                           {
                                              ToolTipIcon = ToolTipIcon.Info,
                                              ToolTipTitle =
                                              "Start-Button für Demo-Thread",
                                              UseAnimation = true,
                                              IsBalloon = true
                                           };
         buttonThreadStartToolTip.SetToolTip(ThreadStartButton,
            "Mehrmaliges klicken bewirkt nichts,"
            + Environment.NewLine
            + "weil der Hintergrundthread gelockt ist."
            + Environment.NewLine
            + "Kommentiere den Lock aus,"
            + Environment.NewLine
            + "um einen interessanten Effekt zu sehen.");

         //====================================================================

         var buttonThreadStopToolTip = new ToolTip
                                          {
                                             ToolTipIcon = ToolTipIcon.Info,
                                             ToolTipTitle =
                                             "Stop-Button für Demo-Thread.",
                                             UseAnimation = true,
                                             IsBalloon = true
                                          };
         buttonThreadStopToolTip.SetToolTip(ThreadStopButton,
                                            "Der Thread wird nicht über"
                                            + " Thread.Abort() beendet,"
                                            + Environment.NewLine
                                            + "sondern über die boolsche"
                                            + " Variable \"stopThread\".");

         //====================================================================

         var buttonTaskStartToolTip = new ToolTip
                                         {
                                            ToolTipIcon = ToolTipIcon.Info,
                                            ToolTipTitle =
                                            "Start-Button für Demo-Task.",
                                            UseAnimation = true,
                                            IsBalloon = true
                                         };
         buttonTaskStartToolTip.SetToolTip(TaskStartButton,
            "Mehrmaliges klicken bewirkt nichts,"
            + Environment.NewLine
            + "weil das bool Property 'TaskIsRunning' ein queueing verhindert.");
      }

      private void ThreadStartButtonClick(object sender, EventArgs e)
      {
         worker.StartThread();
      }

      private void ThreadStopButtonClick(object sender, EventArgs e)
      {
         worker.StopThread();
      }

      private void TaskStartButtonClick(object sender, EventArgs e)
      {
         RenewCancellationTokenSourceAfterItsCanceled();
         worker.StartTask(cancellationTokenSource.Token);
      }

      private void TaskStopButtonClick(object sender, EventArgs e)
      {
         worker.StopTask(cancellationTokenSource);
      }

      /// <summary>
      /// Wenn ein CancellationTokenSource einmal auf Cancel gesetzt wurde,
      /// muß eine neue Instanz gebildet werden,
      /// um diesen Zustand wieder zu ändern.
      /// </summary>
      private void RenewCancellationTokenSourceAfterItsCanceled()
      {
         if(cancellationTokenSource.Token.IsCancellationRequested)
            cancellationTokenSource = new CancellationTokenSource();
      }

      private void ProcessFlashThreadEvent(Color color)
      {
         syncContext.Post(state => ThreadLed.BackColor = (Color)state, color);
      }

      private void ProcessFlashTaskEvent(Color color)
      {
         syncContext.Post(state => TaskLed.BackColor = (Color)state, color);
      }
   }
}

Die Post-Methode des SynchronizationContext sorgt für eine asynchrone Ausführung.

Da natürlich auch hier das SRP berücksichtigt wird, stehen der Thread und der Task in einer eigenen Klasse:

WorkerClass.cs

using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System;

namespace UsingSynchronizationContext
{
internal class WorkerClass
{
internal event Action<Color> FlashThreadEvent;
internal event Action<Color> FlashTaskEvent;

/* Note: Hier sind zwei verschiedene SyncLocks nötig,
* denn ein SyncLock gilt für die gesamte Klasse.
* Das heißt, wird irgendwo in der Klasse ein Lock gesetzt,
* gilt das nicht nur für die Methode, in der das passiert ist,
* sondern auch für alle anderen Methoden in dieser Klasse.
* Würde z.B. LedFlashThread gestartet, könnte LedFlashtask nicht mehr
* gestartet werden, solange der Lock nicht freigegeben,
* d.h. LedFlashThread nicht beendet ist. */
private static readonly Object ThreadSyncLock = new Object();
private static readonly Object TaskSyncLock = new Object();
private volatile bool stopThread;

internal bool TaskIsRunning { get; private set; }

internal void StartThread()
{
stopThread = false;
new Thread(LedFlashThread).Start();
}

internal void StopThread()
{
stopThread = true;
}

internal void StartTask(CancellationToken cancellationToken)
{
if(!TaskIsRunning)
Task.Factory.StartNew(LedFlashTask, cancellationToken);
}

internal void StopTask(CancellationTokenSource cts)
{
cts.Cancel();
}

private void LedFlashThread(object state)
{
lock(ThreadSyncLock)
{
while(!stopThread)
{
if(FlashThreadEvent != null)
{
FlashThreadEvent(Color.Blue);
Thread.Sleep(500);
FlashThreadEvent(Color.YellowGreen);
Thread.Sleep(500);
}
}
}
}

/* Note: Um zuzulassen daß mehrere Tasks gequeued werden, die Zeilen,
* die TaskIsRunning setzen, auskommentieren. */
private void LedFlashTask(object state)
{
lock(TaskSyncLock)
{
TaskIsRunning = true;
var cancellationToken = (CancellationToken)state;
while(!cancellationToken.IsCancellationRequested)
{
if(FlashTaskEvent != null)
{
FlashTaskEvent(Color.Blue);
Thread.Sleep(500);
FlashTaskEvent(Color.YellowGreen);
Thread.Sleep(500);

}
}
TaskIsRunning = false;
}
}
}
}

Der Vollständigkeit halber hier noch der automatisch generierte Windows.Forms-Designer-Code:

Form1.Designer.cs

using System.Windows.Forms;
 
namespace SynchronizationContext_Versuche
{
   partial class Form1
   {
      /// <summary>
      /// Required designer variable.
      /// </summary>
      private System.ComponentModel.IContainer components = null;
 
      /// <summary>
      /// Clean up any resources being used.
      /// </summary>
      /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
      protected override void Dispose(bool disposing)
      {
         if(disposing && (components != null))
         {
            components.Dispose();
         }
         base.Dispose(disposing);
      }
 
      #region Windows Form Designer generated code
 
      /// <summary>
      /// Required method for Designer support - do not modify
      /// the contents of this method with the code editor.
      /// </summary>
      private void InitializeComponent()
      {
         this.label1 = new System.Windows.Forms.Label();
         this.ThreadPictureBox = new System.Windows.Forms.PictureBox();
         this.ThreadStartButton = new System.Windows.Forms.Button();
         this.ThreadStopButton = new System.Windows.Forms.Button();
         this.groupBox1 = new System.Windows.Forms.GroupBox();
         this.groupBox2 = new System.Windows.Forms.GroupBox();
         this.TaskStopButton = new System.Windows.Forms.Button();
         this.label2 = new System.Windows.Forms.Label();
         this.TaskStartButton = new System.Windows.Forms.Button();
         this.TaskPictureBox = new System.Windows.Forms.PictureBox();
         this.InformationLabel = new System.Windows.Forms.Label();
         ((System.ComponentModel.ISupportInitialize)(this.ThreadPictureBox)).BeginInit();
         this.groupBox1.SuspendLayout();
         this.groupBox2.SuspendLayout();
         ((System.ComponentModel.ISupportInitialize)(this.TaskPictureBox)).BeginInit();
         this.SuspendLayout();
         // 
         // label1
         // 
         this.label1.AutoSize = true;
         this.label1.Location = new System.Drawing.Point(5, 27);
         this.label1.Name = "label1";
         this.label1.Size = new System.Drawing.Size(255, 13);
         this.label1.TabIndex = 0;
         this.label1.Text = "Wenn diese LED blinkt, läuft der Hintergrund-Thread";
         // 
         // ThreadPictureBox
         // 
         this.ThreadPictureBox.BackColor = System.Drawing.Color.Gray;
         this.ThreadPictureBox.Location = new System.Drawing.Point(267, 27);
         this.ThreadPictureBox.Name = "ThreadPictureBox";
         this.ThreadPictureBox.Size = new System.Drawing.Size(20, 20);
         this.ThreadPictureBox.TabIndex = 1;
         this.ThreadPictureBox.TabStop = false;
         // 
         // ThreadStartButton
         // 
         this.ThreadStartButton.Location = new System.Drawing.Point(8, 61);
         this.ThreadStartButton.Name = "ThreadStartButton";
         this.ThreadStartButton.Size = new System.Drawing.Size(75, 23);
         this.ThreadStartButton.TabIndex = 2;
         this.ThreadStartButton.Text = "START";
         this.ThreadStartButton.UseVisualStyleBackColor = true;
         this.ThreadStartButton.Click += new System.EventHandler(this.ThreadStartButton_Click);
         // 
         // ThreadStopButton
         // 
         this.ThreadStopButton.Location = new System.Drawing.Point(90, 61);
         this.ThreadStopButton.Name = "ThreadStopButton";
         this.ThreadStopButton.Size = new System.Drawing.Size(75, 23);
         this.ThreadStopButton.TabIndex = 3;
         this.ThreadStopButton.Text = "STOP";
         this.ThreadStopButton.UseVisualStyleBackColor = true;
         this.ThreadStopButton.Click += new System.EventHandler(this.ThreadStopButton_Click);
         // 
         // groupBox1
         // 
         this.groupBox1.Controls.Add(this.ThreadStopButton);
         this.groupBox1.Controls.Add(this.label1);
         this.groupBox1.Controls.Add(this.ThreadStartButton);
         this.groupBox1.Controls.Add(this.ThreadPictureBox);
         this.groupBox1.Location = new System.Drawing.Point(12, 12);
         this.groupBox1.Name = "groupBox1";
         this.groupBox1.Size = new System.Drawing.Size(290, 101);
         this.groupBox1.TabIndex = 4;
         this.groupBox1.TabStop = false;
         this.groupBox1.Text = "Über Thread";
         // 
         // groupBox2
         // 
         this.groupBox2.Controls.Add(this.TaskStopButton);
         this.groupBox2.Controls.Add(this.label2);
         this.groupBox2.Controls.Add(this.TaskStartButton);
         this.groupBox2.Controls.Add(this.TaskPictureBox);
         this.groupBox2.Location = new System.Drawing.Point(12, 131);
         this.groupBox2.Name = "groupBox2";
         this.groupBox2.Size = new System.Drawing.Size(290, 101);
         this.groupBox2.TabIndex = 5;
         this.groupBox2.TabStop = false;
         this.groupBox2.Text = "Über Task";
         // 
         // TaskStopButton
         // 
         this.TaskStopButton.Location = new System.Drawing.Point(90, 61);
         this.TaskStopButton.Name = "TaskStopButton";
         this.TaskStopButton.Size = new System.Drawing.Size(75, 23);
         this.TaskStopButton.TabIndex = 3;
         this.TaskStopButton.Text = "STOP";
         this.TaskStopButton.UseVisualStyleBackColor = true;
         this.TaskStopButton.Click += new System.EventHandler(this.TaskStopButton_Click);
         // 
         // label2
         // 
         this.label2.AutoSize = true;
         this.label2.Location = new System.Drawing.Point(5, 27);
         this.label2.Name = "label2";
         this.label2.Size = new System.Drawing.Size(245, 13);
         this.label2.TabIndex = 0;
         this.label2.Text = "Wenn diese LED blinkt, läuft der Hintergrund-Task";
         // 
         // TaskStartButton
         // 
         this.TaskStartButton.Location = new System.Drawing.Point(8, 61);
         this.TaskStartButton.Name = "TaskStartButton";
         this.TaskStartButton.Size = new System.Drawing.Size(75, 23);
         this.TaskStartButton.TabIndex = 2;
         this.TaskStartButton.Text = "START";
         this.TaskStartButton.UseVisualStyleBackColor = true;
         this.TaskStartButton.Click += new System.EventHandler(this.TaskStartButton_Click);
         // 
         // TaskPictureBox
         // 
         this.TaskPictureBox.BackColor = System.Drawing.Color.Gray;
         this.TaskPictureBox.Location = new System.Drawing.Point(267, 27);
         this.TaskPictureBox.Name = "TaskPictureBox";
         this.TaskPictureBox.Size = new System.Drawing.Size(20, 20);
         this.TaskPictureBox.TabIndex = 1;
         this.TaskPictureBox.TabStop = false;
         // 
         // InformationLabel
         // 
         this.InformationLabel.AutoSize = true;
         this.InformationLabel.Location = new System.Drawing.Point(13, 237);
         this.InformationLabel.Name = "InformationLabel";
         this.InformationLabel.Size = new System.Drawing.Size(10, 13);
         this.InformationLabel.TabIndex = 6;
         this.InformationLabel.Text = ".";
         // 
         // Form1
         // 
         this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
         this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
         this.ClientSize = new System.Drawing.Size(314, 272);
         this.Controls.Add(this.InformationLabel);
         this.Controls.Add(this.groupBox2);
         this.Controls.Add(this.groupBox1);
         this.Name = "Form1";
         this.Text = "SynchronizationContext-Versuche";
         ((System.ComponentModel.ISupportInitialize)(this.ThreadPictureBox)).EndInit();
         this.groupBox1.ResumeLayout(false);
         this.groupBox1.PerformLayout();
         this.groupBox2.ResumeLayout(false);
         this.groupBox2.PerformLayout();
         ((System.ComponentModel.ISupportInitialize)(this.TaskPictureBox)).EndInit();
         this.ResumeLayout(false);
         this.PerformLayout();
 
      }
 
      #endregion
 
      private System.Windows.Forms.Label label1;
      private System.Windows.Forms.PictureBox ThreadPictureBox;
      private System.Windows.Forms.Button ThreadStartButton;
      private System.Windows.Forms.Button ThreadStopButton;
      private System.Windows.Forms.GroupBox groupBox1;
      private System.Windows.Forms.GroupBox groupBox2;
      private System.Windows.Forms.Button TaskStopButton;
      private System.Windows.Forms.Label label2;
      private System.Windows.Forms.Button TaskStartButton;
      private System.Windows.Forms.PictureBox TaskPictureBox;
      private System.Windows.Forms.Label InformationLabel;
   }
}

Anmerkung zum SynchronizationContext: Der Haupt-Thread, also der für die UI verantwortlich ist, hat schon eine Instanz des SynchronizationContext im Gepäck!Abgerufen wird sie über SynchronizationContext.Current.

Noch ein wichtiger Hinweis zum Schluß: Falls sie eine Architektur haben, in der mehrere Threads parallel auf so eine Methode wie z.B. UpdateUiFromThread zugreifen, funktioniert der SynchronizationContext nicht mehr! Der Grund: Sie müßten in diese Methode einen Lock einbauen. Nur der Thread, der als erstes darauf zugreift, kommt durch. Alle anderen Threads würden blockiert - und schon ist es vorbei mit der Asynchronizität! Ich hatte gerade das Problem als ich ein UI-Control (Fortschrittsbalken) aus einer Parallel.For-Schleife heraus aktualisieren wollte. In so einem Fall ist die Invoke-Geschichte besser geeignet.

von Rainer Hilmer, 29.08.2010 zugeordnet zu Cliententwicklung , Tutorials .

Kommentare

Es sind noch keine Kommentare vorhanden.

Eigener Kommentar

Sie müssen angemeldet sein, um ein Kommentar zu erstellen.
  • Schwierigkeit: Fortgeschrittene
  • Views: 1746
  • Zur Druckversion
  • Artikel von Rainer Hilmer

Kick it on dotnet-kicks.de

Artikel

Autor

Kick it!

Wenn ihnen dieser Artikel gefällt, bitte "kicken" sie ihn.

WPF Forum | ASP.NET Forum | ASP.NET MVC Forum | Silverlight Forum | Windows Phone 7 Forum | SharePoint Forum | Dotnet Jobs | Dotnet Termine | Developer Blogs | Dotnet News

Das Team | Regeln | Impressum