Async Await in C#
Inleiding
Het kan bijzonder irritant wezen als computers en schermen blokkeren zodra een langlopend programma is opgestart en we in die tussentijd verder niks meer met die computer kunnen doen.
We willen dat het programma op de achtergrond (asynchroon) draait zonder geblokkeerde schermen zodat we in die tussentijd ook nog andere dingen op die computer kunnen doen. We kunnen met Threads en de TPL (Task Parallel Library) ervoor zorgen dat een programma asynchroon wordt opgestart (zie de desbetreffende posts over Threads en de TPL).
We kunnen sinds C# 5.0 ook de async await keywords gebruiken. We geven dan vanuit een hoofdprogramma met keyword async aan dat iets asynchroon wordt opgestart. Met de await keyword geven we aan dat, net zoals de engel op de uitgelichte afbeelding, gewacht moet worden op datgene wat asynchroon is opgestart.
In de voorbeelden in deze post moet een hoofdprogramma wachten op een asynchrone taak zodat die de gelegenheid krijgt om zijn dingen te doen en om zijn resultaten in een textbox te zetten en in het geval van de engel moet de engel wachten op jou zodat jij de gelegenheid hebt om een (hopelijk) prettig leven te leiden voordat je gaat hemelen en ze je kan meenemen naar de hemel (of afhankelijk van je daden in je leven naar de hel).
Helaas voor een hoofdprogramma en de engel dat die moeten wachten, maar dat geldt niet voor jou. Jij kan blijven doen wat je wilt doen want de schermen bevriezen verder niet omdat alles asynchroon is opgestart. Het scherm wordt bij de asynchroon draaiende taken vanuit de achtergrond bijgewerkt en je hoeft het scherm niet zelf te “refreshen”.
Zoals de TPL veel uit handen nam m.b.t. threads, zo nemen de async await keywords weer veel dingen uit handen m.b.t. de TPL. De communicatie met de TPL wordt via de async await keywords gedaan zodat jij dat niet meer hoeft te doen.
Windows Form
We hebben tot dusver het één en ander gedemonstreerd aan de hand van Console Applications, maar een Console Application is niet echt bedoeld voor uitgebreide interactie met gebruikers met een user interface (UI).
We zullen daarom het één en ander toelichten aan de hand van een Windows Form Application dat wel een uitgebreide UI heeft. Vanuit een Windows Form gaan we een programma als een taak opstarten waarbij we zien dat het scherm benaderbaar blijft en niet bevriest als de taak asynchroon wordt opgestart.
We maken een Windows Form met een textbox waarin we de resultaten neerzetten en drie buttons waarmee een programma op verschillende manieren kan worden opgestart.
Een blik op de code achter de Windows Form. We zien de _Click(…)-event handlers die het opgeworpen event vanuit de User Interface (UI) afvangen. In dit geval is de event een muisklik op een bepaalde button. We gebruiken een randomizer (Random) om elke taak een unieke naam te geven zodat we beter kunnen zien wat de taak doet.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsForms_Async
{
public partial class Form1 : Form
{
// randomizer
Random random = new Random();
public Form1() { ... }
private void CLEAR_Click
(object sender, EventArgs e) { ... }
private string Actie_01
(string ID, string Soort) { ... }
private void SYNCHROON_Click
(object sender, EventArgs e) { ... }
private void TPL_Click
(object sender, EventArgs e) { ... }
private async void ASYNC_Click
(object sender, EventArgs e) { ... }
}
}
Methode
In vorige posts over threads en de TPL hadden we het voorbeeld aangehaald van een olietank waar meerdere monteurs naar toegaan om de olie af te tappen die zij nodig hebben voor hun onderhoudsbeurten.
We gaan verder met het voorbeeld en we beschouwen de form als een console van waaruit we de gevraagde liters olie voor de monteurs vrijgeven. Het vrijgeven gebeurt door methode Actie_01 waarbij de methode vanuit de form wordt opgestart.
We zetten een vertraging van twee seconden in de methode met Thread.Sleep. Dit om te simuleren dat voor de daadwerkelijke vrijgave ook wat tijd nodig is. De methode doet verder niet veel bijzonders. Het laat de ID zien die aan de taak is toegekend en het simuleert met een randomizer het aantal liters dat wordt vrijgegeven.
private string Actie_01(string ID,
string Soort)
{
Random random2 = new Random();
Thread.Sleep(2000);
return
"\r\n Einde verwerking " +
Soort + " taak " +
ID + " -> Vrijgave " +
random2.Next(1, 65) + " liter.\r\n";
}
Synchroon
De onderstaande code wordt “getriggered” zodra op button Synchroon wordt geklikt:
private void SYNCHROON_Click(object sender,
EventArgs e)
{
string ID = random.Next().ToString();
textBox1.Text +=
"\r\n Begin verwerking SYNCHRONE taak " +
ID + "\r\n";
textBox1.Text +=
Actie_01(ID,"SYNCHRONE");
}
Methode Actie_01 wordt synchroon opgestart en draait nadat op de button is geklikt, maar dat zien we verder niet. We zien alleen een bevroren scherm en we kunnen op de button blijven klikken, maar de UI geeft geen feedback want het scherm wordt pas bijgewerkt als het langlopende programma klaar is met draaien en de thread weer voor het scherm beschikbaar is.
Je komt er pas dan achter dat je misschien drie keer of meer op de knop hebt geklikt omdat response na de eerste keer klikken uitbleef.
TPL
De onderstaande code wordt “getriggered” zodra op button TPL wordt geklikt:
private void TPL_Click(object sender, EventArgs e)
{
string ID = random.Next().ToString();
textBox1.Text +=
"\r\n Bezig met ASYNCHRONE TPL taak " +
ID + ". Even geduld s.v.p.\r\n";
Task.Factory.StartNew
(() => Actie_01(ID,"ASYNCHRONE TPL "))
.ContinueWith
(t => textBox1.Text += t.Result,
TaskScheduler
.FromCurrentSynchronizationContext());
}
De code zorgt ervoor dat methode Actie_01 als een TPL taak wordt opgestart. Het scherm en de taak hebben nu hun eigen thread en je merkt dat dit een betere “User Experience” geeft. Het scherm bevriest niet en je krijgt na elke klik op de TPL-button meteen response.
De taak moet uiteindelijk gegevens teruggegeven aan het scherm, maar het scherm draait op een andere thread en je moet de taak dan ook opstarten met TaskScheduler.FromCurrenSynchronizationContext omdat je anders een foutmelding krijgt.
Ten slotte volgt een .ContinueWith(). De taak moet na afloop de textbox vullen met de resultaten.
Async Await
De onderstaande code wordt “getriggered” zodra op button Async Await wordt geklikt:
private async void ASYNC_Click(object sender,
EventArgs e)
{
// Genereer ID
string ID = random.Next().ToString();
textBox1.Text +=
"\r\n Bezig met ASYNC AWAIT taak " + ID +
". Even geduld s.v.p." + "\r\n";
// Inplannen en uitvoeren als taak
var result =
await Task.Run(()
=> Actie_01(ID, "ASYNC AWAIT "));
// We werken de textbox bij
// als de await taak klaar is.
textBox1.Text += result;
}
Event Handler ASYNC_Click vangt de klik event af op button Async Await. We voorzien de desbetreffende event handler van keyword async om aan te geven dat in de event handler iets asynchroon wordt opgestart.
Met de await keyword geven we aan dat gewacht moet worden op datgene wat asynchroon is opgestart. In dit voorbeeld starten we een taak op en we moeten wachten tot de taak klaar is. De taak krijgt zo de gelegenheid om haar dingen te doen en om na afloop de textbox te vullen .
Je start nog steeds via de TPL een taak en de taak heeft in dit geval betrekking op methode “Actie_01”. De communicatie met en het “optuigen” van de TPL wordt nu volledig via de async await keywords gedaan zodat jij dat niet meer hoeft te doen.
En we zien dat methode “Actie_01” asynchroon wordt opgestart hetgeen gepaard gaat met een directe response zodra je nogmaals op de button klikt en een scherm dat niet meer “bevriest”.
Resultaat
Hoe het scherm eruit ziet. Zo is taak 1960435853 de laatste taak die is opstart. Taak 1714463805 is eerder opgestart en we zien de vrijgegeven 52 liter van die taak voor de 2 liter van taak 1960435853 verschijnen. Het scherm wordt bij de asynchroon draaiende taken vanuit de achtergrond bijgewerkt. Je hoeft het schern niet zelf te refreshen.
Slot
In deze post heb ik laten zien hoe vanuit een Windows Form een methode opgestart wordt. De methode kan op verschillende manieren opgestart worden en we zien dat het scherm bevriest bij het synchroon opstarten van de methode en het scherm pas wordt vrijgegeven en pas wordt bijgewerkt nadat de desbetreffende methode haar dingen heeft gedaan.
We zien dat allemaal niet bij het asynchroon opstarten van de methode. De schermen bevriezen niet omdat alles asynchroon is opgestart en het scherm en de taak nu hun eigen threads hebben. Verder wordt het scherm bij de asynchroon draaiende taken vanuit de achtergrond bijgewerkt. Je hoeft het scherm niet zelf te refreshen.
Een programma wordt bij het asynchroon opstarten als een taak opgestart. We kunnen hiervoor de TPL gebruiken en sinds sinds C# 5.0 de async await. Zoals de TPL veel uit handen nam m.b.t. threads, zo nemen de async await keywords weer veel dingen uit handen m.b.t. de TPL. De communicatie met en het “optuigen” van de TPL wordt via de async await keywords gedaan zodat jij dat niet meer hoeft te doen.
Hopelijk ben je met deze posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over C# heb geschreven? Hit the C# button…
Voorbeeldprogramma
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsForms_Async
{
public partial class Form1 : Form
{
// randomizer
Random random = new Random();
//--------------------------------------------
public Form1()
{
InitializeComponent();
}
//--------------------------------------------
private void CLEAR_Click(object sender,
EventArgs e)
{
textBox1.Text = null;
}
//--------------------------------------------
private string Actie_01(string ID,
string Soort)
{
Random random2 = new Random();
Thread.Sleep(2000);
return
"\r\n Einde verwerking " +
Soort + " taak " +
ID + " -> Vrijgave " +
random2.Next(1, 65) + " liter.\r\n";
}
//--------------------------------------------
private void SYNCHROON_Click(object sender,
EventArgs e)
{
string ID = random.Next().ToString();
textBox1.Text +=
"\r\n Begin verwerking SYNCHRONE taak " +
ID + "\r\n";
textBox1.Text +=
Actie_01(ID,"SYNCHRONE");
}
//--------------------------------------------
private void TPL_Click(object sender, EventArgs e)
{
string ID = random.Next().ToString();
textBox1.Text +=
"\r\n Bezig met ASYNCHRONE TPL taak " +
ID + ". Even geduld s.v.p.\r\n";
Task.Factory.StartNew
(() => Actie_01(ID,"ASYNCHRONE TPL "))
.ContinueWith
(t => textBox1.Text += t.Result,
TaskScheduler
.FromCurrentSynchronizationContext());
}
//--------------------------------------------
private async void ASYNC_Click(object sender,
EventArgs e)
{
// Genereer ID
string ID = random.Next().ToString();
textBox1.Text +=
"\r\n Bezig met ASYNC AWAIT taak " + ID +
". Even geduld s.v.p." + "\r\n";
// Inplannen en uitvoeren als taak
var result =
await Task.Run(()
=> Actie_01(ID, "ASYNC AWAIT "));
// We werken de textbox bij
// als de await taak klaar is.
textBox1.Text += result;
}
//--------------------------------------------
}
}