Grafikus C# – Windows Forms tanfolyam › 11.6 lecke

Windows Forms · 11.6 lecke · háttérmunka

Háttérben végzett műveletek

A Task.Run és az await (11.5 lecke) az alap. Ebben a leckében a háttérmunka gyakorlati oldalát nézzük: a Windows Forms régi, de sok programban még ma is használt eszközét, a BackgroundWorkert (érdemes ismerned, mert régi kódban találkozol vele); a Task.Run haladóbb használatát (hosszú munka, haladás-jelzés, eredmény); a párhuzamos feldolgozást (Parallel.For), amely a több processzormag erejét használja, és a vele járó versenyhelyzetet; végül azt, hogyan választod ki a feladathoz illő eszközt. A gyakorlóprojekt egy képfeldolgozó: nagy kép szűrése a felület lefagyása nélkül.

  • kb. 120 perc
  • haladó szint
  • háttérmunka
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A lecke végére

  • használod a BackgroundWorkert (DoWork, ProgressChanged, RunWorkerCompleted, megszakítás), és tudod, mikor van értelme;
  • a Task.Run segítségével eredményt adó, haladást jelentő háttérmunkát írsz;
  • párhuzamos ciklust (Parallel.For) használsz, és felismered a versenyhelyzetet (Interlocked, lock);
  • tudod, hogy a Bitmap nem szálbiztos, és LockBits segítségével dolgozol;
  • feladathoz választasz eszközt (Task.Run, ...Async, Parallel, Thread, BackgroundWorker);
  • a nagy fájl keresését (10.2 lecke) átírod aszinkronra.

Mielőtt elkezded

Ehhez a leckéhez a 11.3–11.5 lecke (felületi szál, blokkolás, async/await) és a 9.1–9.2 lecke (Bitmap) szükséges.

11.5 Task, async, await →

BackgroundWorker: az esemény-alapú háttérmunka

A BackgroundWorker a Toolboxból a Formra tehető komponens, három eseménnyel: a DoWork háttérszálon fut; a ProgressChanged és a RunWorkerCompleted a felületi szálon (ezért ott a vezérlők közvetlenül elérhetők, Invoke nélkül). Az async/await előtti időkben ez volt a szokásos megoldás, ma is sok meglévő programban találkozol vele:

Form1.cs (using System.ComponentModel;)C#
// beállítás (Designer vagy Load): haladás és megszakítás engedélyezése
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;

private void btnIndit_Click(object sender, EventArgs e)
{
btnIndit.Enabled = false;
btnMegse.Enabled = true;
pb.Value = 0;

bw.RunWorkerAsync((int)nudDb.Value); // a paraméter a DoWork e.Argument értéke (a bemenetet itt, a felületi szálon olvassuk ki)
}

// 1. DoWork: HÁTTÉRSZÁLON! Vezérlőhöz nem nyúlhat.
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
int db = (int)e.Argument;
long osszeg = 0;

for (int i = 1; i <= db; i++)
{
if (bw.CancellationPending) // a megszakítás kérésének ellenőrzése
{
e.Cancel = true;
return;
}

osszeg += NehezSzamitas(i);

if (i % 100 == 0) { bw.ReportProgress(i * 100 / db); } // haladás jelzése: a felületi szálra érkezik
}

e.Result = osszeg; // az eredmény a RunWorkerCompleted-ben
}

// 2. ProgressChanged: a FELÜLETI szálon
private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pb.Value = e.ProgressPercentage;
}

// 3. RunWorkerCompleted: a FELÜLETI szálon: kész, hibás vagy megszakított
private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
btnIndit.Enabled = true;
btnMegse.Enabled = false;

if (e.Error != null) { lblAllapot.Text = "Hiba: " + e.Error.Message; } // a DoWork kivétele ide kerül
else if (e.Cancelled) { lblAllapot.Text = "Megszakítva."; }
else { lblAllapot.Text = "Eredmény: " + (long)e.Result; }
}

private void btnMegse_Click(object sender, EventArgs e)
{
bw.CancelAsync(); // csak kéri a megszakítást: a DoWork ciklusnak kell figyelnie (CancellationPending)
}
Három esemény, három szerepA DoWork a munka (háttérszál), a ProgressChanged a haladás, a RunWorkerCompleted a befejezés (felületi szál). Ez a felosztás kényelmes: nem kell Invoke, de szétszabdalja a logikát három metódusra.
AdatátadásA bemenet az e.Argument, az eredmény az e.Result. A bemenetet a felületi szálon olvasd ki, és a RunWorkerAsync paramétereként add át; a DoWork-ban vezérlőt ne érj.
MegszakításA CancelAsync() csak kéri a leállítást (a CancellationPending igaz lesz): a DoWork ciklusának kell ellenőriznie és kilépnie, e.Cancel = true beállítással.
Kivétel a DoWork-banA DoWork kezeletlen kivétele nem omlasztja össze a programot: a RunWorkerCompleted e.Error tulajdonságába kerül. Ha nem ellenőrzöd az e.Error értékét, és az e.Result-ot olvasod, TargetInvocationException jön.

Jósolj!

A DoWork kezelő közvetlenül írja a lblAllapot.Text értékét. Mi történik?

Jóslat ellenőrzése

InvalidOperationException (cross-thread), amely a RunWorkerCompleted e.Error értékeként jelenik meg (a program nem omlik össze, de a munka félbeszakad). A haladáshoz a ReportProgress való, az eredményhez az e.Result.

Task.Run haladóbban: ugyanaz egyszerűbben

Ugyanezt a feladatot a Task.Run és az await egyetlen metódusban oldja meg. A haladás jelzésére a Progress<T> való: a felületi szálon létrehozva a visszahívás is oda érkezik (részletesen a 11.7 leckében), a megszakítás a CancellationToken (11.8 lecke):

Form1.csC#
private async void btnIndit_Click(object sender, EventArgs e)
{
btnIndit.Enabled = false;
pb.Value = 0;

int db = (int)nudDb.Value; // a bemenet a felületi szálon
Progress<int> halad = new Progress<int>(szazalek => pb.Value = szazalek); // a visszahívás a FELÜLETI szálon fut

try
{
long osszeg = await Task.Run(() => Szamol(db, halad));

lblAllapot.Text = "Eredmény: " + osszeg;
}
catch (Exception ex)
{
lblAllapot.Text = "Hiba: " + ex.Message;
}
finally
{
btnIndit.Enabled = true;
}
}

// a háttérszálon futó munka: vezérlő nélkül, a haladást a Progress-en át jelzi
private long Szamol(int db, IProgress<int> halad)
{
long osszeg = 0;

for (int i = 1; i <= db; i++)
{
osszeg += NehezSzamitas(i);

if (i % 100 == 0) { halad.Report(i * 100 / db); }
}

return osszeg;
}
BackgroundWorkerTask.Run + await
SzerkezetHárom esemény, szétszórt kód.Egy metódus, soronként olvasható.
Eredmény, kivétele.Result, e.Error az eseményben.Az await értéke, try–catch.
HaladásReportProgress (csak százalék + egy objektum).IProgress<T> tetszőleges típussal.
MegszakításCancelAsync, CancellationPending.CancellationToken (a szabvány).
Több művelet együttNehézkes.WhenAll, WhenAny.
Mikor?Meglévő kód karbantartása.Új kód: ezt válaszd.

Jósolj!

A Progress<int> példány a háttérszálon jön létre (a Task.Run lambdájában: new Progress<int>(p => pb.Value = p)). Mi történik a Report hívásakor?

Jóslat ellenőrzése

A visszahívás nem a felületi szálon fut, mert a Progress<T> a létrehozásának szálához tartozó környezetet jegyzi meg: háttérszálon létrehozva a pb.Value írása cross-thread kivételt (vagy hibát) okoz. A Progress<T>-t mindig a felületi szálon hozd létre.

Párhuzamos feldolgozás és a versenyhelyzet

A Task.Run a felületet védi, de a számítás egy magon fut. Ha a munka független elemekből áll (egy kép sorai, egy lista elemei), a Parallel.For a ciklust több szálon, egyszerre futtatja, így a több processzormag is dolgozik:

Form1.cs (using System.Threading; using System.Threading.Tasks;)C#
// SOROS: egy szál, egy mag
for (int y = 0; y < magassag; y++) { SortFeldolgoz(y); }

// PÁRHUZAMOS: a sorokat több szál osztja meg (a sorrend nem garantált!)
Parallel.For(0, magassag, y => SortFeldolgoz(y));

// FONTOS: a Parallel.For BLOKKOLJA a hívó szálat, amíg a ciklus végez!
// A felületi szálon közvetlenül hívva fagyaszt: mindig Task.Run-ba tesszük.
await Task.Run(() => Parallel.For(0, magassag, y => SortFeldolgoz(y)));

// VERSENYHELYZET: a közös változót több szál egyszerre írja
long db = 0;
Parallel.For(0, 1000000, i => { db++; }); // HIBÁS: az eredmény nem 1 000 000 (elveszett növelések)

long jo = 0;
Parallel.For(0, 1000000, i => { Interlocked.Increment(ref jo); }); // HELYES: oszthatatlan növelés

// zárral (összetettebb közös adatnál)
object zar = new object();
List<int> talalatok = new List<int>();

Parallel.For(0, 1000, i =>
{
if (i % 7 == 0)
{
lock (zar) { talalatok.Add(i); } // a List<T> NEM szálbiztos: a közös listához zár kell
}
});
Mikor éri meg?A Parallel.For akkor gyorsít, ha a munka számításigényes és az elemek függetlenek. Apró művelet (egy összeadás) esetén a szálkezelés költsége nagyobb, mint a nyereség. Mérd meg (Stopwatch).
Versenyhelyzet (race condition)A db++ három lépés (olvas, növel, ír): két szál összefésülődő lépései elveszítik a növelések egy részét. Az eredmény véletlenszerű: egyszer 998 342, máskor 999 001. A hiba nehezen reprodukálható, és „általában működik”.
Interlocked és lockAz Interlocked.Increment oszthatatlan (atomi) művelet egyszerű számlálóhoz. Összetettebb közös adatnál (lista, szótár) a lock kritikus szakaszt ad: egyszerre egy szál léphet be. Kevesebb közös adat, kevesebb zár: a legjobb, ha a szálak külön adaton dolgoznak, és az eredményt a végén összegzed.
A Bitmap nem szálbiztosA Bitmap GetPixel/SetPixel hívásai több szálról egyszerre InvalidOperationException (Object is currently in use elsewhere) vagy hibás képet okozhatnak. Párhuzamos képfeldolgozáshoz a képpontokat LockBits segítségével bájttömbbe másold, a tömbön dolgozz (a sorok függetlenek), majd írd vissza.

Jósolj!

A Parallel.For(0, 1000000, i => { db++; }) ciklus után a db értéke 1 000 000 lesz?

Jóslat ellenőrzése

Nem (többmagos gépen): jóval kevesebb, és futásonként más, mert a szálak felülírják egymás növeléseit (versenyhelyzet). Egymagos környezetben „véletlenül” jó lehet: éppen ez veszélyes. Interlocked.Increment kell.

Melyik eszköz mikor?

FeladatEszköz
Fájl, hálózat, adatbázis (várakozás)Beépített ...Async metódus + await. Nincs szál.
Hosszú számítás (egy darab)await Task.Run(...)
Sok független elem számításaawait Task.Run(() => Parallel.For(...)) vagy PLINQ (AsParallel())
Egyszeri vagy ismétlődő késleltetés, kijelzésTimer, await Task.Delay
Hosszú ideig (a program teljes életén át) futó figyelőTask.Factory.StartNew(..., TaskCreationOptions.LongRunning) vagy külön Thread (IsBackground = true)
Meglévő, BackgroundWorker-t használó kódMaradhat; új funkciót már Task-kal írj.
Form1.cs (a 10.2 lecke keresésének átírása)C#
// A 10.2 lecke keresése DoEvents-szel volt; most aszinkron: a felület közben él, a haladás a Progress-en át érkezik
private async void btnKeres_Click(object sender, EventArgs e)
{
string szo = txtSzo.Text.Trim(); // a bemenet a felületi szálon
string ut = txtUt.Text;

if (szo.Length == 0 || !File.Exists(ut)) { return; }

btnKeres.Enabled = false;
lstTalalat.Items.Clear();

Progress<int> halad = new Progress<int>(p => pbHaladas.Value = p);

try
{
List<string> talalatok = await Task.Run(() => KeresFajlban(ut, szo, halad)); // az eredmény: a háttérben gyűjtött lista

lstTalalat.Items.AddRange(talalatok.ToArray()); // egyszerre, a felületi szálon (nem soronként)
tsslUzenet.Text = talalatok.Count + " találat.";
}
catch (IOException ex) { tsslUzenet.Text = ex.Message; }
finally { btnKeres.Enabled = true; }
}

private List<string> KeresFajlban(string ut, string szo, IProgress<int> halad)
{
List<string> talalatok = new List<string>();
long sorszam = 0;

using (FileStream fs = new FileStream(ut, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (StreamReader olvaso = new StreamReader(fs))
{
string sor;

while ((sor = olvaso.ReadLine()) != null)
{
sorszam++;

if (sor.IndexOf(szo, StringComparison.CurrentCultureIgnoreCase) >= 0)
{
talalatok.Add(sorszam + ": " + sor);

if (talalatok.Count >= 1000) { break; }
}

if (sorszam % 5000 == 0) { halad.Report((int)(fs.Position * 100 / Math.Max(1, fs.Length))); }
}
}

return talalatok;
}

Jósolj!

A fenti keresés közben a háttérszál a talalatok listához ad elemeket, miközben a felületi szál (a Progress visszahívásban) ugyanezt a listát olvassa. Mi a veszély?

Jóslat ellenőrzése

A List<T> nem szálbiztos: egyidejű írás és olvasás InvalidOperationException kivételt (a gyűjtemény módosult) vagy hibás adatot okozhat. A lecke megoldása a lista átadása a végén (a háttérszál végez vele, a felületi szál az await után használja): egyszerre egyetlen szál éri el.

📷 Képernyőkép helye

Mit tegyél ide: a képfeldolgozó: az eredeti és a szűrt kép, a négy módszer időmérése, a folyamatjelző.Javasolt fájlnév: winforms-11-6-kepfeldolgozo.webp · Alt szöveg: A képfeldolgozó 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 lefagy a Parallel.For alatt

private void btn_Click(object sender, EventArgs e)
{
Parallel.For(0, 2000, y => Sor(y)); // a hívó (felületi) szálat blokkolja
}
Hiba és javítás

A Parallel.For megvárja a ciklus végét. Tedd await Task.Run(() => Parallel.For(...)) alakba.

2. A számláló értéke nem stimmel

Parallel.ForEach(elemek, x => { if (Jo(x)) { jok++; } });
Hiba és javítás

Versenyhelyzet. Interlocked.Increment(ref jok), vagy szálanként helyi számláló, a végén összegezve.

3. InvalidOperationException: Object is currently in use elsewhere

Parallel.For(0, h, y => { for (int x = 0; x < w; x++) { kep.SetPixel(x, y, szin); } });
Hiba és javítás

A Bitmap nem szálbiztos. Másold a képpontokat LockBits segítségével bájttömbbe, és a tömbön dolgozz.

4. A haladás-jelzés cross-thread kivételt ad

await Task.Run(() =>
{
Progress<int> h = new Progress<int>(p => pb.Value = p); // háttérszálon létrehozva
h.Report(50);
});
Hiba és javítás

A Progress<T>-t a felületi szálon kell létrehozni (a Task.Run előtt), és átadni a háttérmunkának.

5. TargetInvocationException a RunWorkerCompleted-ben

lblAllapot.Text = e.Result.ToString();   // nincs e.Error ellenőrzés
Hiba és javítás

Ha a DoWork kivételt dobott, az e.Result olvasása újra dobja. Előbb ellenőrizd az e.Error és e.Cancelled tulajdonságot.

6. A megszakítás nem hat

A Mégse gomb után a feldolgozás tovább fut.

Ok és javítás

A CancelAsync() csak kéri a leállítást: a DoWork ciklusnak kell figyelnie a CancellationPending jelzőt (vagy CancellationToken esetén a token állapotát), és kilépnie.

Mini projekt: képfeldolgozó

Készíts programot, amely egy nagy képet szűr (szürkeárnyalat, majd elmosás), négyféleképpen futtatva, és mutatja a különbségeket:

  1. Felület: pbForras és pbEredmeny (PictureBox, Zoom); Kép generálása (2400 × 1600 pixeles színátmenetes, zajos Bitmap, a program maga állítja elő), Megnyitás... (a 9.1 lecke KepBetolt metódusával); cmbMod (1. Felületi szálon, 2. BackgroundWorker, 3. Task.Run, 4. Task.Run + Parallel.For); cmbSzuro (Szürkeárnyalat, Negatív, Elmosás (box blur 5×5)); Indítás, Mégse; pb (folyamatjelző), lblIdo; lstNaplo; az animáció és késésmérő (11.3 lecke).
  2. Szálbiztos feldolgozás: a forráskép képpontjait LockBits segítségével byte[] tömbbe másolod (Format32bppArgb), a szűrő a tömbön dolgozik (a sorok függetlenek), az eredményt új Bitmapba írod vissza; a Bitmapot nem éri több szál egyszerre.
  3. 1. mód: a felületi szálon; az animáció áll, a késés nagy.
  4. 2. mód: BackgroundWorker: DoWork (a feldolgozás, haladás soronként 5%-onként), ProgressChanged, RunWorkerCompleted (e.Error, e.Cancelled ellenőrzése).
  5. 3. mód: await Task.Run és Progress<int>; a bemenet kiolvasása a felületi szálon.
  6. 4. mód: await Task.Run(() => Parallel.For(0, magassag, sor)); a haladás: Interlocked.Increment számláló (a kész sorok), a jelentés nem minden sorra (legfeljebb 20 ms-onként).
  7. Megszakítás: a Mégse a BackgroundWorkernél CancelAsync; a Task-oknál egy volatile bool megszakit jelző (a 11.8 lecke CancellationToken megoldását előkészítve); a feldolgozó ciklusok ellenőrzik.
  8. Összehasonlítás: a naplóban a négy mód ideje és a legnagyobb késés; a 4. mód a többmagos gépen gyorsabb; a kép azonos eredményt ad (az első 1000 képpont összehasonlítása).
  9. Hibatűrés: nincs kép (érthető üzenet); a régi eredménykép felszabadítása (9.8 lecke); a Form bezárása futó feldolgozás közben; a gombok állapota.
  10. Teszteld: mind a négy mód mindhárom szűrővel, a mozgó négyzet (a késésmérő), megszakítás, dupla indítás, egy nagyon kicsi kép (1 × 1), a Feladatkezelőben a processzorhasználat a 3. és a 4. módnál.
Megoldás megnyitása (próbáld ki előbb önállóan!)

A központi rész a szálbiztos Szur metódus (LockBits + bájttömb) és a négy futtatási mód.

Form1.cs (using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Diagnostics; using System.ComponentModel;)C#
private Bitmap forras = null;
private volatile bool megszakit = false;
private int futo = 0;

// ---- kép generálása ----
private void btnGeneral_Click(object sender, EventArgs e)
{
Bitmap uj = new Bitmap(2400, 1600, PixelFormat.Format32bppArgb);
Random v = new Random(1);

for (int y = 0; y < uj.Height; y += 4)
{
for (int x = 0; x < uj.Width; x += 4)
{
Color c = Color.FromArgb(x * 255 / uj.Width, y * 255 / uj.Height, v.Next(256));

for (int dy = 0; dy < 4; dy++) { for (int dx = 0; dx < 4; dx++) { uj.SetPixel(x + dx, y + dy, c); } }
}
}

Bitmap regi = forras;

forras = uj;
pbForras.Image = forras;
if (regi != null) { regi.Dispose(); }
}

// ---- szálbiztos szűrő: a képpontok bájttömbben ----
private Bitmap Szur(Bitmap kep, int szuro, bool parhuzamos, Action<int> sorKesz)
{
int w = kep.Width, h = kep.Height;
BitmapData bd = kep.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
int stride = bd.Stride;
byte[] be = new byte[stride * h];

Marshal.Copy(bd.Scan0, be, 0, be.Length);
kep.UnlockBits(bd);

byte[] ki = new byte[be.Length];

Action<int> sor = y =>
{
if (megszakit) { return; }

for (int x = 0; x < w; x++)
{
int j = y * stride + x * 4;

if (szuro == 0) // szürkeárnyalat
{
byte g = (byte)(0.114 * be[j] + 0.587 * be[j + 1] + 0.299 * be[j + 2]);

ki[j] = ki[j + 1] = ki[j + 2] = g; ki[j + 3] = 255;
}
else if (szuro == 1) // negatív
{
ki[j] = (byte)(255 - be[j]); ki[j + 1] = (byte)(255 - be[j + 1]); ki[j + 2] = (byte)(255 - be[j + 2]); ki[j + 3] = 255;
}
else // elmosás 5×5 (a bemenet olvasása, a kimenet írása: külön tömb, nincs verseny)
{
int r = 0, g = 0, b = 0, n = 0;

for (int dy = -2; dy <= 2; dy++)
{
int yy = y + dy;

if (yy < 0 || yy >= h) { continue; }

for (int dx = -2; dx <= 2; dx++)
{
int xx = x + dx;

if (xx < 0 || xx >= w) { continue; }

int k = yy * stride + xx * 4;

b += be[k]; g += be[k + 1]; r += be[k + 2]; n++;
}
}

ki[j] = (byte)(b / n); ki[j + 1] = (byte)(g / n); ki[j + 2] = (byte)(r / n); ki[j + 3] = 255;
}
}

if (sorKesz != null) { sorKesz(y); }
};

if (parhuzamos) { Parallel.For(0, h, sor); }
else { for (int y = 0; y < h; y++) { sor(y); } }

if (megszakit) { return null; }

Bitmap eredmeny = new Bitmap(w, h, PixelFormat.Format32bppArgb);
BitmapData bd2 = eredmeny.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);

Marshal.Copy(ki, 0, bd2.Scan0, ki.Length);
eredmeny.UnlockBits(bd2);

return eredmeny;
}

private void Mutat(Bitmap uj, string mod, long ms)
{
if (uj == null) { Naploz(mod + ": megszakítva"); return; }

Bitmap regi = pbEredmeny.Image as Bitmap;

pbEredmeny.Image = uj;
if (regi != null) { regi.Dispose(); }

Naploz(mod + ": " + ms + " ms, legnagyobb késés: " + legnagyobbKeses + " ms");
}

private void Naploz(string s) { lstNaplo.Items.Insert(0, DateTime.Now.ToString("HH:mm:ss") + " " + s); }

private void Fut(bool fut) { btnIndit.Enabled = !fut; btnMegse.Enabled = fut; cmbMod.Enabled = !fut; cmbSzuro.Enabled = !fut; }

// ---- futtatás ----
private async void btnIndit_Click(object sender, EventArgs e)
{
if (forras == null) { tsslUzenet.Text = "Előbb generálj vagy nyiss meg egy képet."; return; }

int mod = cmbMod.SelectedIndex, szuro = cmbSzuro.SelectedIndex;
int magassag = forras.Height;

megszakit = false;
legnagyobbKeses = 0;
pb.Value = 0;
Fut(true);

Stopwatch sw = Stopwatch.StartNew();

try
{
if (mod == 0) // 1. felületi szálon
{
Mutat(Szur(forras, szuro, false, null), "1. Felületi szál", sw.ElapsedMilliseconds);
}
else if (mod == 1) // 2. BackgroundWorker
{
bw.RunWorkerAsync(new int[] { szuro });
return; // a Fut(false) a RunWorkerCompleted-ben
}
else
{
bool par = mod == 3;
int kesz = 0;
Progress<int> halad = new Progress<int>(p => pb.Value = Math.Min(100, p)); // a FELÜLETI szálon létrehozva
Stopwatch jelent = Stopwatch.StartNew();
Bitmap forrasMasolat = forras; // a háttérben csak olvassuk (LockBits), a felület közben nem módosítja

Bitmap uj = await Task.Run(() => Szur(forrasMasolat, szuro, par, y =>
{
int n = Interlocked.Increment(ref kesz);

if (n % 40 == 0 && jelent.ElapsedMilliseconds > 20) { jelent.Restart(); ((IProgress<int>)halad).Report(n * 100 / magassag); }
}));

if (IsDisposed) { if (uj != null) { uj.Dispose(); } return; }

Mutat(uj, mod == 2 ? "3. Task.Run" : "4. Task.Run + Parallel.For", sw.ElapsedMilliseconds);
}
}
catch (Exception ex) { tsslUzenet.Text = "Hiba: " + ex.Message; }
finally
{
if (mod != 1 && !IsDisposed) { pb.Value = 100; Fut(false); }
}
}

// ---- BackgroundWorker ----
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
int szuro = ((int[])e.Argument)[0];
int kesz = 0, magassag = forras.Height;

Stopwatch sw = Stopwatch.StartNew();

Bitmap uj = Szur(forras, szuro, false, y =>
{
kesz++;

if (kesz % (magassag / 20) == 0) { bw.ReportProgress(kesz * 100 / magassag); }
});

if (uj == null) { e.Cancel = true; return; }

e.Result = new object[] { uj, sw.ElapsedMilliseconds };
}

private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e) { pb.Value = Math.Min(100, e.ProgressPercentage); }

private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Fut(false);

if (e.Error != null) { tsslUzenet.Text = "Hiba: " + e.Error.Message; return; }
if (e.Cancelled) { Naploz("2. BackgroundWorker: megszakítva"); return; }

object[] r = (object[])e.Result;

pb.Value = 100;
Mutat((Bitmap)r[0], "2. BackgroundWorker", (long)r[1]);
}

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

if (bw.IsBusy) { bw.CancelAsync(); }
}
📷 Képernyőkép helye

Mit tegyél ide: a kész képfeldolgozó a négy mód időmérésével.Javasolt fájlnév: winforms-11-6-kesz.webp · Alt szöveg: A kész képfeldolgozó

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. Fájlok kötegelt kicsinyítése

Egy mappa 200 képét kicsinyítsd (9.2 lecke) a felület lefagyása nélkül, haladás-jelzéssel. Mely részek mennek a háttérbe?

Egy lehetséges megoldás

A betöltés, a kicsinyítés és a mentés a Task.Run lambdájában (vezérlő nélkül, a fájlok listáját előre átadva); a haladás Progress<int>; a képenkénti using blokk felszabadít. A fájlonkénti párhuzamosítás (Parallel.ForEach) a gyors lemezen gyorsíthat.

2. PLINQ

Egy 1 milliós szám-listából a prímeket kell kiszűrni. Hogyan használod a PLINQ-t?

Egy lehetséges megoldás

await Task.Run(() => szamok.AsParallel().Where(Prim).ToList()). A AsParallel() a kifejezést több szálon végzi; a sorrend nem garantált (ha kell: AsOrdered()).

3. Háttér-figyelő

Egy háttérszál percenként ellenőriz egy mappát, és az új fájlokról a felületet értesíti. Mit használsz?

Egy lehetséges megoldás

Egy Timer (felületi szál, egyszerű ellenőrzés await-tel), vagy a FileSystemWatcher (az eseményei másik szálon érkeznek: BeginInvoke kell a felületi frissítéshez). Külön Thread csak hosszú, folyamatos ciklushoz indokolt.

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. Melyik szálon?

Melyik szálon fut a BackgroundWorker három eseménye?

Megoldás megnyitása

DoWork: háttérszál; ProgressChanged és RunWorkerCompleted: a felületi szál.

2. Eszköz

Melyiket választod: (a) fájl letöltése; (b) 50 millió elem összegzése; (c) minden másodpercben frissülő óra?

Megoldás megnyitása

(a) HttpClient + await; (b) await Task.Run (esetleg Parallel); (c) Timer.

Közepes szint

3. Interlocked

Mi a különbség a db++ és az Interlocked.Increment(ref db) között?

Megoldás megnyitása

A db++ három lépés (olvas, növel, ír), amelyek két szálon összefésülődhetnek; az Interlocked.Increment oszthatatlan (atomi), nincs elveszett növelés.

4. Lista és szálak

Mit használhatsz a List<T> helyett, ha több szál ad hozzá elemeket?

Megoldás megnyitása

ConcurrentBag<T> vagy ConcurrentQueue<T> (System.Collections.Concurrent), vagy a lista lock-kal védve; legjobb a szálanként külön lista, a végén egyesítve.

Kihívás szint

5. Hány szál?

A Parallel.For hány szálat használ, és hogyan korlátozod?

Megoldás megnyitása

Alapból a szálkészlet méretezése és a processzormagok száma szerint. Korlát: new ParallelOptions { MaxDegreeOfParallelism = 2 } (például, hogy a felület és a többi program is kapjon processzoridőt).

Form1.csC#
ParallelOptions opciok = new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount - 1) };Parallel.For(0, h, opciok, y => Sor(y));

6. Gyorsulás mérése

Négymagos gépen a párhuzamos változat 2,6-szor gyorsabb, nem 4-szer. Miért?

Megoldás megnyitása

A szálkezelés költsége, a közös erőforrások (memória-sávszélesség), a felület és a többi program is használ processzoridőt, és a munka egy része sorosan marad (Amdahl törvénye). A tökéletes gyorsulás ritka.

Gyors önellenőrzés

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

1. Melyik BackgroundWorker esemény fut háttérszálon?

A DoWork; a másik kettő a felületi szálon.

2. Miért jobb a Task.Run + await a BackgroundWorkernél?

Egyetlen metódusban olvasható, try–catch, eredmény, CancellationToken, WhenAll.

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

A felületi szálon, a Task.Run előtt; így a visszahívás is a felületi szálon fut.

4. Mit blokkol a Parallel.For?

A hívó szálat: felületi szálon await Task.Run alatt használd.

5. Mi a versenyhelyzet?

Több szál egyszerre írja ugyanazt az adatot, és elvész egy módosítás; az eredmény véletlenszerű.

6. Miért nem szálbiztos a Bitmap?

A GDI+ objektum egyidejű használata hibát okoz; a képpontokat LockBits-tel bájttömbbe kell másolni.

7. Kell-e szál az I/O-hoz?

Nem: a beépített ...Async metódusok szál nélkül várnak.

Mit tudsz már?

  • Használod és megérted a BackgroundWorkert (régi kód), de új kódot Task-kal írsz.
  • Task.Run segítségével eredményt adó, haladást jelentő háttérmunkát készítesz.
  • Párhuzamos ciklust használsz, és kezeled a versenyhelyzetet.
  • Tudod, hogy a Bitmap és a List<T> nem szálbiztos.
  • Feladathoz választasz eszközt, és a nagy fájl keresését aszinkronra írod át.

Következő lecke: UI-vezérlőelemek biztonságos frissítése

Invoke, BeginInvoke, Progress<T>, a felületi frissítések szabályozása (throttling), és a Form bezárása futó háttérmunka mellett.