Primer: generisanje N PDF dokumenata za štampu, sa progress barom, logom, pauzom i prekidom.
Obrada direktno u Click handleru zauzme UI nit. Prozor ne crta, ne reaguje na klik, Windows ga označi kao Not responding, a progress bar se ne pomera. Dugme „LOŠE: posao na UI niti (5 s)" u demo formi to pokazuje.
UI nit samo crta i reaguje na klik. Sve teško ide u Task.Run:
private async void btnStart_Click(object sender, EventArgs e)
{
SetJobRunning(true);
try
{
await Task.Run(() => _job.RunAsync(parallelism, _cts.Token));
}
catch (OperationCanceledException) { /* prekinuto */ }
finally
{
SetJobRunning(false); // posle await-a smo opet na UI niti → smemo da diramo kontrole
}
} GUI → pozadina pozadina → GUI
┌──────────────────────────────┐ ┌───────────────────────────────────────┐
│ Prekini → CancellationToken │ │ BatchJob.Log ─┐ │
│ Pauza → ManualResetEvent │ │ Shell.Output ─┼→ ConcurrentQueue ─┐ │
│ Enter → shell stdin │ │ Done/Failed → Interlocked brojač │ │
└──────────────────────────────┘ │ ▼ │
│ uiTimer (100 ms) → TextBox, │
│ ProgressBar │
│ Shell.Exited → BeginInvoke (retko) │
└───────────────────────────────────────┘Ne radi Invoke za svaku liniju loga. 10.000 linija u sekundi znači 10.000 Invoke-a u sekundi, pa se UI zaguši isto kao da radi posao.
Umesto toga:
ConcurrentQueue) i povećavaju brojač (Interlocked)UI je zato podjednako brz bez obzira da li posao pravi 100 ili 100.000 linija u sekundi.
BatchJob radiParallel.ForEachAsync sa MaxDegreeOfParallelism.ManualResetEventSlim. Radnici stoje na kapiji dok je zatvorena, a Cancel ih budi.RenderDocumentAsync.BatchJob.cs
namespace WinFormsApp_test
{
/// <summary>
/// Dugačak posao (simulacija: generisanje N PDF dokumenata za štampu).
/// Ne zna ništa o formi — nema TextBox-a, ProgressBar-a ni Invoke-a.
/// GUI → posao: CancellationToken (prekid), Pause()/Resume().
/// Posao → GUI: brojači Done/Failed (forma ih čita na tajmer) i Log event.
/// </summary>
public sealed class BatchJob
{
private int _done;
private int _failed;
private readonly ManualResetEventSlim _pauseGate = new(initialState: true); // otvorena = radi
public BatchJob(int total) => Total = total;
public int Total { get; }
public int Done => Volatile.Read(ref _done);
public int Failed => Volatile.Read(ref _failed);
public int Processed => Done + Failed;
public bool IsPaused => !_pauseGate.IsSet;
/// <summary>Jedna linija loga. Diže se sa POZADINSKIH niti — handler ne sme da dira kontrole.</summary>
public event Action<string>? Log;
public void Pause() => _pauseGate.Reset();
public void Resume() => _pauseGate.Set();
public async Task RunAsync(int parallelism, CancellationToken ct)
{
var options = new ParallelOptions { MaxDegreeOfParallelism = parallelism, CancellationToken = ct };
await Parallel.ForEachAsync(Enumerable.Range(1, Total), options, async (id, token) =>
{
_pauseGate.Wait(token); // na pauzi radnici stoje ovde; Cancel ih budi sa OperationCanceledException
try
{
int pages = await RenderDocumentAsync(id, token);
Interlocked.Increment(ref _done);
Log?.Invoke($"#{id:D6} OK {pages,3} str.");
}
catch (OperationCanceledException)
{
throw; // prekid nije greška dokumenta
}
catch (Exception ex)
{
// Jedan loš zapis ne sme da obori ceo batch → zabeleži i nastavi
Interlocked.Increment(ref _failed);
Log?.Invoke($"#{id:D6} GREŠKA {ex.Message}");
}
});
}
/// <summary>
/// Ovde ide prava obrada: čitanje zapisa iz baze/CSV-a, popunjavanje šablona, render PDF-a, upis na disk.
/// Simulacija: malo CPU rada + malo čekanja na "disk", povremeno neispravan zapis.
/// </summary>
private static async Task<int> RenderDocumentAsync(int id, CancellationToken ct)
{
double x = 0;
for (int i = 0; i < 300_000; i++) x += Math.Sqrt(i ^ id); // "render"
await Task.Delay(Random.Shared.Next(5, 30), ct); // "upis na disk"
if (Random.Shared.Next(300) == 0)
throw new InvalidDataException("neispravan zapis u izvoru (prazno polje ADRESA)");
return 1 + (int)(x % 12);
}
}
}UiLogSink: bafer između niti i TextBox-aWrite() sme sa bilo koje niti, jer samo stavlja u red.Flush() zove UI tajmer; najviše 5000 linija po Tick-u.UiLogSink.cs
using System.Collections.Concurrent;
using System.Text;
namespace WinFormsApp_test
{
/// <summary>
/// Log bafer između pozadinskih niti i TextBox-a.
/// Write() sme da se zove sa BILO KOJE niti (samo stavlja u red, ne dira UI).
/// Flush() zove UI tajmer na ~100 ms i upisuje sve odjednom.
/// Zašto ne Invoke po liniji: 10.000 linija/s = 10.000 Invoke-a/s → UI se zaguši isto kao da radi posao.
/// </summary>
public sealed class UiLogSink
{
private readonly ConcurrentQueue<string> _queue = new();
private readonly TextBox _target;
private readonly int _maxChars;
public UiLogSink(TextBox target, int maxChars = 1_000_000)
{
_target = target;
_maxChars = maxChars;
}
public void Write(string line) => _queue.Enqueue(line);
public void Info(string message) => Write($"{DateTime.Now:HH:mm:ss.fff} {message}");
/// <summary>Samo sa UI niti (npr. iz Timer.Tick).</summary>
public void Flush()
{
if (_queue.IsEmpty) return;
var sb = new StringBuilder();
int lines = 0;
while (lines < 5000 && _queue.TryDequeue(out var line)) // limit da jedan Tick ne traje predugo
{
sb.AppendLine(line);
lines++;
}
// TextBox sa više MB teksta postaje spor → odseci stariju polovinu
if (_target.TextLength + sb.Length > _maxChars)
_target.Text = _target.Text[(_target.TextLength / 2)..];
_target.AppendText(sb.ToString()); // AppendText i skroluje na kraj
}
public void Clear()
{
_queue.Clear();
_target.Clear();
}
}
}Konzolni test bez forme: 2000 dokumenata obrađeno do kraja, pauza zaustavlja brojač (94 → 94 posle 1 s), nastavak ga pomera, a prekid tokom pauze uredno baca OperationCanceledException.