Grafikus C# – Windows Forms tanfolyam › 11.7 lecke Windows Forms · 11.7 lecke · felületfrissítés 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 UI-vezérlőelemek biztonságos frissítése
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.
A lecke végére
- használod az
InvokeésBeginInvokemetódust, és tudod a különbségüket; - az
InvokeRequiredmintá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.
Invoke és BeginInvoke
// 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 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 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.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
// 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!
}
}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.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.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ó.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:
// 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)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.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.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
// 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
}szamlalo++ több szálról elveszíti a növeléseket (11.6 lecke): Interlocked vagy lock.BeginInvoke-ot, ObjectDisposedException vagy InvalidOperationException keletkezik. Jelző + IsDisposed ellenőrzés + try–catch védi.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).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.
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. Az üzenetsor elárasztódott BeginInvoke hívásokkal. Gyűjtsd Ok és javítás
ConcurrentQueue-ba, és időzítővel, adagban ürítsd.
2. A haladás-jelentés összekeveredik
A Ugyanazt az objektumot módosítod és jelented. Minden Progress<T> hibás értékeket mutat.Ok és javítás
Report-hoz új példány.
3. A háttérszál megáll az Invoke-nál
A munka lassabb, ha naplóz. Az Ok és javítás
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 egyszerreHiba és javítás
Versenyhelyzet: Interlocked.Increment(ref db) vagy lock.
5. Kivétel a Form bezárásakor
Bezáráskor A háttérszál a megszűnt Formra hív BeginInvoke-ot. Jelző (ObjectDisposedException jelenik meg.Ok és javítás
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. Elemenkénti Ok és javítás
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:
- 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;lstEredmenya futások idejével. - A mód: minden sor külön
BeginInvoke(a védettBiztonsagosFrissitmetódussal); a felület elakad. - B mód:
Progress<string>soronként: ugyanaz a jelenség. - C mód:
ConcurrentQueueés 100 ms-osTimer; egy Tick legfeljebb 500 sort ürít (AddRange,BeginUpdate); a lista legfeljebb 2000 sort tart; a felület sima. - 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.
- Versenyhelyzet-kísérlet: négy
Taskegyenké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. - 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. - 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!)
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; } }
}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 Az UiBiztos(this Control c, Action a) segédmetódust.Egy lehetséges megoldás
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. A háttérszál Egy lehetséges megoldás
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 A 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
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? A hívó szál nem a vezérlő szála: a vezérlő eléréséhez Megoldás megnyitása
Invoke/BeginInvoke kell.
Közepes szint
3. Hol hozod létre a Progress-t?
Miért a felületi szálon? A létrehozás szálának környezetét jegyzi meg: így a visszahívás a felületi szálon fut.Megoldás megnyitása
4. Adagolás
Miért jobb az 500 soros adag a soronkénti frissítésnél? Egyetlen újrarajzolás 500 helyett; az üzenetsor nem árad el.Megoldás megnyitása
Kihívás szint
5. Termelő–fogyasztó
Mi a A fogyasztó blokkolva vár új elemre (BlockingCollection<T> előnye a ConcurrentQueue-val szemben?Megoldás megnyitása
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 Ha a felületi szál a háttérszálra vár (Invoke holtpontot?Megoldás megnyitása
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. Az A visszahívást a létrehozó (felületi) szálra küldi. A háttérszál gyorsabban küld frissítést, mint ahogy a felület feldolgozza. Szálbiztos sor + időzítő, adagolt frissítés, korlátozott megjelenítés. Hogy a háttérszál ne módosítsa a felület által még nem olvasott adatot.1. Mi a különbség az Invoke és a BeginInvoke között?
Invoke megvárja a végrehajtást, a BeginInvoke nem.2. Mit csinál a Progress<T>?
3. Miért árad el az üzenetsor?
4. Mi a megoldá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?
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.



