Grafikus C# – Windows Forms tanfolyam › 11.3 lecke Windows Forms · 11.3 lecke · szálak Minden Windows Forms program tapasztalt már lefagyást: egy gombnyomás után az ablak nem mozdítható, nem rajzolódik újra, a címsorban megjelenik a (Nem válaszol) felirat. Ez nem a számítógép „lassúsága”, hanem a program szerkezetéből következik: a felület minden eseményét egyetlen szál dolgozza fel, a felületi szál (UI-szál, főszál). Ha ez a szál egy eseménykezelőben hosszú munkát végez, nincs ideje rajzolni és az egérre reagálni. Ebben a leckében megérted az üzenetsort, a lefagyás pontos okát, a vezérlők szálhoz kötöttségét, az UI-szál és a felület lefagyása
Application.DoEvents csapdáit, és megteszed az első lépést a háttérszál felé.
A lecke végére
- elmagyarázod az üzenetsor (message loop) működését és a felületi szál szerepét;
- tudod, miért fagy le az ablak egy hosszú eseménykezelő alatt, és felismered a tüneteit;
- ismered a szabályt: a vezérlők csak a létrehozó szálról érhetők el (
InvalidOperationException,InvokeRequired); - érted az
Application.DoEventsműködését és az újrabelépés (reentrancy) veszélyét; - mérni tudod a felület késését (az időzítők közti eltérés);
- elindítasz egy háttérszálat (
Thread), és tudod, mit szabad és mit nem szabad benne.
Mielőtt elkezded
Ehhez a leckéhez az 5.1 lecke (események), a 11.1–11.2 lecke (Timer, Stopwatch, folyamatjelző) szükséges. Szálakról korábban nem kellett tanulnod.
Az üzenetsor és a felületi szál
A Windows a felhasználói eseményeket (kattintás, billentyű, újrarajzolás kérése, időzítő jelzése) üzenetként egy sorba teszi minden ablakhoz. A programod Main metódusában az Application.Run(new Form1()) egy végtelen ciklust indít, amely az üzeneteket sorban kiveszi, és meghívja a megfelelő eseménykezelőt. Ez a ciklus és minden eseménykezelő ugyanazon a szálon fut:
Application.Run(...)
ismételd:
várj, amíg érkezik üzenet
kattintás? → hívd a Click eseménykezelőt
újrarajzolás? → hívd a Paint-et
időzítő jelzés? → hívd a Tick-et
billentyű? → hívd a KeyDown-t
bezárás? → hívd a FormClosing-ot
(közben EGY üzenetet dolgozol fel; a többi a sorban VÁR)private int felületiSzal;
private void Form1_Load(object sender, EventArgs e)
{
felületiSzal = Environment.CurrentManagedThreadId; // a felületi szál azonosítója
lblSzal.Text = "Felületi szál: " + felületiSzal;
}
private void btnTeszt_Click(object sender, EventArgs e)
{
// minden eseménykezelő ugyanazon a szálon fut
lstNaplo.Items.Add("Click: szál " + Environment.CurrentManagedThreadId); // ugyanaz a szám
}
private void tmr_Tick(object sender, EventArgs e)
{
lstNaplo.Items.Add("Tick: szál " + Environment.CurrentManagedThreadId); // ez is ugyanaz
}Click, Paint, Tick, TextChanged és minden más felületi esemény a felületi szálon fut, egymás után: sosem egyszerre. Emiatt nem kell zárakat használnod, amíg a felületi szálon maradsz; ez a Windows Forms egyik kényelme.Paint üzenet sorban áll: az ablak üres, szürke vagy tele van „szellem-nyomokkal”, amíg a szál szabad lesz.Environment.CurrentManagedThreadId az éppen futó szál száma. Kísérletezéshez hasznos: kiírva látod, melyik kód melyik szálon fut.Jósolj!
A Click és a Tick eseménykezelő is kiírja a szál azonosítóját. Egyezik-e a két szám?
Jóslat ellenőrzése
Igen: mindkettő a felületi szálon fut (a Windows.Forms.Timer a felületi szálon küldi a Tick eseményt, 11.1 lecke). A System.Timers.Timer.Elapsed kezelője ezzel szemben más számot mutatna: az másik szálon fut.
Miért fagy le az ablak?
private void btnSzamol_Click(object sender, EventArgs e)
{
btnSzamol.Enabled = false;
lblAllapot.Text = "Számolok..."; // A SZÖVEG NEM FOG MEGJELENNI: a felület szál nem jut rajzoláshoz
long db = 0;
for (long n = 2; n < 60000000; n++) // hosszú számítás a felületi szálon
{
if (Prim(n)) { db++; }
}
lblAllapot.Text = "Kész: " + db; // csak ekkor frissül mindkét szöveg (egyszerre)
btnSzamol.Enabled = true;
}lblAllapot.Text = ... csak módosítja a vezérlő adatát és kér egy újrarajzolást (Paint üzenet). A tényleges rajzolás az üzenetsor feldolgozásakor történik. Mivel a felületi szál a ciklusban van, a Paint csak a kezelő végén fut le: a „Számolok...” szöveg sosem látszik, az ablak „kész”-re ugrik.Jósolj!
A felhasználó a hosszú számítás közben kétszer rákattint a Bezár (X) gombra. Mi történik?
Jóslat ellenőrzése
A kattintások a sorba kerülnek, és csak a számítás vége után dolgozódnak fel (a FormClosing akkor fut le). A Windows ugyanakkor felajánlja: Program bezárása (a folyamat erőszakos leállítása), ami az adatvesztés kockázatával jár.
A szabály: a vezérlők a létrehozó szálhoz tartoznak
Ha a számítást másik szálra tesszük (a megoldás), új szabály lép életbe: a vezérlők csak azon a szálon érhetők el, amelyik létrehozta őket (a felületi szálon). A Windows Forms vezérlők nem szálbiztosak: két szál egyidejű módosítása megsérthetné a belső állapotukat. A hibát a keretrendszer észleli:
private void btnSzal_Click(object sender, EventArgs e)
{
Thread szal = new Thread(() =>
{
long db = PrimekSzama(60000000); // hosszú munka: a felület közben él
// lblAllapot.Text = "Kész: " + db; // InvalidOperationException: másik szálról nem érhető el!
// helyesen: a felületi szálra „küldjük” a módosítást
this.BeginInvoke(new Action(() => lblAllapot.Text = "Kész: " + db));
});
szal.IsBackground = true; // a program bezárásakor ne tartsa életben
szal.Start();
}
// ellenőrzés: melyik szálon vagyunk?
private void UjCimke(string szoveg)
{
if (lblAllapot.InvokeRequired) // igaz, ha NEM a vezérlő szálán hívták
{
lblAllapot.BeginInvoke(new Action<string>(UjCimke), szoveg); // ugyanezt a metódust a felületi szálon újra hívja
return;
}
lblAllapot.Text = szoveg;
}InvalidOperationException kivételt dob (Cross-thread operation not valid). Régebbi .NET Framework alatt ez csak hibakereséskor (a Visual Studióban) jelentkezhetett, éles futásnál csendben tönkretehette a programot: a hibát mindig javítani kell.InvokeRequired tulajdonsága igaz, ha az aktuális szál nem a vezérlőé. Ilyenkor az Invoke (megvárja) vagy BeginInvoke (nem vár) a megadott kódot a felületi szálon futtatja. Részletesen a 11.7 leckében.Control.CheckForIllegalCrossThreadCalls = false kikapcsolja a hibajelzést, de nem szünteti meg a problémát: versenyhelyzeteket, véletlenszerű hibákat okoz. Soha ne használd.Jósolj!
A háttérszál a számítás közben ötször közvetlenül írja a progressBar1.Value értékét, kivétel nélkül (mert a CheckForIllegalCrossThreadCalls hamis). Mi a veszély?
Jóslat ellenőrzése
A vezérlő belső állapotát két szál egyszerre módosíthatja: ritkán, véletlenszerűen megjelenő hibák (lefagyás, hibás rajz, handle-hibák). Mivel „általában működik”, a hiba nehezen reprodukálható és nehezen javítható. A helyes út az Invoke/BeginInvoke, vagy a Progress<T> (11.7 lecke).
Az Application.DoEvents és az újrabelépés
Az Application.DoEvents() a hosszú ciklus közben ideiglenesen feldolgozza a sorban álló üzeneteket: az ablak újrarajzolódik, a gombok reagálnak. Régi programokban (és a mi 10.2 leckénk vészmegoldásában) gyakori. Csak vészmegoldás, mert maga a ciklus továbbra is a felületi szálon fut, és új hibát hoz: az újrabelépést:
private int futo = 0;
private void btnMunka_Click(object sender, EventArgs e)
{
futo++;
if (futo > 1) { lstNaplo.Items.Add("ÚJRABELÉPÉS: a kezelő már fut, és újra elindult (" + futo + ". példány)"); }
for (int i = 0; i < 50; i++)
{
Thread.Sleep(100); // a „munka”
Application.DoEvents(); // a felület frissül, DE új Click is feldolgozódhat!
}
futo--;
}DoEvents közben a sorban álló új kattintás is feldolgozódik: ugyanaz a kezelő még egyszer elindul, a futó példány „alatt”. Két párhuzamos (egymásba ágyazott) munka keletkezik, az adatok összekeverednek, az állapot (futo) hibás.DoEvents közben a FormClosing is lefuthat (a Form bezárul, miközben a munka a vezérlőket használja), vagy más események módosíthatják az adatokat, amelyeket a ciklus épp feldolgoz.Enabled = false), ellenőrizz egy futo jelzőt, és kezeld a bezárást. A valódi megoldás nem a DoEvents finomítása, hanem a háttérszál.async/await (11.5) és a Task.Run (11.6) ezt a problémát elegánsan megoldja.Jósolj!
A fenti kezelő futása közben (5 másodperc) a felhasználó háromszor kattint az Indít gombra. A gomb nincs letiltva. Hányszor indul el a ciklus, és mikor ér véget a legelső?
Jóslat ellenőrzése
Összesen négyszer (az eredeti + három újrabelépés); mivel egymásba ágyazódnak, a legelső csak utoljára fejeződik be (a veremben a legalsó): a futási idő megsokszorozódik, és a naplóban az ÚJRABELÉPÉS üzenetek jelennek meg.
Mit tegyél ide: a fagyás-labor: a négy változat gombjai, a mozgó négyzet, a késésmérő értéke és a napló az ÚJRABELÉPÉS üzenetekkel.Javasolt fájlnév: winforms-11-3-fagyas-labor.webp · Alt szöveg: A fagyás-labor program
Hibakeresés: te vagy a hibadetektív
Mi a baj, és hogyan javítod?
Gondold végig az okot, és csak utána nyisd le a megoldást.
1. Az állapotszöveg nem jelenik meg a művelet közben
lblAllapot.Text = "Dolgozom...";
HosszuMunka(); // ugyanazon a szálonHiba és javítás
A felületi szál foglalt: a rajzolás a kezelő után történik. A munkát háttérszálra kell tenni (vagy darabolni); az Application.DoEvents csak vészmegoldás.
2. InvalidOperationException a háttérszálban
new Thread(() => { lstNaplo.Items.Add("kész"); }).Start();Hiba és javítás
A ListBox másik szálról nem érhető el. Használj BeginInvoke / Invoke metódust.
3. A programot nem lehet bezárni
Thread szal = new Thread(Munka);
szal.Start(); // előtér-szál: a program addig fut, amíg a szál élHiba és javítás
Az előtér-szál (alapértelmezett) életben tartja a folyamatot a Form bezárása után is. Állítsd IsBackground = true értékre, és adj lehetőséget a megszakításra (11.8 lecke).
4. A gomb kétszer indítja a munkát
A gyors dupla kattintás két párhuzamos (vagy egymásba ágyazott) feldolgozást indít. Nincs letiltva a gomb, és nincs Ok és javítás
futo jelző. A kezelő első sora: btnIndit.Enabled = false (és a végén true, finally blokkban).
5. CheckForIllegalCrossThreadCalls = false
Control.CheckForIllegalCrossThreadCalls = false; // „megoldás”Hiba és javítás
Elfedi, nem javítja a hibát: a program véletlenszerűen összeomolhat. Töröld, és használj Invoke/BeginInvoke vagy Progress<T> megoldást.
6. A DoEvents után eltűnt vezérlőt érünk el
A ciklus A NullReferenceException vagy ObjectDisposedException kivételt dob.Ok és javítás
DoEvents közben a felhasználó bezárta az ablakot. Ellenőrizd az IsDisposed értéket, vagy használj háttérszálat és megszakítást (11.8 lecke), és tiltsd a bezárást, amíg a munka fut.
Mini projekt: fagyás-labor késésmérővel
Készíts kísérleti programot, amely négyféle módon futtat ugyanazt a „munkát”, és méri, mennyire marad élő a felület:
- Felület: négy gomb (1. Blokkoló, 2. DoEvents, 3. Háttérszál, 4. Darabolt (Timer));
nudMp(a munka hossza, 1–10 mp); egy animáció: apnlAnimpanelen egy négyzet 20 ms-onként mozog (tmrAnim); egytxtGepelszövegmező (próbáld a munka közben gépelni);lblKeses,lblSzal;lstNaplo. - Késésmérő: a
tmrMero(Interval = 50) minden Tick-nél megméri az előző Tick óta eltelt időt (Stopwatch); a legnagyobb eltérést (legnagyobb késés) kiírja; a Nullázás gomb törli. Ez a szám mutatja: mennyi ideig nem „lélegzett” a felületi szál. - 1. Blokkoló: a munka (
Thread.Sleepdarabokban vagy egy ciklus) közvetlenül a kezelőben; a késés ≈ a munka hossza; az animáció áll, a gépelés nem látszik. - 2. DoEvents: a munka 100 ms-os darabokban, közben
Application.DoEvents(); az animáció mozog; afutoszámláló jelzi az újrabelépést, ha többször kattintanak (a gomb szándékosan nincs letiltva); a naplóban az ÚJRABELÉPÉS sor. - 3. Háttérszál: új
Thread(IsBackground = true), a munka a szálon; az eredményBeginInvokesegítségével a naplóba kerül; a szál azonosítója látszik (lblSzal: felületi és munkaszál); az animáció végig mozog; a késés kicsi. - 4. Darabolt (Timer): egy
Ticklegfeljebb 25 ms munkát végez (11.2 lecke); az animáció mozog; a késés kicsi; megszakítható. - Cross-thread kísérlet: a Közvetlen elérés szálról gomb új szálból közvetlenül módosít egy címkét; a kivétel (
InvalidOperationException) üzenete a naplóba kerül (a szálbólBeginInvokesegítségével), a program nem omlik össze. - Biztonság: a Form bezárása futó szál mellett nem hagy „szellem” folyamatot (
IsBackground); aBeginInvokehívásokObjectDisposedExceptionvédelme. - Teszteld: mind a négy változat 3 és 8 másodperccel, gépelés és ablakmozgatás közben, a legnagyobb késés értéke, dupla kattintás a DoEvents-nél, bezárás futás közben.
Megoldás megnyitása (próbáld ki előbb önállóan!)
A központi rész a késésmérő és a négyféle futtatás; a „munka” mindegyiknél ugyanaz: Munka(ms).
private int futo = 0;
private int felulet;
private Stopwatch meroOra = Stopwatch.StartNew();
private long utolsoTick = 0, legnagyobbKeses = 0;
private int negyzetX = 0, irany = 4;
private void Form1_Load(object sender, EventArgs e)
{
felulet = Environment.CurrentManagedThreadId;
lblSzal.Text = "Felületi szál: " + felulet;
tmrMero.Interval = 50;
tmrMero.Start();
tmrAnim.Interval = 20;
tmrAnim.Start();
}
// ---- késésmérő: az elvárt 50 ms-hoz képest mennyit késik a Tick ----
private void tmrMero_Tick(object sender, EventArgs e)
{
long most = meroOra.ElapsedMilliseconds;
long keses = most - utolsoTick - tmrMero.Interval;
utolsoTick = most;
if (keses > legnagyobbKeses) { legnagyobbKeses = keses; }
lblKeses.Text = "Legnagyobb késés: " + legnagyobbKeses + " ms";
}
private void btnNulla_Click(object sender, EventArgs e) { legnagyobbKeses = 0; lstNaplo.Items.Clear(); }
// ---- animáció: ha a felület szál foglalt, megáll ----
private void tmrAnim_Tick(object sender, EventArgs e)
{
negyzetX += irany;
if (negyzetX < 0 || negyzetX > pnlAnim.Width - 30) { irany = -irany; negyzetX += irany; }
pnlNegyzet.Left = negyzetX;
}
// ---- a „munka”: ugyanaz mindegyik változatnál ----
private void Munka(int ms)
{
Stopwatch sw = Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < ms) { Thread.Sleep(20); }
}
private int Hossz { get { return (int)nudMp.Value * 1000; } }
private void Naploz(string s) { lstNaplo.Items.Insert(0, DateTime.Now.ToString("HH:mm:ss") + " " + s); }
// 1. blokkoló: a felület szál dolgozik
private void btnBlokkolo_Click(object sender, EventArgs e)
{
Naploz("1. Blokkoló indul (szál " + Environment.CurrentManagedThreadId + ")");
Munka(Hossz);
Naploz("1. Blokkoló kész (a felület közben nem reagált)");
}
// 2. DoEvents: a felület él, de az újrabelépés veszélyes
private void btnDoEvents_Click(object sender, EventArgs e)
{
futo++;
if (futo > 1) { Naploz("ÚJRABELÉPÉS: már fut " + (futo - 1) + " példány!"); }
Stopwatch sw = Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < Hossz)
{
Thread.Sleep(20);
Application.DoEvents();
if (IsDisposed) { return; } // közben bezárhatták
}
Naploz("2. DoEvents kész (példányok: " + futo + ")");
futo--;
}
// 3. háttérszál
private void btnSzal_Click(object sender, EventArgs e)
{
int ms = Hossz;
Naploz("3. Háttérszál indul");
Thread szal = new Thread(() =>
{
int enSzalam = Environment.CurrentManagedThreadId;
Munka(ms);
try { BeginInvoke(new Action(() => Naploz("3. Háttérszál kész (munkaszál: " + enSzalam + ", felületi szál: " + felulet + ")"))); }
catch (ObjectDisposedException) { } // a Form közben bezárult
catch (InvalidOperationException) { }
});
szal.IsBackground = true;
szal.Start();
}
// 4. darabolt, időzítővel
private Stopwatch dMunka = new Stopwatch();
private void btnDarabolt_Click(object sender, EventArgs e)
{
if (tmrDarab.Enabled) { return; }
Naploz("4. Darabolt indul");
dMunka.Restart();
tmrDarab.Interval = 30;
tmrDarab.Start();
}
private void tmrDarab_Tick(object sender, EventArgs e)
{
Stopwatch keret = Stopwatch.StartNew();
while (keret.ElapsedMilliseconds < 25 && dMunka.ElapsedMilliseconds < Hossz) { Thread.Sleep(5); } // egy Tick legfeljebb ~25 ms
if (dMunka.ElapsedMilliseconds >= Hossz)
{
tmrDarab.Stop();
Naploz("4. Darabolt kész (a felület közben élt)");
}
}
// cross-thread kísérlet
private void btnCross_Click(object sender, EventArgs e)
{
Thread szal = new Thread(() =>
{
try { lblSzal.Text = "másik szálról"; }
catch (InvalidOperationException ex)
{
try { BeginInvoke(new Action(() => Naploz("Cross-thread kivétel: " + ex.Message))); }
catch (ObjectDisposedException) { }
}
});
szal.IsBackground = true;
szal.Start();
}Mit tegyél ide: a kész fagyás-labor a négy változat méréseivel.Javasolt fájlnév: winforms-11-3-kesz.webp · Alt szöveg: A kész fagyás-labor
Három önálló gyakorlófeladat
Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.
1. Melyik szálon vagyok?
Írj segédmetódust, amely kiírja, hogy az aktuális kód a felületi szálon fut-e. Mit használsz? A felületi szál azonosítóját a Egy lehetséges megoldás
Load-ban eltárolod, és összehasonlítod: Environment.CurrentManagedThreadId == felulet. Vezérlőre a InvokeRequired is megmondja.
2. Fagyás-érzékelő
A program naplózza, ha a felület több mint 200 ms-ig nem reagált. Hogyan? Egy 50 ms-os Egy lehetséges megoldás
Timer és a Stopwatch (a késésmérő): ha a Tick-ek közti idő > 250 ms, naplózod. Éles programban a naplóba az időpont és az utolsó művelet kerülhet.
3. Biztonságos címkefrissítő
Írj Az Szoveget(Label l, string s) segédmetódust, amely bármelyik szálról hívható. Mi a szerkezete?Egy lehetséges megoldás
InvokeRequired ellenőrzés, igaz esetén BeginInvoke a saját magát hívó metódusra; hamis esetén közvetlen értékadás. Ellenőrizd az IsHandleCreated és IsDisposed értéket is.private void Szoveget(Label l, string s){ if (l.IsDisposed) { return; } if (l.InvokeRequired) { try { l.BeginInvoke(new Action<Label, string>(Szoveget), l, s); } catch (ObjectDisposedException) { } catch (InvalidOperationException) { } // a fogantyú még nincs létrehozva vagy már megszűnt return; } l.Text = s;}
Gyakorlótár: 6 további feladat
Három szinten gyakorolhatsz: alap, közepes és kihívás. Csak a próbálkozás után nyisd le a megoldást.
Alap szint
1. Mi a felületi szál?
Fogalmazd meg egy mondatban, mi a felületi szál feladata. Az a szál, amely az üzenetsort feldolgozza: meghívja az eseménykezelőket és rajzol; a vezérlők csak erről a szálról érhetők el.Megoldás megnyitása
2. Mi okozza a (Nem válaszol) jelzést?
Mi történik a háttérben, hogy a Windows ezt kiírja? A felületi szál több másodpercig nem veszi ki az üzeneteket a sorból (hosszú eseménykezelő); a Windows ezt észleli.Megoldás megnyitása
Közepes szint
3. Mi nem megy másik szálon?
Melyik művelet tilos a háttérszálban: (a) számítás; (b) fájl olvasása; (c) (c): a vezérlő csak a felületi szálról érhető el. (a) és (b) szabad.Label.Text beállítása?Megoldás megnyitása
4. IsBackground
Mi a különbség az előtér- és a háttérszál között? A program addig fut, amíg előtér-szál van; a háttérszál a program bezárásakor automatikusan leáll. UI-programban a munkaszálak általában háttérszálak.Megoldás megnyitása
Kihívás szint
5. Invoke és BeginInvoke
Mi a különbség, és mikor okozhat az Az Invoke holtpontot?Megoldás megnyitása
Invoke megvárja a felületi szál végrehajtását, a BeginInvoke nem. Holtpont: ha a felületi szál épp a háttérszálra vár (például szal.Join()), a háttérszál pedig Invoke-ra vár: kölcsönösen várnak (11.4 és 11.7 lecke).
6. Hány szál?
Egy egyszerű Windows Forms program Feladatkezelőben látható szálszáma több, mint egy. Miért? A .NET futtatókörnyezet saját szálai (szemétgyűjtő, finalizer, szálkészlet) és a Windows belső szálai is megjelennek. A programod logikai felületi szála egy; a többi nem a te kódodat futtatja.Megoldás megnyitása
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. A Windows eseményeit (kattintás, rajzolás, időzítő) tároló sor; a felületi szál ciklusa dolgozza fel. A hosszú eseménykezelő foglalja a felületi szálat: nem rajzol és nem dolgozza fel a többi üzenetet. ~100 ms alatt észrevétlen, ~1 s fölött akadozás, 5 s fölött (Nem válaszol). Számítás, fájl, hálózat, saját adatok; vezérlőt csak a felületi szálról. A DoEvents közben az új kattintás ugyanazt a kezelőt újra elindítja a futó példány „alatt”. Elfedi a hibát, versenyhelyzetet és véletlenszerű összeomlást okozhat.1. Mi az üzenetsor?
2. Miért fagy le az ablak?
3. Mi a küszöb a lassúság érzetéhez?
4. Mit szabad a háttérszálban?
5. Mit okoz a másik szálról elért vezérlő?
InvalidOperationException (Cross-thread operation not valid).6. Mi az újrabelépés a DoEvents-nél?
7. Miért ne használd a CheckForIllegalCrossThreadCalls = false értéket?
Mit tudsz már?
- Érted az üzenetsort és azt, hogy minden eseménykezelő a felületi szálon fut.
- Tudod, miért fagy le az ablak, és mérni is tudod a felület késését.
- Ismered a szabályt: a vezérlők csak a létrehozó szálról érhetők el.
- Érted a
DoEventscsapdáit (újrabelépés). - Elindítasz egy háttérszálat, és az eredményt biztonságosan hazaküldöd.
Következő lecke: Thread.Sleep és a blokkolás problémája
A blokkoló hívások (Thread.Sleep, .Wait(), .Result) hatása a felületre, a holtpont és a nem blokkoló alternatívák.



