C# Invoke, BeginInvoke, InvokeRequired

Controls kunnen op twee manieren worden gemanipuleerd vanuit een andere thread, namelijk:
  1. Invoke()
  2. BeginInvoke()
Waarom is het niet mogelijk om direct controls te manipuleren vanuit een andere thread? Dat is de vraag die centraal staat voor dit blog.

Message queue en message pumping
Elke Windows applicatie die wordt gemaakt is bij het opstarten voorzien van een thread. Deze thread roept de main method aan van de applicatie. GUI applicaties zijn event gebaseerd, wat inhoud dat er een entiteit moet bestaan om events af te vuren en te verwerken. Windows doet dit door een message queue aan te maken voor de applicatie. Alle UI gerelateerde acties worden vertaald naar messages die worden doorgestuurd naar de message queue. Nu is er nog iets nodig die deze messages van de queue leest en de geschikte event handlers aanroept. Dit gebeurt door de message pump. De message pump is in principe een loop die wacht totdat iemand een message op de queue plaatst. Zodra iemand dit doet, haalt de pump de message van de queue en roept hij de geassocieerde event handler aan. Denk bijvoorbeeld aan zoiets:

Message message = null;
while ((message = GetNextMessage()) != null)
{
ProcessMessage(message);
}
Waar draait de message pump? Op de main thread.Dus een typische GUI applicatie, na het uitvoeren van initializatie in the main method, begint dan met het starten van de message pump. De while loop stopt alleen op het moment dat de applicatie wordt afgesloten. Dat betekent dus dat de main thread niks anders kan doen op het moment dat de message pump is gestart. In .NET GUI applicaties zorgt Application.Run() voor message pumping. Elke .NET GUI applicatie's main methode ziet er zo uit:

public static void Main(string[] args)
{
Form f = new Form();
Application.Run(f);
}
Application.Run() voert de message loop uit op de thread waarop hij is aangemaakt. In dit geval is dit de main thread. Modal dialogs (form.ShowDialog()) hebben hun eigen message pump, terwijl niet-modal dialogs (form.Show()) geen eigen message pump hebben. Dit betekent dat form.ShowDialog() vanuit elke thread kan worden aangeroepen, maar form.Show() heeft de aanroepende thread nodig voor de message pump. Merk op dat als form.Show() wordt aangeroepen vanuit een UI event handler, het nieuwe form en de huidige form beide dezelfde message pump gebruiken. Dit betekent dat als één form de fout ingaat met één van de event handlers het andere form ook onbruikbaar is.

Die ene regel
Een van de belangrijkste regels van Windows GUI programmeren is dat alleen de thread die een control heeft aangemaakt toegang heeft tot de methods/properties van dat control. Indien via een andere thread geprobeerd wordt toegang te verkrijgen tot dat control, kan er onverwacht gedrag optreden of kunnen excepties worden verwacht. De juiste manier om een control te manipuleren vanuit een andere thread is om een message te sturen naar de applicatie's message queue. Wanneer de message pump die message uitvoert, wordt het control bijgewerkt op dezelfde thread waarop het control is aangemaakt (nogmaals, de message pump loopt op de main thread).

Even wat dieper in de Win32 wereld. Er zijn twee fundamentele Win32 API aanroepen om een control te manipuleren: SendMessage en PostMessage. Er is een groot verschil op de manier waarop de twee werken. Het grootste verschil is dat SendMessage de aanroeper blokkeert totdat de message is verwerkt door de message pump, dit in tegenstelling tot PostMessage die direct retourneert. Een subtiel maar belangrijk verschil is dat messages die zijn verzonden door SendMessage niet in de message queue worden geplaatst, terwijl messages verzonden door PostMessage wel in de message queue worden geplaatst. SendMessage messages worden direct naar de message pump gestuurd. De message pump ontvangt en verwerkt messages die zijn ontvangen van SendMessage voordat hij in de message queue kijkt. Er zijn dus twee queues, één voor SendMessage en één voor PostMessage (welke de message queue wordt genoemd). De message pump verwerkt eerst alle messages in de SendMessage queue en daarna de messages in de message queue. Een interessante bemerking is dat als een SendMessage wordt uitgevoerd binnen de message pumping thread, de window procedure direct wordt aangeroepen, zonder door de message pump te gaan.

Waarom die ene regel bestaat
De eerste reden waarom die ene regel bestaat is de blokkerende eigenschap die heeft. Stel een situatie voor waar de message pumping thread wacht op een andere thread totdat die klaar is en dat die andere thread een SendMessage uitvoert. SendMessage retourneert alleen nadat de message pump de message heeft verwerkt, maar de message pump loopt vast omdat hij wacht totdat de thread klaar is. Deadlock.

In .NET, method/property aanroepen op een control worden vertaald naar aanroepen. SendMessagePostMessage verzend een message naar the queue en retourneert direct, geen kans dus op een deadlock.

De tweede reden is meer subtiel. Omdat er twee queues zijn is het mogelijk dat op een gegeven moment, beide queues dezellfde message hebben, maar met andere parameters. Even aannemen dat een message de tekst van een control zet en dat SendMessage is gebruikt. De message pump verwerkt eerst de SendMessage messages en de tekst is gezet naar de gewenste waarde. Vervolgens verwerkt de message pump de messages in de message queue (verzonden door PostMessage). Nu komt dezelfde message voorbij, maar met die andere parameters. De tekst voor het control wordt nu overschreven door deze parameters. Het is niet mogelijk te weten wanneer welke message werd verstuurd. Het is goed mogelijk dat de SendMessage als laatst werd verstuurt, maar dat is nooit zeker. Bij het uitlezen van de tekst property kan het natuurlijk ook misgaan op deze manier. Als SendMessage de waarde uitleest en de waarde is nog niet gezet door de message queue krijg je onverwachte resultaten.

Als PostMessage was gebruikt in plaats van SendMessage dan zouden beide messages verzonden zijn naar dezelfde queue en op volgorde van binnenkomst worden uitgevoerd.

Er is nog een ander issue met SendMessage die niet helemaal te verwachten is. SendMessage, terwijl die de huidge thread blokkeert, gaat door met het verwerken van messages die zijn verzonden door SendMessage of één van de methods die nonqueued messages verstuurt. Dit betekent dat de code voorbereid moet zijn op inkomende messages die geblokkeerd waren tijdens een SendMessage aanroep. Dit kan problemen veroorzaken als de code afhankelijk is van SendMessage om een blokkerende te hebben en geen gebruik maakt van andere synchronisatie mechanismen. Bekijk de volgende code:

void ButtonClick_Handler(object sender, EventArgs e)
{
int val = count; // count is a class member variable
SendMessage(...);
Console.WriteLine(val == count); // Surprise, val != count can happen!
}

void SomeOtherMessageHandler()
{
count++;
}
Op het moment dat bovenstaand stukje code geblokkeerd is op SendMessage en iets anders voert een SendMessage uit naar deze applicatie die de methode SomeOtherMessageHandler() uitvoert, dan is val niet meer gelijk aan count. De kern van deze issue is dat SomeOtherMessageHandler() wordt uitgevoerd, zelfs als ButtonClick_Handler is geblokkeerd door SendMessage. Dit gedrag wordt niet door veel mensen verwacht. Om zulk soort problemen te voorkomen, biedt Windows de SendMessageTimeout API functie. Op het moment dat deze functie wordt aangeroepen met de juiste parameters, voorkomt hij het pumpen non-queued messages wanneer een blokkering is bij het versturen van een message. Let wel, alle .NET UI controls gebruiken SendMessage dus het probleem is niet te vermijden.

Waarom en wanneer BeginInvoke() aanroepen
BeginInvoke() versturrt een PostMessage. Wanneer een control moet worden bijgewerkt welke niet aangemaakt is op de thread waarop de aanroep plaatsvindt, moet dit gebeuren via BeginInvoke(). Volgens MSDN: "There are four methods on a control that are safe to call from any thread: Invoke, BeginInvoke, EndInvoke, and CreateGraphics. For all other method calls, you should use one of the invoke methods to marshal the call to the control's thread". De control class biedt één property en twee methoden voor manipulatie:
  • Invokerequired (property): Deze bool property retourneert true als de thread waarop deze property wordt aangeroepen niet de thread is waarop hij is aangemaakt. Het komt erop neer dat als true wordt geretourneert één van de Invoke methods moet worden aangeroepen.
  • BeginInvoke (method): Deze is functioneel hetzelfde als de PostMessage API. Hij verstuurt een message naar de queue en retourneert direct zonder te wachten totdat de message is verwerkt. BeginInvoke retourneert een IAsyncResult, net als de op een delegate. Met de IAsyncResult kan worden gewacht totdat de message is verwerkt. EndInvoke kan worden gebruikt om return values, of out parameter waarden op te halen.
  • Invoke (method): Deze is als de SendMessage API functie en wacht totdat demessage is verwerkt, maar hij verzend intern geen SendMessage, maar een PostMessage. Het verschil is dat hij wacht totdat de delegate uitgevoerd is op de UI thread voordat iets wordt geretourneert. Opdat er een kans op een deadlock, is het verzekerd dat andere problemen die geassocieerd zijn met SendMessage niet gebeuren. Invoke retourneert de waarde die de methode, welke is aangeroepen door de delegate, retourneert.
Een typisch voorbeeld van het gebruik van de BeginInvoke is als volgt:


Een waarde toewijzen aan label.text direct in SetLabelText resulteert in een SendMessage naar het onderliggende control van de huidige thread en dat geeft problemen. Daarom wordt BeginInvoke gebruikt, waarbij dezelfde methode wordt meegestuurd als delegate parameter. Op het moment dat de message pomp de message verzend, roept hij SetLabelText aan en binnen die aanroep retourneert InvokeRequired false. Dan wordt de waarde gezet voor de label en is het zeker dat er op de juiste thread wordt gewerkt.

De BCL (Base Class Library) biedt een MethodInvoker delegate welke gebruikt kan worden als de wrapped methode geen parameters en geen return value heeft. Ook kan de EventHandler delegate gebruikt worden, wat wordt aangeraden door de MSDN documentatie.

Beide, BeginInvoke en Invoke gaan na of zij worden aangeroepen op de juiste thread (de thread die het control heeft aangemaakt) en als dat zo is, wordt direct het control bijgewerkt in plaats van het uivoeren van de PostMessage. Naast de performance voordelen, voorkomt het ook deadlocks als Invoke wordt aangeroepen vanuit een methode die reeds draait op de UI thread.

Invoke en BeginInvoke
Welke nu te gebruiken? Dat hangt af van de situatie. Als het nodig is de UI te updaten voordat verder mag worden gewerkt, maak dan gebruik van Invoke. Als dit niet nodig is, gebruik dan BeginInvoke. Er zijn wel een paar tricky dingen bij het gebruik van BeginInvoke.
  • Als de methode die aangeroepen wordt via BeginInvoke een gedeelde status binnentreed (gedeelde status tussen de UI thread en andere threads) kan het makkelijk fout gaan. De status kan veranderen tussen de tijd dat BeginInvoke is aangeroepen en wanneer de wrapped methode daadwerkelijk wordt uitgevoerd. Dit kan lastig te debuggen zijn.
  • Als reference parameters worden meegestuurd aan de methode die wordt aangeroepen via BeginInvoke dan moet er voor worden gezorgd dat niks/niemand anders deze waarden aanpast. Het is gebruikelijk het object te klonen voordat het wordt meegegeven als parameter aan BeginInvoke, wat dus de problemen vermijd.
Let er op dat bovenstaande punten gelden voor elke methode die worden uitgevoerd als een thread. Ze zijn niet zo voor de hand liggend als BeginInvoke wordt gebruikt, omdat BeginInvoke eigenlijk geen thread aanmaakt en in plaats daarvan de wrapped methode op een bestaande thread (de UI thread) uitvoert.

Tot slot
Control.BeginInvoke, wat tot nu toe is besproken, werkt iets anders dan delegate.BeginInvoke. Delegate.BeginInvoke neemt een threadpool thread en voert de meegegeven delegate uit op die thread. Control.BeginInvoke gebruikt geen threadpool thread en doet een PostMessage naar de doel window handle en retourneert. Dit is cruciaal omdat als het threads gebruikt, dan is er geen garantie op de volgorde waarop messages worden verzonden en verwerkt door de applicatie. In tegenstelling tot Delegate.BeginInvoke, bij het aanroepen van Control.BeginInvoke hoeft niet gevolgd te worden door EndInvoke. Als gebruik gemaakt wordt van de return waarden dan is het natuurlijk wel nodig.

bron: http://www.codeproject.com/KB/cs/begininvoke.aspx

Reacties

Populaire posts van deze blog

[SQL Server] varchar vs nvarchar

MS Sql 70-461: Chapter 5

[C#] Class serialiseren en deserialiseren