Grafikus C# – Windows Forms tanfolyam › 11.8 lecke

Windows Forms · 11.8 lecke · megszakítás

CancellationToken és 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 (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.

  • kb. 100 perc
  • haladó szint
  • megszakítás
Diák C# programozást gyakorol egy tanulósarokban

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 OperationCanceledException kivé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.

11.7 UI frissítés →

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:

Form1.cs (using System.Threading; using System.Threading.Tasks;)C#
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;
}
Együttműködő megszakításA 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.
Két ellenőrzési módAz 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.
Milyen sűrűn?Az ellenőrzés olcsó, de nem ingyenes: elég néhány ezred másodpercenként (például ciklusonként minden 1000. lépésben). Egy lépés ne tartson tovább, mint amennyit a felhasználó hajlandó megvárni.
A token átadásaA 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

Form1.csC#
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ájlt
Megszakítás ≠ hibaAz OperationCanceledException 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é).
TaskCanceledExceptionA Task-ok megszakításakor gyakran TaskCanceledException érkezik: az OperationCanceledException leszármazottja, ezért az OperationCanceledException elkapja.
Beépített támogatásA legtöbb ...Async metódus (Task.Delay, ReadAllTextAsync, HttpClient) fogad tokent, és megszakításkor magától kivételt dob.
TakarításA megszakításkor a félkész állapotot rendbe kell tenni: nyitott fájlok (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

Form1.csC#
// 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.";
}
}
CancelAfterA 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á”.
ÖsszekapcsolásA 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ő.
Ki szakította meg?Az eredeti források 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.
DisposeA 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

Form1.csC#
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ó
}
}
Három állapotFutás közben az Indít tiltott, a Mégse engedélyezett; a megszakítás kérése után a Mégse is tiltott (nem kattintható kétszer), a felirat Megszakítás folyamatban..., mert a munka kilépése időbe telik.
BezárásA 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.
Null ellenőrzésA 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.
Nincs Thread.AbortA szál erőszakos leállítása (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.

📷 Képernyőkép helye

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 Cancel()-t hív, a feldolgozás tovább fut.

Ok és javítás

A munka nem ellenőrzi a tokent. Adj 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.

Ok és javítás

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 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.

Ok és javítás

A munka nem állt le a Form megszűnése előtt. A 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ó:

  1. 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.
  2. Munka: a mappa .txt fá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ény List<FajlStat>.
  3. Megszakítás: egy CancellationTokenSource indításonként; a Mégse Cancel(); három állapot (Kesz, Fut, Megszakitas); a Megszakítás folyamatban... szöveg.
  4. 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).
  5. 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).
  6. 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).
  7. Bezárás: a FormClosing megszakítja a futó munkát és megvárja; nincs kivétel.
  8. 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!)
Form1.cs (using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic;)C#
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();
}
}
📷 Képernyőkép helye

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 await Task.Delay(1000, token) hívásokkal. Mi történik megszakításkor?

Egy lehetséges megoldás

A 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?

Egy lehetséges megoldás

A 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?

Egy lehetséges megoldás

A várakozá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?

Megoldás megnyitása

A CancellationTokenSource kéri (Cancel), a CancellationToken csak ellenőriz.

2. Mit csinál a Cancel?

Leállítja a munkát?

Megoldás megnyitása

Nem: csak jelez. A munkának kell ellenőriznie és kilépnie.

Közepes szint

3. Catch sorrend

Hová kerül az OperationCanceledException ága?

Megoldás megnyitása

Az általános 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?

Megoldás megnyitása

Nem: egy megszakított token végleg megszakított. Minden indításhoz új forrás kell.

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?

Megoldás megnyitása

Az eredeti források 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 token.Register(() => ...)?

Megoldás megnyitása

Visszahívást regisztrál a megszakítás pillanatára: például egy blokkoló művelet (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.

1. Miért együttműködő a megszakítás?

A munka maga ellenőrzi a tokent és rendet tesz; nincs erőszakos leállí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?

Nem: a felhasználó szándéka; külön ág.

4. Mire jó a CancelAfter?

Időtúllépésre: a forrás maga megszakít.

5. Mire jó a CreateLinkedTokenSource?

Több forrás (Mégse, időtúllépés, bezárás) egyetlen tokenné kapcsolására.

6. Miért kell Dispose?

Az időzítőt és regisztrációkat felszabadítja; felszabadítás után nem hívható a Cancel.

7. Mit teszel bezáráskor?

Megszakítod a munkát, 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 CancellationToken segí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ó.