Ungültiger threadübergreifender Vorgang – .NET Fehlermeldung 2024

.NET Fehler: Ungültiger threadübergreifender Vorgang
.NET Fehler: Ungültiger threadübergreifender Vorgang – Der Zugriff auf das Steuerelement erfolgte von einem anderen Thread….

Sicherlich bist auch Du schon einmal bei Deiner VB.NET-, bzw. C#-Programmierung auf eine der häufigsten Fehlermeldungen gestoßen. Eventuell sogar mehrfach, aber keine Sorge: Du bist absolut nicht allein! Im heutigen Beitrag erkläre ich Dir, wie Diese Fehlermeldung zustande kommt, bzw. wie Du Sie verhindern kannst:

InvalidOperationException: Ungültiger threadübergreifender Vorgang
InvalidOperationException: Ungültiger threadübergreifender Vorgang

Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement textBox1 erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde.“

Was sagt diese Fehlermeldung aus, bzw. warum kommt Sie?

Was von vornherein ein wenig abrupt und erstmal völlig komisch klingt, hat im Grunde genommen, einen ganz „einfachen“ Grund. Du sagst als Herr des Codes praktisch dem Nachbarn: „Hey, geh‘ mal bei den Müllers auf’s Klo“ – denn Du bist ja der Entwickler und kannst das prinzipiell tun. Verständlicherweise wird Familie Müller sich aber wundern und sich denken: „Was zur Hölle machst Du denn hier?“.

Bezogen auf unsere Fehlermeldung, bist Du immer noch in der gleichen Position, Du sagst als höhere „Macht“, wo es lang geht. Allerdings sieht es auch hier mit den „Rechten“ ähnlich aus – Moment, ich packe mal meine ausgezeichneten Paint-Fähigkeiten aus..

Threadübergreifender Vorgang - Thread A greift auf Dinge aus Thread B zu
Threadübergreifender Vorgang – Thread A greift auf Dinge aus Thread B zu

Also ein Streit unter Nachbarn?

Im Bild kannst Du 3 Threads erkennen:

  1. Der Thread der grafischen OberflächeA
  2. Einen Thread B, für eine Hintergrundaufgabe 1 (mit Unteraufgaben)
  3. Ein willkürlicher dritter Thread C – nicht weiter wichtig..

Auf Thread A läuft die notwendige Arbeit, um die Eingaben, Veränderungen, etc. an der Oberfläche zu verarbeiten und darzustellen. Dazu gehören z. B. die Reaktionen und grafischen Anpassungen, wenn man auf einen Button klickt, oder das Zeichnen von Labels und anderen Steuerelementen.

Fazit – Ursache

Du kannst also nicht einfach zwischen diesen Threads hin-und-her arbeiten wie es Dir passt, zumindest nicht ohne Sinn und Verstand. Besonders jedoch dann nicht, wenn Du mit der grafischen Oberfläche interagieren willst: Z. B. mit dem Setzen eines Textbox-Textes, usw. Im Bild ist das durch das „Warn-Dreieck“ gekennzeichnet.

Wie verhindere ich den Fehler?

Den Fehler verhinderst Du relativ einfach, indem Du den korrekten Umgang mit Threads und deren (korrekter) Kommunikation untereinander lernst. Dies ist allerdings nicht unbedingt einfach – „if you understand“ 😉! Besonders am Anfang, wenn man sich mit den Grundkonzepten und der korrekten Aneinanderreihung von Dingen beschäftigt, macht’s dieses zusätzliche Wissen nicht einfacher.

Ich werde in diesem Beitrag 2-3 Beispiele abarbeiten, jedoch die Themen an sich nur ein wenig anschneiden. Wenn es um das Thema asynchrone Programmierung geht, kann ich Dir die noch folgenden Beiträge ans Herz legen. Schaue einfach, wo Du Dich am ehesten zu hause fühltst!

Obwohl ich gerne bei den „rohen Tools“ starten würde, ist der erste Kontakt für .NET-Entwickler häufig nicht die „normale“ Thread-Klasse. Ich spreche stattdessen von der BackgroundWorker-Klasse, wozu Du gern mein ausführliches Beispiel „Die ultimative BackgroundWorker Anleitung“ als Lernmaterial nehmen kannst. Im nächsten Schritt hüpfen wir aber wieder „back to topic“!

Eine besondere Beobachtung

Was ich heutzutage übrigens sehr interessant finde ist, dass eine moderne Winforms- (sowie WPF-) App den Fehler teils zu unterdrücken scheint. Dies ist natürlich weder ein Zeichen für saubere Programmierung, noch eine tolle Nachricht. Ebenso sollte man von der im Netz weit verbreiteten Lösung „CheckForIllegalCrossThreadCalls“ auf „False“ zu setzen absehen. Statt dem Leck ein Pflaster aufzusetzen, gehört der Patient richtig gecheckt und fachmännisch behandelt!

Auch wenn der Code (je nach Installation / Konfiguration) heute eventuell keinen Fehler mehr wirft, gibt es dennoch Probleme. In meinen Tests für diesen Beitrag konnte ich feststellen, dass trotz des ausbleibenden Fehlers der Text nicht gesetzt wird. Meines Erachtens nach sogar „zum Glück“, sonst würden viele vermutlich nie ein Gespür dafür bekommen, dass die Methode dort nicht ganz super ist.

Schaue Dir daher unbedingt die einzelnen unten präsentierten Beispiele mit BackgroundWorkern, Tasks und „rohen“ Threads an. Diese werden Dir beim tieferen Verständnis helfen und den Fehler in Zukunft vermeiden!

Der BackgroundWorker – ein häufig erster Threading-Kontakt

Threadübergreifender Zugriff bei BackgroundWorker vermeiden
Threadübergreifender Zugriff bei BackgroundWorker vermeiden

Wenn Du nun Deine Programmier-Anweisungen mit einem BackgroundWorker (aus welchen Gründen auch immer) in den Hintergrund verlagern möchtest – let’s go! Schaue Dir dazu einmal den folgenden Code an. Die eigentliche Arbeit wird in die Ereignishandler-Methode namens DoWork angehangen.

Ein Beispiel dazu könnte so aussehen: Wir erstellen ein klassenweites Feld namens „_backgroundWorker“ – natürlich vom Typ „System.ComponentModel.BackgroundWorker“ und instanziieren Dieses im „MainWindow“-Konstruktor. Danach machen wir die übliche „Ereignis“-Arbeit, indem wir ein gewisses Ereignis mit einer „Verarbeitungs“-Methode verknüpfen. Die Methode wird ab sofort also immer dann aufgerufen, wenn der BackgroundWorker das „DoWork“-Ereignis auslöst.

Fehlerbehaftete Demonstration

Beachte – wie schon erwähnt – , dass dies hier kein vollständiges BackgroundWorker-Beispiel wird, sondern nur eine Demonstration des Fehlers – „Ungültiger threadübergreifender Vorgang…“. Im nächsten Schritt gehen wir also hin und versuchen einfach mal, die grafische Oberfläche aus dem DoWork-Handler zu erreichen. Wir schreiben also eine Anweisung in den „DoWork“-Ereignishandler, Welche die grafische Oberfläche betrifft: textBox1.Text = „Ich bin Error!“.

using System.ComponentModel;

namespace WindowsFormsApp1;

public partial class Form1 : Form
{

    private BackgroundWorker _backgroundWorker;

    public MainWindow()
    {
        InitializeComponent();
        _backgroundWorker = new BackgroundWorker();
        _backgroundWorker.DoWork += BackgroundWorker_DoWork; ;
    }

    private void BackgroundWorker_DoWork(object? sender, DoWorkEventArgs e)
    {
        // Anweisung 1
        // langdauernde Anweisung 2
        // Anweisung 3
        
        // problematisch
        textBox1.Text = "Ich bin Error!";
    }
}
Imports System.ComponentModel

Namespace WindowsFormsApp1
    Public Partial Class Form1
        Inherits Form

        Private _backgroundWorker As BackgroundWorker

        Public Sub New()
            InitializeComponent()
            _backgroundWorker = New BackgroundWorker()
            AddHandler _backgroundWorker.DoWork, AddressOf BackgroundWorker_DoWork
        End Sub

        Private Sub BackgroundWorker_DoWork(ByVal sender As Object?, ByVal e As DoWorkEventArgs)
            ' Anweisung 1
            ' langdauernde Anweisung 2
            ' Anweisung 3
        
            ' problematisch
            textBox1.Text = "Ich bin Error!"
        End Sub
    End Class
End Namespace

Wie man es richtig mit dem BackgroundWorker macht

Nachdem wir nun im Beispiel des BackgroundWorkers gesehen haben, wie man es nicht macht, nun die Spots, wo es denn dann geht! In erster Linie gibt es akut zwei passende Punkte um einen Fortschritt an die GUI und den dazugehörigen Thread weiterzuleiten:

  • Bei einer Fortschritts-Veränderung im „ProgressChanged„-Ereignishandler
  • In einem „RunWorkerCompleted„-Ereignishandler

Schaue für ausführliche Beispiele allerdings bitte in meinen ultimativen BackgroundWorker-Guide. Im nächsten Abschnitt schauen wir uns die nächste Arbeit an, wo wir auf den „Ungültiger threadübergreifender Vorgang“-Fehler stoßen können.

Multithreading mit eigenen Threads

Threadübergreifender Zugriff bei Threads vermeiden
Threadübergreifender Zugriff bei Threads vermeiden

Nachdem wir uns nun die Thematik – über Welche die meisten Programmierer zuerst stoßen – zu Gemüte geführt haben, geht es nun Richtung Threads. Um allerdings nicht zu tief einzutauchen, werde ich auch hier nur die relevante Oberfläche ankratzen. In Zukunft werde ich natürlich auch für das Thema Threads einen eigenen Beitrag veröffentlichen – bis dahin allerdings..

Die Basis-Problematik von oben, bleibt natürlich die Gleiche, daher werde ich nicht zu sehr in diese Richtung ausschweifen. Wir schauen uns im nächsten Schritt an, wie auch hier das obige Problem zustande kommt. Also hör‘ ich schon auf zu quatschen und los geht’s: Um es kurz zu halten, habe ich nur den „Button.Click“-Ereignishandler geschrieben. Dieser ist alles was wir hier brauchen (denke natürlich an einen Knopf auf der Form..).

Die fehlerhafte Variante

Wenn unser Handler nun den Klick des Buttons verarbeitet, instanziieren wir einen neuen Thread und starten Ihn weiter unten. Vorher übergebe ich (hier für die Kürze als Lambda-Ausdruck) einen Delegaten, Welcher ausgeführt werden soll. Wir versuchen also wieder einmal aus einem fremden Thread auf die grafische Oberfläche zuzugreifen – geht immer noch nicht! Dementsprechend bekommen wir auch hier den üblichen Fehler!

    // Verknüpfung des Handlers im Konstruktor..
    private void button1_Click(object sender, EventArgs e)
    {
        var thread = new Thread(() =>
        {
            textBox1.Text = "Ich bin Error!";
        });
        thread.Start();
    }
Private Sub button1_Click(sender As Object, e As EventArgs) Handles button1.Click
    Dim thread = New Thread(Sub()
                                textBox1.Text = "Ich bin Error!"
                            End Sub)
    thread.Start()
End Sub

Eine mögliche Lösung für das Problem

Nun schauen wir uns an, wie wir das Problem vom vorherigen Code, also im Bereich Threads lösen können. Eigentlich müssen wir „nur“ nach erledigter (auslastender) Arbeit dafür sorgen, dass die grafischen Operationen wieder im UI-Thread stattfinden. Okay, ich könnte jetzt hier von Synchronisationskontext und blabla anfangen – ich lasse es. Dies ist ebenso etwas für einen separaten Beitrag!

Ich baue hier auch noch zur Veranschaulichung eine kleine Wartezeit ein. Die Oberfläche hängt durch den separaten Thread nicht und wir können anschließend aber trotzdem wieder auf Sie zugreifen. Die Operationen können wir also so wieder in den UI-Thread hieven:

    private void button1_Click(object sender, EventArgs e)
    {
        var thread = new Thread(() =>
        {
            Thread.Sleep(2000);
            BeginInvoke(() =>
            {
                textBox1.Text = "Ich bin KEIN Error!";
            });
        });
        thread.Start();
    }
Private Sub button1_Click(sender As Object, e As EventArgs)
    Dim thread = New Thread(Sub()
                                Thread.Sleep(2000)
                                BeginInvoke(Function()
                                           textBox1.Text = "Ich bin KEIN Error!"
                                       End Function)
                            End Function)
    thread.Start()
End Sub

Wir verwenden die Invoke- / BeginInvoke-Methode der Form, wir könnten Sie aber auch von der TextBox aus verwenden. Könnte einigen eventuell rein logisch besser passen.. Ich habe mich hier bewusst für BeginInvoke entschieden, da ich hier nicht auf die Beendigung warten möchte. Oh man, noch ein Thema für einen weiteren Beitrag, sorry..

Beispiel mit Tasks

Threadübergreifender Zugriff bei Tasks vermeiden
Threadübergreifender Zugriff bei Tasks vermeiden

Ganz nach dem Motto „Das Beste kommt zum Schluss“, schauen wir uns den Fehler nun ein letztes Mal, jedoch dieses Mal durch Tasks verursacht. Wenn Du meinen Beitrag „Was ist ConfigureAwait(False) und warum sollte es der Standard sein“ gesehen hast, weißt Du auch ggf. schon, wie dies zustande kommt. Auch hier werden wir wieder nur einen Ausschnitt behandeln!

Schaue Dir auch ein letztes Mal den folgenden Code an. Dort markieren wir den Ereignishandler des „Button.Click“-Ereignisses mit dem „async“-Schlüsselwort. Dies hat zur Folge, dass wir Tasks erwarten und nach deren Ausführung fortfahren können. Okay, auch hier könnte ich jetzt wieder mit Sub-Themen wie „State Machines“, Synchronisationskontexte, usw. anfangen, aber nein, auch hier nicht..

Fehlerhafte Verwendung

Besonders interessant ist hier die „ConfigureAwait“-Methode mit dem als „False“ übergebenen Wert. Dies hat die Auswirkung, dass „er“ nach der Ausführung der „Delay“-Task nicht mehr zurück in den vorherigen Thread hüpft – kurz gesagt.. Dies bedeutet aber auch hier, dass wir uns danach nicht mehr im Thread der grafischen Oberfläche befinden! Wir bekommen also unsern altbekannten Fehler: „Ungültiger threadübergreifender Vorgang“.

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task
            .Delay(2000)
            .ConfigureAwait(false);
        textBox1.Text = "Ich bin Error!";
    }
Private Async Sub button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles button1.Click
    Await Task.Delay(2000).ConfigureAwait(False)
    textBox1.Text = "Ich bin Error!"
End Sub

Die Lösung für Tasks

Zum Abschluss blicken wir nun auf die Lösung für die Task: Lasse die Ausführung nach Abschluss der Task wieder zurück in den „captured context“ hüpfen. Vermeide also „ConfigureAwait(False)“, wenn Du anschließend in den Ursprungs-Kontext – also hier den Thread der grafischen Oberfläche zurück möchtest. Wie gesagt, ich erspare die hier die Erklärungen des Synchronisationskontext-Prinzips (erst einmal).

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Delay(2000);
        textBox1.Text = "Ich bin KEIN Error!";
    }
Private Async Sub button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles button1.Click
    Await Task.Delay(2000)
    textBox1.Text = "Ich bin KEIN Error!"
End Sub

Fazit – „Der Ungültiger threadübergreifender Vorgang“-Fehler

Fazit zum Fehler "Ungültiger threadübergreifender Vorgang"
Fazit zum Fehler „Ungültiger threadübergreifender Vorgang“

Puh, nun am Ende angekommen, können wir unser kleines Fazit zum benannten Fehler ziehen. Er taucht auf, weil wir einfach aus einem anderen Thread als die Oberfläche, auf die Steuerelemente wie „Label1.Text“, „Button1.Text = „…“, usw. zugreifen. Dies geht nicht, da die grafische Oberfläche nur aus dem „UI-Thread“ aus verändert werden kann. Wenn wir dies dennoch aus einem anderen Thread tun wollen, müssen wir unsere Aktionen irgendwie rüber in die UI hieven. Je nach Situation: BackgroundWorker, rohe Threads, oder Tasks müssen wir hierfür andere Varianten wählen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert