Grafikus C# – Windows Forms tanfolyam › 11.8 lecke Windows Forms · 11.8 lecke · megszakítás Minden hosszú műveletnek kell egy Mégse gomb: a felhasználó elindította, rájött, hogy rossz mappát választott, és nem akar percekig várni. A megszakításhoz nem jó a szál erőszakos leállítása (CancellationToken és megszakítás
Thread.Abort: elavult, veszélyes). A .NET szabványos megoldása az együttműködő megszakítás: a hívó kéri a leállítást a CancellationToken segítségével, a munka ellenőrzi a kérést, rendet tesz maga után, és kilép. Ebben a leckében megtanulod a CancellationTokenSource és a CancellationToken használatát, az OperationCanceledException kezelését, az időtúllépést (CancelAfter), a Mégse gomb helyes állapotkezelését és a megszakítást a Form bezárásakor.
A lecke végére
- létrehozol és megszakítasz
CancellationTokenSource-t, a tokent átadod a munkának; - a munka ellenőrzi a kérést (
IsCancellationRequested,ThrowIfCancellationRequested) és kilép; - helyesen kezeled az
OperationCanceledExceptionkivételt (a hiba és a megszakítás nem ugyanaz); - időtúllépést állítasz be (
CancelAfter), és több forrást kapcsolsz össze (CreateLinkedTokenSource); - a Mégse gomb és a gombállapotok helyesen működnek, a
CancellationTokenSource-t felszabadítod; - a Form bezárásakor megszakítod a futó munkát.
Mielőtt elkezded
Ehhez a leckéhez a 11.5–11.7 lecke (async, Task.Run, Progress<T>) és a 4.7 lecke (kivételkezelés) szükséges.
CancellationTokenSource és CancellationToken
Két objektum dolgozik együtt: a CancellationTokenSource (forrás) a kérés kiadója (Cancel()); a CancellationToken a forrásból kapott jelző, amelyet átadsz a munkának: az csak olvassa (nem tudja megszakítani önmagát). A munka feladata, hogy rendszeresen ellenőrizze:
private CancellationTokenSource forras; // az éppen futó művelet forrása
private async void btnIndit_Click(object sender, EventArgs e)
{
forras = new CancellationTokenSource();
CancellationToken token = forras.Token;
try
{
long db = await Task.Run(() => Szamol(5000000, token), token);
lblAllapot.Text = "Kész: " + db;
}
catch (OperationCanceledException)
{
lblAllapot.Text = "Megszakítva."; // ez NEM hiba
}
finally
{
forras.Dispose();
forras = null;
}
}
private void btnMegse_Click(object sender, EventArgs e)
{
if (forras != null) { forras.Cancel(); } // csak KÉRI a leállítást
}
// a munka: rendszeresen ellenőrzi a tokent
private long Szamol(int hatar, CancellationToken token)
{
long db = 0;
for (int n = 2; n <= hatar; n++)
{
if (n % 1000 == 0) { token.ThrowIfCancellationRequested(); } // nem minden lépésnél: olcsó, de nem ingyen
if (Prim(n)) { db++; }
}
return db;
}Cancel() nem állít le semmit: csak beállítja a token jelzőjét. A munkának kell figyelnie és kilépnie. Ha a munka sosem ellenőrzi, a Mégse hatástalan.IsCancellationRequested (igaz/hamis) lehetővé teszi a saját kilépést (takarítás, részeredmény). A ThrowIfCancellationRequested() OperationCanceledException kivételt dob, és a hívási láncon kilép.Task.Run(..., token) második paramétere még az indulás előtt megszakítja a Task-ot (ha a token már megszakított); a futó munkát a lambdán belüli ellenőrzés állítja le. Mindkettő kell.Jósolj!
A Szamol metódus nem ellenőrzi a tokent. A felhasználó a Mégse gombra kattint. Mi történik?
Jóslat ellenőrzése
Semmi: a Cancel() csak a jelzőt állítja, és a munka a végéig fut. A megszakítás együttműködésen alapul. Ezért a hosszú ciklusokban rendszeres ellenőrzés kell.
Az OperationCanceledException: nem hiba
try
{
await FeldolgozAsync(token);
lblAllapot.Text = "Kész.";
}
catch (OperationCanceledException) // ELŐBB a megszakítás (a TaskCanceledException is ennek leszármazottja)
{
lblAllapot.Text = "Megszakítva.";
}
catch (IOException ex) // aztán a valódi hibák
{
lblAllapot.Text = "Hiba: " + ex.Message;
}
// aszinkron beépített metódusok is elfogadnak tokent: ők maguk kilépnek
string s = await File.ReadAllTextAsync(ut, token);
await Task.Delay(2000, token);
string w = await kliens.GetStringAsync(url, token); // újabb .NET: GetStringAsync(url, token)
// takarítás megszakításkor: finally vagy using (nem a catch)
using (StreamWriter iro = new StreamWriter(kimenet))
{
foreach (string sor in sorok)
{
token.ThrowIfCancellationRequested();
iro.WriteLine(sor);
}
} // a using a kivételnél is lezárja a fájltOperationCanceledException a felhasználó szándéka, nem hibajelenség. Külön catch kell neki, a többi előtt (különben egy általános catch (Exception) hibaként jelezné).Task-ok megszakításakor gyakran TaskCanceledException érkezik: az OperationCanceledException leszármazottja, ezért az OperationCanceledException elkapja....Async metódus (Task.Delay, ReadAllTextAsync, HttpClient) fogad tokent, és megszakításkor magától kivételt dob.using), félig írt kimeneti fájl (törlés vagy jelölés), a gombok állapota (finally).Jósolj!
A kód catch (Exception ex) { MessageBox.Show("Hiba: " + ex.Message); } blokkot tartalmaz, és nincs külön OperationCanceledException ág. Mit lát a felhasználó a Mégse után?
Jóslat ellenőrzése
Egy hibaüzenetet (A művelet megszakadt.), pedig a felhasználó maga kérte a megszakítást: félrevezető. Az OperationCanceledException-nek külön ága kell, a hibaüzenet előtt.
Időtúllépés és összekapcsolt tokenek
// időtúllépés: a forrás maga megszakít 5 másodperc után
using (CancellationTokenSource idozito = new CancellationTokenSource())
{
idozito.CancelAfter(TimeSpan.FromSeconds(5));
try { await LetoltAsync(url, idozito.Token); }
catch (OperationCanceledException) { tsslUzenet.Text = "Időtúllépés: 5 másodperc alatt nem készült el."; }
}
// ÖSSZEKAPCSOLÁS: a Mégse gomb ÉS az időtúllépés is megszakíthat
using (CancellationTokenSource felhasznalo = new CancellationTokenSource())
using (CancellationTokenSource ido = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
using (CancellationTokenSource egyutt = CancellationTokenSource.CreateLinkedTokenSource(felhasznalo.Token, ido.Token))
{
try { await FeldolgozAsync(egyutt.Token); }
catch (OperationCanceledException)
{
// ki szakította meg? a felhasználó vagy az idő?
tsslUzenet.Text = ido.IsCancellationRequested ? "Időtúllépés (30 mp)." : "Megszakítva.";
}
}CancelAfter (vagy a konstruktor időparamétere) beállítja, hogy a forrás maga megszakítson. Egyszerűbb, mint a WhenAny + Delay minta (11.5 lecke), mert a munka ténylegesen leáll, nem csak „nem várunk rá”.CreateLinkedTokenSource több forrásból egyet készít: bármelyik megszakítása megszakítja az összekapcsoltat. Így a Mégse és az időtúllépés együtt kezelhető.IsCancellationRequested tulajdonsága megmondja az okot: eltérő üzenet jár az időtúllépésnek és a felhasználói kérésnek.CancellationTokenSource IDisposable: using vagy Dispose(). Különösen az időzítős és összekapcsolt források tartanak erőforrást.Jósolj!
Az idozito.CancelAfter(5000) után a munka 3 másodperc alatt befejeződik, a using blokk lezárul. Mi történik 5 másodperc múlva?
Jóslat ellenőrzése
Semmi: a Dispose leállítja az időzítőt, így nem keletkezik „késői” megszakítás. Ezért kell felszabadítani a forrást.
A Mégse gomb és a bezárás
private enum Allapot { Kesz, Fut, Megszakitas }
private void Beallit(Allapot a)
{
btnIndit.Enabled = a == Allapot.Kesz;
btnMegse.Enabled = a == Allapot.Fut; // a megszakítás alatt már nem kattintható újra
lblAllapot.Text = a == Allapot.Megszakitas ? "Megszakítás folyamatban..." : lblAllapot.Text;
}
private void btnMegse_Click(object sender, EventArgs e)
{
Beallit(Allapot.Megszakitas);
forras?.Cancel();
}
// bezáráskor: kérjük a megszakítást, és megvárjuk a befejezést
private bool bezarasra = false;
private async void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (forras != null && !bezarasra)
{
e.Cancel = true; // most nem zárjuk be
bezarasra = true;
forras.Cancel();
while (forras != null) { await Task.Delay(50); } // a munka finally ága kinullázza a forrást
this.Close(); // most már bezárható
}
}FormClosing megszakítja a futó munkát, és e.Cancel = true értékkel megvárja a befejezést; csak utána zár be. Így nincs szellemfolyamat és nincs kivétel a megszűnt Formon.forras?.Cancel() védi, ha nincs futó művelet. A finally ág nullázza a mezőt, hogy a bezáráskor lehessen várni.Thread.Abort) elavult és veszélyes (zárolt erőforrások, félkész állapot); a modern .NET-ben nem is támogatott. Mindig együttműködő megszakítás.Jósolj!
A Mégse gomb kétszeri gyors kattintására a forras.Cancel() kétszer fut le. Baj-e?
Jóslat ellenőrzése
A Cancel() többszöri hívása ártalmatlan (idempotens). Baj akkor van, ha közben a finally már felszabadította a forrást: ObjectDisposedException. Ezért tiltjuk le a gombot, és nullázzuk a mezőt.
Mit tegyél ide: a megszakítható feldolgozó: futás közben, a Megszakítás folyamatban... állapottal és az időtúllépés üzenetével.Javasolt fájlnév: winforms-11-8-megszakitas.webp · Alt szöveg: A megszakítható feldolgozó
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 Mégse gomb nem állítja le a munkát
A gomb A munka nem ellenőrzi a tokent. Adj Cancel()-t hív, a feldolgozás tovább fut.Ok és javítás
token paramétert, és rendszeresen hívd a ThrowIfCancellationRequested() metódust.
2. A megszakítás hibaüzenetként jelenik meg
catch (Exception ex) { MessageBox.Show(ex.Message); }Hiba és javítás
Külön catch (OperationCanceledException) ág kell, a catch (Exception) előtt.
3. ObjectDisposedException a Mégse után
forras.Dispose(); // a finally-ban
// ... később a Mégse gomb: forras.Cancel();Hiba és javítás
A felszabadított forráson hívták a Cancel-t. A finally nullázza a mezőt (forras = null), a gomb forras?.Cancel(), és a Mégse futás után tiltott.
4. A második indítás azonnal megszakad
Megszakítás után az újraindított munka rögtön leáll. A régi, már megszakított forrást használja újra. A megszakított token végleg megszakított: minden indításhoz új Ok és javítás
CancellationTokenSource kell.
5. Az időtúllépés után is fut a feladat
Task elso = await Task.WhenAny(feladat, Task.Delay(3000));Hiba és javítás
A WhenAny csak a várást fejezi be, a feladat tovább fut. Valódi leállításhoz a feladat kapjon tokent (CancelAfter), és figyelje.
6. A bezárás közben kivétel jelenik meg
A Form bezárásakor a háttérmunka kivételt dob. A munka nem állt le a Form megszűnése előtt. A Ok és javítás
FormClosing kérje a megszakítást (Cancel), és e.Cancel = true mellett várja meg a befejezést.
Mini projekt: megszakítható feldolgozó
Készíts programot, amely szöveges fájlokat dolgoz fel háttérben, és bármikor megszakítható:
- Felület:
txtMappa, Tallózás... (10.4 lecke),nudIdo(időtúllépés másodpercben, 0 = nincs), Indítás, Mégse;pb,lblAllapot,lstEredmeny; állapotsor. - Munka: a mappa
.txtfájljait soronként elemzi (sorok, szavak és karakterek száma fájlonként); minden fájl végén és 2000 soronként ellenőrzi a tokent; az eredményList<FajlStat>. - Megszakítás: egy
CancellationTokenSourceindításonként; a MégseCancel(); három állapot (Kesz,Fut,Megszakitas); a Megszakítás folyamatban... szöveg. - Időtúllépés: a
CancelAfterés az összekapcsolt token; az üzenet megmondja, ki szakította meg (Időtúllépés vagy Megszakítva). - Részeredmény: megszakításkor a már kész fájlok eredménye látszik (Megszakítva: 14 / 40 fájl kész); a háttérszál a listát a végén adja át (szálbiztos).
- Kivételek: külön
OperationCanceledExceptionág; a nem olvasható fájl (IOException,UnauthorizedAccessException) kihagyva, a hiba az állapotsorban és a napló végén összegezve (a feldolgozás tovább fut). - Bezárás: a
FormClosingmegszakítja a futó munkát és megvárja; nincs kivétel. - Teszteld: kis és nagy mappa, megszakítás az elején, közepén és a végén, időtúllépés 1 mp-pel, dupla kattintás a Mégse gombra, újraindítás megszakítás után (új forrás), bezárás futás közben, zárolt vagy hozzáférés nélküli fájl.
Megoldás megnyitása (próbáld ki előbb önállóan!)
private class FajlStat { public string Nev; public int Sorok, Szavak; public long Karakterek; }
private CancellationTokenSource forras;
private bool bezarasra = false;
private enum Allapot { Kesz, Fut, Megszakitas }
private void Beallit(Allapot a)
{
btnIndit.Enabled = a == Allapot.Kesz;
btnMegse.Enabled = a == Allapot.Fut;
txtMappa.Enabled = a == Allapot.Kesz;
nudIdo.Enabled = a == Allapot.Kesz;
if (a == Allapot.Megszakitas) { lblAllapot.Text = "Megszakítás folyamatban..."; }
}
private async void btnIndit_Click(object sender, EventArgs e)
{
string mappa = txtMappa.Text;
if (!Directory.Exists(mappa)) { tsslUzenet.Text = "A mappa nem létezik."; return; }
int idoMp = (int)nudIdo.Value;
List<FajlStat> kesz = new List<FajlStat>();
List<string> hibak = new List<string>();
lstEredmeny.Items.Clear();
pb.Value = 0;
Beallit(Allapot.Fut);
lblAllapot.Text = "Feldolgozás...";
CancellationTokenSource felhasznalo = new CancellationTokenSource();
CancellationTokenSource ido = idoMp > 0 ? new CancellationTokenSource(TimeSpan.FromSeconds(idoMp)) : new CancellationTokenSource();
CancellationTokenSource egyutt = CancellationTokenSource.CreateLinkedTokenSource(felhasznalo.Token, ido.Token);
forras = felhasznalo; // a Mégse ezt szakítja meg (az összekapcsolt is megszakad)
Progress<int> halad = new Progress<int>(p => pb.Value = Math.Min(100, p));
string uzenet;
try
{
CancellationToken token = egyutt.Token;
await Task.Run(() =>
{
string[] fajlok = Directory.GetFiles(mappa, "*.txt", SearchOption.TopDirectoryOnly);
for (int i = 0; i < fajlok.Length; i++)
{
token.ThrowIfCancellationRequested();
try { kesz.Add(Elemez(fajlok[i], token)); }
catch (IOException ex) { hibak.Add(Path.GetFileName(fajlok[i]) + ": " + ex.Message); }
catch (UnauthorizedAccessException ex) { hibak.Add(Path.GetFileName(fajlok[i]) + ": " + ex.Message); }
((IProgress<int>)halad).Report((i + 1) * 100 / fajlok.Length);
}
}, token);
uzenet = "Kész: " + kesz.Count + " fájl feldolgozva" + (hibak.Count > 0 ? ", " + hibak.Count + " hibával." : ".");
}
catch (OperationCanceledException)
{
uzenet = (ido.IsCancellationRequested ? "Időtúllépés (" + idoMp + " mp)" : "Megszakítva") + ": " + kesz.Count + " fájl kész.";
}
catch (Exception ex)
{
uzenet = "Hiba: " + ex.Message;
}
finally
{
egyutt.Dispose(); ido.Dispose(); felhasznalo.Dispose();
forras = null;
}
if (IsDisposed) { return; }
// a háttérszál már befejeződött: a listák biztonságosan olvashatók
foreach (FajlStat f in kesz) { lstEredmeny.Items.Add(f.Nev + ": " + f.Sorok + " sor, " + f.Szavak + " szó, " + f.Karakterek + " karakter"); }
foreach (string h in hibak) { lstEredmeny.Items.Add("⚠ " + h); }
lblAllapot.Text = uzenet;
tsslUzenet.Text = uzenet;
Beallit(Allapot.Kesz);
}
private FajlStat Elemez(string ut, CancellationToken token)
{
FajlStat f = new FajlStat { Nev = Path.GetFileName(ut) };
using (StreamReader olvaso = new StreamReader(ut))
{
string sor;
while ((sor = olvaso.ReadLine()) != null)
{
f.Sorok++;
f.Karakterek += sor.Length;
f.Szavak += sor.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries).Length;
if (f.Sorok % 2000 == 0) { token.ThrowIfCancellationRequested(); }
}
}
return f;
}
private void btnMegse_Click(object sender, EventArgs e)
{
Beallit(Allapot.Megszakitas);
if (forras != null) { forras.Cancel(); }
}
private async void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (forras != null && !bezarasra)
{
e.Cancel = true;
bezarasra = true;
forras.Cancel();
while (forras != null) { await Task.Delay(50); }
Close();
}
}Mit tegyél ide: a kész megszakítható feldolgozó, megszakítás után a részeredménnyel.Javasolt fájlnév: winforms-11-8-kesz.webp · Alt szöveg: A kész megszakítható feldolgozó
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. Megszakítható visszaszámlálás
Egy 10 másodperces visszaszámlálás A await Task.Delay(1000, token) hívásokkal. Mi történik megszakításkor?Egy lehetséges megoldás
Task.Delay azonnal TaskCanceledException kivételt dob; a catch (OperationCanceledException) kiírja: Megszakítva. Nincs másodperces késés.
2. Keresés megszakítással
A 11.6 lecke nagy fájl keresése kapjon Mégse gombot. Mit kell változtatni? A Egy lehetséges megoldás
KeresFajlban kap CancellationToken paramétert, a ciklusban 5000 soronként ThrowIfCancellationRequested(); a hívó CancellationTokenSource-t hoz létre, és az OperationCanceledException-t külön kezeli (részeredmény: a már megtalált sorok).
3. Újrapróbálás megszakítással
Egy művelet hiba esetén háromszor újrapróbálkozik (10.9 lecke). Hogyan hat rá a megszakítás? A várakozás Egy lehetséges megoldás
await Task.Delay(ms, token), és az OperationCanceledException-t nem kell újrapróbálni: a catch blokkban throw;-ral továbbadjuk, vagy a szűrő (catch (IOException) when (!token.IsCancellationRequested)) kihagyja.
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. Forrás és token
Melyik objektum kérheti a megszakítást, és melyik csak ellenőrizhet? A Megoldás megnyitása
CancellationTokenSource kéri (Cancel), a CancellationToken csak ellenőriz.
2. Mit csinál a Cancel?
Leállítja a munkát? Nem: csak jelez. A munkának kell ellenőriznie és kilépnie.Megoldás megnyitása
Közepes szint
3. Catch sorrend
Hová kerül az Az általános OperationCanceledException ága?Megoldás megnyitása
catch (Exception) elé (és minden szélesebb típus elé), különben hibaként jelenik meg.
4. Újrafelhasználás
Használhatod-e újra a megszakított forrást? Nem: egy megszakított token végleg megszakított. Minden indításhoz új forrás kell.Megoldás megnyitása
Kihívás szint
5. Ki szakította meg?
Hogyan döntöd el összekapcsolt tokennél, hogy időtúllépés vagy felhasználó volt? Az eredeti források Megoldás megnyitása
IsCancellationRequested tulajdonságából: ha az időzítős igaz, időtúllépés; különben a felhasználó. (Ha mindkettő, az időtúllépést jelezzük.)
6. Regisztráció
Mire jó a Visszahívást regisztrál a megszakítás pillanatára: például egy blokkoló művelet (token.Register(() => ...)?Megoldás megnyitása
Process.Kill, socket bezárása) leállítására, amely nem fogad tokent. A visszahívás gyors legyen, és a regisztrációt szabadítsd fel (using).
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. A munka maga ellenőrzi a tokent és rendet tesz; nincs erőszakos leállítás. Nem: a felhasználó szándéka; külön ág. Időtúllépésre: a forrás maga megszakít. Több forrás (Mégse, időtúllépés, bezárás) egyetlen tokenné kapcsolására. Az időzítőt és regisztrációkat felszabadítja; felszabadítás után nem hívható a Megszakítod a munkát, 1. Miért együttműködő a megszakítás?
2. Mi a két ellenőrzési mód?
IsCancellationRequested (saját kilépés) és ThrowIfCancellationRequested (kivétel).3. Hibának számít az OperationCanceledException?
4. Mire jó a CancelAfter?
5. Mire jó a CreateLinkedTokenSource?
6. Miért kell Dispose?
Cancel.7. Mit teszel bezáráskor?
e.Cancel = true mellett megvárod a befejezést, és csak utána zársz be.
Mit tudsz már?
- Együttműködő megszakítást írsz
CancellationTokensegítségével. - Külön kezeled a megszakítást és a hibát.
- Időtúllépést állítasz be, és több forrást kapcsolsz össze.
- A Mégse gomb állapotait és a bezárást helyesen kezeled.
Következő lecke: Mini projekt: folyamatkijelző
Az egész fejezet összefoglalása egy záró projektben: kétfázisú, haladást jelző, megszakítható, szüneteltethető feldolgozó.



