Grafikus C# – Windows Forms tanfolyam › 11.7 lecke

Windows Forms · 11.7 lecke · felületfrissítés

UI-vezérlőelemek biztonságos frissítése

A háttérmunka eredményét előbb-utóbb meg kell jeleníteni, de a vezérlők csak a felületi szálról érhetők el (11.3 lecke). Az await után ez automatikus; ha viszont a háttérszál közben kell frissítenie a felületet (haladás, naplósorok, részeredmények), saját eszköz kell: az Invoke és BeginInvoke, vagy a Progress<T>. Ebben a leckében megtanulod ezeket, és azt is, amit a kezdők elfelejtenek: a felület túlterhelése a túl sok frissítéssel, a közös adatok védelme, és a Form bezárása futó munka mellett.

  • kb. 100 perc
  • haladó szint
  • felületfrissítés
Diáklány C# programot készít laptopon

A lecke végére

  • használod az Invoke és BeginInvoke metódust, és tudod a különbségüket;
  • az InvokeRequired mintával szálbiztos segédmetódust írsz;
  • a Progress<T> segítségével összetett haladást jelentesz;
  • szabályozod a frissítések gyakoriságát (sorba állítás + időzítő), hogy a felület ne akadjon;
  • védetten kezeled a közös adatokat (lock, Interlocked, ConcurrentQueue);
  • a Form bezárásakor nem dobsz kivételt a futó munka miatt.

Mielőtt elkezded

Ehhez a leckéhez a 11.3 lecke (felületi szál), a 11.5–11.6 lecke (async, Task.Run) szükséges.

11.6 Háttérműveletek →

Invoke és BeginInvoke

Form1.csC#
// háttérszálból:
this.BeginInvoke(new Action(() => lblAllapot.Text = "Kész")); // a felületi szálra „küldi”, NEM vár
this.Invoke(new Action(() => lblAllapot.Text = "Kész")); // a felületi szálra küldi, és MEGVÁRJA a végrehajtást

// az InvokeRequired minta: ugyanaz a metódus bármelyik szálról hívható
private void Naploz(string uzenet)
{
if (this.IsDisposed) { return; }

if (this.InvokeRequired)
{
this.BeginInvoke(new Action<string>(Naploz), uzenet); // önmagát hívja a felületi szálon
return;
}

lstNaplo.Items.Insert(0, DateTime.Now.ToString("HH:mm:ss") + " " + uzenet);
}
Invoke: várAz Invoke addig blokkol, amíg a felületi szál végrehajtja a kódot. Eredményt is adhat. Holtpontveszély: ha a felületi szál éppen erre a háttérszálra vár (Join, .Result), kölcsönös várakozás lesz (11.4 lecke).
BeginInvoke: nem várA BeginInvoke csak sorba állítja a kódot a felületi szál üzenetsorában, és azonnal visszatér. Naplózáshoz, haladás-jelzéshez ez az ajánlott: a háttérmunka nem lassul. A sorrend megmarad.
InvokeRequiredIgaz, ha a hívó szál nem a vezérlő szála. A minta lehetővé teszi, hogy ugyanazt a metódust a felületi és a háttérszálból is hívd.
Mikor mégse kell?Az await utáni kód már a felületi szálon fut: ott nincs Invoke. Az Invoke/BeginInvoke a háttérszál közbeni frissítésre való.

Jósolj!

A háttérszál ezer Invoke hívást végez gyorsan egymás után, a felületi szál közben egy hosszú (3 mp-es) műveletet futtat. Mit tapasztalsz a háttérszálnál?

Jóslat ellenőrzése

A háttérszál az első Invoke-nál 3 másodpercig áll, mert megvárja a felületi szálat. Ezért jobb a BeginInvoke: a háttérmunka nem függ a felület pillanatnyi állapotától.

Progress<T>: haladás tetszőleges adattal

Form1.csC#
// a jelentés adata: bármilyen típus
private class Halad { public int Kesz; public int Osszes; public string Mostani = ""; }

private async void btnIndit_Click(object sender, EventArgs e)
{
// a FELÜLETI szálon létrehozva: a visszahívás is ott fut
Progress<Halad> jelent = new Progress<Halad>(h =>
{
pb.Maximum = h.Osszes;
pb.Value = Math.Min(h.Osszes, h.Kesz);
lblMostani.Text = h.Mostani;
});

await Task.Run(() => Feldolgoz(fajlok, jelent));
}

// a munka csak az IProgress felületet ismeri: nincs benne vezérlő, tesztelhető
private void Feldolgoz(string[] fajlok, IProgress<Halad> jelent)
{
for (int i = 0; i < fajlok.Length; i++)
{
Egyet(fajlok[i]);
jelent.Report(new Halad { Kesz = i + 1, Osszes = fajlok.Length, Mostani = fajlok[i] }); // új példány minden jelentésnél!
}
}
Mit csinál a Progress?A Progress<T> a létrehozás szálának SynchronizationContext környezetét jegyzi meg, és a Report hívásokat oda elküldi (Post, nem vár). Lényegében egy beépített BeginInvoke.
Új objektum minden jelentéshezA Report az objektum referenciáját küldi: ha a háttérszál ugyanazt a példányt módosítja tovább, a felület szál közben hibás állapotot olvashat. Minden jelentéshez új (vagy változatlan, record/struct) példányt adj.
TesztelhetőségA Feldolgoz nem ismeri a vezérlőket: csak az IProgress<T> felületet. Egységteszt helyettesíthet egy egyszerű gyűjtő osztályt, és a logika felület nélkül is kipróbálható.
Sorrend és késésA visszahívások sorrendje megmarad, de a háttérszál gyorsabban jelenthet, mint ahogy a felület feldolgozza: a sor megnőhet (lásd a következő rész).

Jósolj!

A háttérmunka egyetlen Halad példányt hoz létre, és minden lépésnél csak a Kesz mezőt növeli, majd Report-ot hív ugyanazzal a példánnyal. Mit lát a felület?

Jóslat ellenőrzése

Ugrálást: mire a felületi szál feldolgozza a sorban álló jelentést, a közös példány már továbbnőtt; a régi és az új jelentések ugyanazt a (legfrissebb) értéket mutatják, és közben a mezők összekeveredhetnek. Új példány minden jelentéshez.

A frissítések gyakoriságának szabályozása

Egy háttérszál másodpercenként százezer sort is előállíthat. Ha mindegyikre BeginInvoke hívás kerül, a felületi szál üzenetsora elárasztódik: a felület másodpercekig „csúszik”, a gombok késve reagálnak, a memória nő. Egy ember úgysem olvas 100 000 sort: gyűjtsd össze, és kevesebbszer, nagyobb adagban frissíts:

Form1.cs (using System.Collections.Concurrent;)C#
// a háttérszál egy szálbiztos sorba tesz; a felületi szál időzítővel üríti
private readonly ConcurrentQueue<string> naploSor = new ConcurrentQueue<string>();

// a HÁTTÉRSZÁL: gyors, nem érinti a felületet
private void Munka()
{
for (int i = 0; i < 200000; i++) { naploSor.Enqueue("sor " + i); }
}

// a FELÜLETI szál: 100 ms-onként egy adag (tmrNaplo.Interval = 100)
private void tmrNaplo_Tick(object sender, EventArgs e)
{
List<string> adag = new List<string>();
string sor;

while (adag.Count < 500 && naploSor.TryDequeue(out sor)) { adag.Add(sor); } // egy Tick legfeljebb 500 sort

if (adag.Count == 0) { return; }

lstNaplo.BeginUpdate();
lstNaplo.Items.AddRange(adag.ToArray()); // egyetlen frissítés, nem 500

while (lstNaplo.Items.Count > 2000) { lstNaplo.Items.RemoveAt(0); } // korlát: csak az utolsó 2000 sor látszik

lstNaplo.EndUpdate();
lstNaplo.TopIndex = lstNaplo.Items.Count - 1;
}

// az utolsó érték elve: csak a legfrissebb számít (például egy számláló)
private long utolsoSzam; // a háttérszál írja (Interlocked.Exchange), az időzítő olvassa (Interlocked.Read)
A felület nem tud lépést tartaniEgy ListBox.Items.Add hívás drága (újrarajzolás, görgetősáv). Százezer hívás percekig tart. Az adag (AddRange) és a BeginUpdate/EndUpdate ezt nagyságrenddel csökkenti.
ConcurrentQueueA ConcurrentQueue<T> szálbiztos: az egyik szál Enqueue-val tesz bele, a másik TryDequeue-val vesz ki, zár nélkül. A termelő–fogyasztó minta alapja.
Korlátozott megjelenítésA lista legfeljebb néhány ezer sort mutat: a régi sorokat eldobja. A teljes napló fájlba kerülhet (10.2 lecke).
Időzítős kiürítésA Timer 50–200 ms-onként ürít: a felhasználónak ez folyamatos frissítésnek tűnik, a felületi szál pedig bőven ráér a többi üzenetre.

Jósolj!

A háttérszál 200 000 sort ad át BeginInvoke(() => lstNaplo.Items.Add(sor)) hívásokkal. A háttérszál 1 másodperc alatt végez. Mikor lesz a felület újra használható?

Jóslat ellenőrzése

Csak percekkel később: a háttérszál gyorsan végzett, de a 200 000 sorban álló üzenetet a felületi szálnak kell feldolgozni (mindegyik egy drága Add). Eközben a felület alig reagál. A sorba állítás és az adagolt kiürítés ezt megoldja.

Közös adatok és a Form bezárása

Form1.cs (using System.Threading;)C#
// KÖZÖS ADAT: több szál ugyanazt írja → védelem kell
long szamlalo = 0;
object zar = new object();

Interlocked.Increment(ref szamlalo); // egyszerű számláló: atomi
lock (zar) { lista.Add(elem); osszeg += elem; } // összetett módosítás: kritikus szakasz

// LEÁLLÍTÁSI JELZŐ: volatile, hogy a másik szál lássa a változást
private volatile bool lezarva = false;

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
lezarva = true;
tmrNaplo.Stop();
}

private void BiztonsagosFrissit(Action muvelet)
{
if (lezarva || IsDisposed || !IsHandleCreated) { return; }

try { BeginInvoke(muvelet); }
catch (ObjectDisposedException) { } // a Form közben megszűnt
catch (InvalidOperationException) { } // a fogantyú még/már nincs meg
}
VersenyhelyzetA szamlalo++ több szálról elveszíti a növeléseket (11.6 lecke): Interlocked vagy lock.
Bezárás közbenHa a háttérszál a Form megszűnése után hív BeginInvoke-ot, ObjectDisposedException vagy InvalidOperationException keletkezik. Jelző + IsDisposed ellenőrzés + try–catch védi.
volatileA volatile biztosítja, hogy a másik szál a jelző friss értékét lássa (nem gyorsítótárból). Egyszerű igen/nem jelzőhöz elég; a szabványos megoldás a CancellationToken (11.8 lecke).
Kérdezz bezáráskorHa munka fut, a FormClosing rákérdezhet (Biztosan kilépsz?), és megszakíthatja a munkát, vagy e.Cancel = true értékkel megvárhatja.

Jósolj!

A háttérszál naplózza a haladást BeginInvoke hívásokkal, a felhasználó közben bezárja az ablakot. Mi történik, ha nincs védelem?

Jóslat ellenőrzése

A háttérszál kezeletlen kivételt kap (ObjectDisposedException), amely a Task-ban elvész, vagy a programot hibával leállítja. Védelem: jelző, IsDisposed, try–catch.

📷 Képernyőkép helye

Mit tegyél ide: a naplózó-labor három frissítési módja és a mért késések.Javasolt fájlnév: winforms-11-7-naplozo.webp · Alt szöveg: A naplózó-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. A felület másodpercekig lassú a naplózás után

A háttérszál rég végzett.

Ok és javítás

Az üzenetsor elárasztódott BeginInvoke hívásokkal. Gyűjtsd ConcurrentQueue-ba, és időzítővel, adagban ürítsd.

2. A haladás-jelentés összekeveredik

A Progress<T> hibás értékeket mutat.

Ok és javítás

Ugyanazt az objektumot módosítod és jelented. Minden Report-hoz új példány.

3. A háttérszál megáll az Invoke-nál

A munka lassabb, ha naplóz.

Ok és javítás

Az Invoke megvárja a felületi szálat. Használj BeginInvoke-ot vagy Progress<T>-t.

4. A szám nem stimmel

Task.Run(() => { for (int i = 0; i < 100000; i++) { db++; } });   // négy ilyen feladat egyszerre
Hiba és javítás

Versenyhelyzet: Interlocked.Increment(ref db) vagy lock.

5. Kivétel a Form bezárásakor

Bezáráskor ObjectDisposedException jelenik meg.

Ok és javítás

A háttérszál a megszűnt Formra hív BeginInvoke-ot. Jelző (lezarva), IsDisposed és try–catch; az időzítőt a FormClosing állítsa le.

6. A ListBox rendkívül lassú

Sok elem hozzáadása közben a felület akad.

Ok és javítás

Elemenkénti Add újrarajzolással. BeginUpdate/EndUpdate és AddRange, korlátozott elemszámmal.

Mini projekt: naplózó-labor

Készíts programot, amely háromféleképpen frissíti a naplót egy gyorsan termelő háttérszálból, és méri a felület késését:

  1. Felület: nudDb (1000–200 000), cmbMod (A. BeginInvoke soronként, B. Progress soronként, C. Sor + időzítő), Indítás, Mégse; lstNaplo; lblKeses (a legnagyobb késés, 11.3 lecke késésmérője), mozgó négyzet; lblDb; lstEredmeny a futások idejével.
  2. A mód: minden sor külön BeginInvoke (a védett BiztonsagosFrissit metódussal); a felület elakad.
  3. B mód: Progress<string> soronként: ugyanaz a jelenség.
  4. C mód: ConcurrentQueue és 100 ms-os Timer; egy Tick legfeljebb 500 sort ürít (AddRange, BeginUpdate); a lista legfeljebb 2000 sort tart; a felület sima.
  5. Mérés: a háttérmunka ideje, a felület „kiürülésének” ideje (amíg a sor üres), és a legnagyobb késés mindhárom módra; az eredmények a listában.
  6. Versenyhelyzet-kísérlet: négy Task egyenként 1 000 000 növelést végez közös számlálón: sima ++ (hibás összeg), Interlocked, lock; az eredmények és az idők.
  7. Biztonság: a Form bezárása futó munka mellett nem dob kivételt (jelző, IsDisposed, try–catch); a Mégse jelző megállítja a termelőt.
  8. Teszteld: 1000, 20 000 és 200 000 sor (A és B módnál óvatosan, 20 000-nél kezdve), a három mód késése, bezárás futás közben, a versenyhelyzet többszöri futtatása.
Megoldás megnyitása (próbáld ki előbb önállóan!)
Form1.cs (using System.Collections.Concurrent; using System.Diagnostics; using System.Threading; using System.Threading.Tasks;)C#
private volatile bool lezarva = false, leallit = false;
private ConcurrentQueue<string> sor = new ConcurrentQueue<string>();
private Stopwatch meroOra = Stopwatch.StartNew();
private long utolsoTick = 0, legnagyobb = 0;
private int futo = 0;

private void Form1_Load(object sender, EventArgs e)
{
tmrMero.Interval = 50; tmrMero.Start();
tmrNaplo.Interval = 100;
}

private void tmrMero_Tick(object sender, EventArgs e)
{
long most = meroOra.ElapsedMilliseconds;
long keses = most - utolsoTick - tmrMero.Interval;

utolsoTick = most;
if (keses > legnagyobb) { legnagyobb = keses; }
lblKeses.Text = "Legnagyobb késés: " + legnagyobb + " ms";
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e) { lezarva = true; leallit = true; tmrNaplo.Stop(); tmrMero.Stop(); }

private void BiztonsagosFrissit(Action muvelet)
{
if (lezarva || IsDisposed || !IsHandleCreated) { return; }

try { BeginInvoke(muvelet); }
catch (ObjectDisposedException) { }
catch (InvalidOperationException) { }
}

private async void btnIndit_Click(object sender, EventArgs e)
{
int db = (int)nudDb.Value, mod = cmbMod.SelectedIndex;

lstNaplo.Items.Clear();
legnagyobb = 0;
leallit = false;
sor = new ConcurrentQueue<string>();
btnIndit.Enabled = false;

Progress<string> halad = new Progress<string>(s => lstNaplo.Items.Add(s)); // a felületi szálon létrehozva
Stopwatch sw = Stopwatch.StartNew();
long termeles = 0;

try
{
if (mod == 2) { tmrNaplo.Start(); }

await Task.Run(() =>
{
for (int i = 0; i < db && !leallit; i++)
{
string s = "sor " + i;

if (mod == 0) { BiztonsagosFrissit(() => lstNaplo.Items.Add(s)); }
else if (mod == 1) { ((IProgress<string>)halad).Report(s); }
else { sor.Enqueue(s); }
}
});

termeles = sw.ElapsedMilliseconds;

// várjuk meg, amíg a felület feldolgozza az utolsó sort is
if (mod == 2) { while (!sor.IsEmpty && !lezarva) { await Task.Delay(50); } }
else { await Task.Delay(50); }

if (lezarva) { return; }

lstEredmeny.Items.Insert(0, cmbMod.Text + ": termelés " + termeles + " ms, kiürülés " + sw.ElapsedMilliseconds + " ms, legnagyobb késés " + legnagyobb + " ms");
}
finally
{
tmrNaplo.Stop();

if (!IsDisposed) { btnIndit.Enabled = true; }
}
}

private void tmrNaplo_Tick(object sender, EventArgs e)
{
List<string> adag = new List<string>();
string s;

while (adag.Count < 500 && sor.TryDequeue(out s)) { adag.Add(s); }

if (adag.Count == 0) { return; }

lstNaplo.BeginUpdate();
lstNaplo.Items.AddRange(adag.ToArray());

while (lstNaplo.Items.Count > 2000) { lstNaplo.Items.RemoveAt(0); }

lstNaplo.EndUpdate();
lstNaplo.TopIndex = lstNaplo.Items.Count - 1;
}

private void btnMegse_Click(object sender, EventArgs e) { leallit = true; }

// ---- versenyhelyzet ----
private async void btnVerseny_Click(object sender, EventArgs e)
{
btnVerseny.Enabled = false;

try
{
long sima = 0, atomi = 0, zart = 0;
object zar = new object();
Stopwatch sw = Stopwatch.StartNew();

await Task.WhenAll(Enumerable.Range(0, 4).Select(k => Task.Run(() => { for (int i = 0; i < 1000000; i++) { sima++; } })));
long t1 = sw.ElapsedMilliseconds; sw.Restart();
await Task.WhenAll(Enumerable.Range(0, 4).Select(k => Task.Run(() => { for (int i = 0; i < 1000000; i++) { Interlocked.Increment(ref atomi); } })));
long t2 = sw.ElapsedMilliseconds; sw.Restart();
await Task.WhenAll(Enumerable.Range(0, 4).Select(k => Task.Run(() => { for (int i = 0; i < 1000000; i++) { lock (zar) { zart++; } } })));
long t3 = sw.ElapsedMilliseconds;

lstEredmeny.Items.Insert(0, "Várt: 4 000 000 | sima ++: " + sima + " (" + t1 + " ms) | Interlocked: " + atomi + " (" + t2 + " ms) | lock: " + zart + " (" + t3 + " ms)");
}
finally { if (!IsDisposed) { btnVerseny.Enabled = true; } }
}
📷 Képernyőkép helye

Mit tegyél ide: a kész naplózó-labor a három mód méréseivel.Javasolt fájlnév: winforms-11-7-kesz.webp · Alt szöveg: A kész naplózó-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. Extension metódus

Írj UiBiztos(this Control c, Action a) segédmetódust.

Egy lehetséges megoldás

Az InvokeRequired igaz esetén BeginInvoke(a), különben a(); védd IsDisposed és try–catch blokkal.

2. Utolsó érték

A háttérszál ezerszer frissít egy számot, a címke 10 Hz-en mutassa a legfrissebbet.

Egy lehetséges megoldás

A háttérszál Interlocked.Exchange(ref ertek, x), a 100 ms-os Timer olvassa (Interlocked.Read), és kiírja.

3. Haladás százalékban és szövegben

Egy record típus hordozza a százalékot és a szöveget. Miért jobb a class módosítható mezőinél?

Egy lehetséges megoldás

A record (vagy readonly struct) változatlan: a küldés után a háttérszál nem módosíthatja, így a felület mindig konzisztens adatot lát.

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. Invoke vagy BeginInvoke?

Melyiket választod naplózáshoz, és miért?

Megoldás megnyitása

BeginInvoke: nem várja meg a felületet, a háttérmunka nem lassul.

2. InvokeRequired

Mit jelent az igaz érték?

Megoldás megnyitása

A hívó szál nem a vezérlő szála: a vezérlő eléréséhez Invoke/BeginInvoke kell.

Közepes szint

3. Hol hozod létre a Progress-t?

Miért a felületi szálon?

Megoldás megnyitása

A létrehozás szálának környezetét jegyzi meg: így a visszahívás a felületi szálon fut.

4. Adagolás

Miért jobb az 500 soros adag a soronkénti frissítésnél?

Megoldás megnyitása

Egyetlen újrarajzolás 500 helyett; az üzenetsor nem árad el.

Kihívás szint

5. Termelő–fogyasztó

Mi a BlockingCollection<T> előnye a ConcurrentQueue-val szemben?

Megoldás megnyitása

A fogyasztó blokkolva vár új elemre (Take), és korlátozható a sor mérete: a gyors termelő nem tölti meg a memóriát. A felületi szálon ne használd blokkolva; háttérfogyasztónál kényelmes.

6. Holtpont Invoke-kal

Mikor okoz az Invoke holtpontot?

Megoldás megnyitása

Ha a felületi szál a háttérszálra vár (Join, .Result), a háttérszál pedig Invoke-kal a felületi szálra: kölcsönös várakozás.

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Mi a különbség az Invoke és a BeginInvoke között?

Az Invoke megvárja a végrehajtást, a BeginInvoke nem.

2. Mit csinál a Progress<T>?

A visszahívást a létrehozó (felületi) szálra küldi.

3. Miért árad el az üzenetsor?

A háttérszál gyorsabban küld frissítést, mint ahogy a felület feldolgozza.

4. Mi a megoldás?

Szálbiztos sor + időzítő, adagolt frissítés, korlátozott megjelenítés.

5. Mit használsz közös számlálóhoz?

Interlocked.Increment (vagy lock).

6. Mi történik a bezárt Formra küldött BeginInvoke-kal?

ObjectDisposedException vagy InvalidOperationException: jelző és védelem kell.

7. Miért új példány minden Report-hoz?

Hogy a háttérszál ne módosítsa a felület által még nem olvasott adatot.

Mit tudsz már?

  • Biztonságosan frissíted a felületet háttérszálból (BeginInvoke, Progress<T>).
  • Szabályozod a frissítések gyakoriságát (sor + időzítő).
  • Védetten kezeled a közös adatokat.
  • A Form bezárásakor nem dobsz kivételt a futó munka miatt.

Következő lecke: CancellationToken és megszakítás

A szabványos megszakítás: CancellationTokenSource, időtúllépés, OperationCanceledException, Mégse gomb.