TPL
Task Parallel Library (TPL)
Sezione intitolata âTask Parallel Library (TPL)âhttps://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl
La libreria Task Parallel Library (TPL) è un insieme di tipi e API pubblici negli spazi dei nomi System.Threading e System.Threading.Tasks. Lo scopo di TPL è rendere gli sviluppatori piĂš produttivi semplificando lâaggiunta di parallelismo e concorrenza alle applicazioni. La libreria TPL adatta dinamicamente il grado di concorrenza per sfruttare efficacemente tutti i processori disponibili. Inoltre gestisce il partizionamento del lavoro, la schedulazione dei thread nel ThreadPool, il supporto per lâannullamento, la gestione dello stato e altri dettagli di basso livello. Usando TPL è possibile ottimizzare le prestazioni concentrandosi sulle operazioni applicative rilevanti.
A partire da .NET Framework 4, TPL è la soluzione consigliata per scrivere codice multithreading e parallelo. Tuttavia non tutto il codice è adatto alla parallelizzazione; ad esempio, se un ciclo esegue poco lavoro per iterazione o non esegue molte iterazioni, lâoverhead della parallelizzazione può rallentare lâesecuzione. La parallelizzazione introduce anche complessitĂ (blocchi, deadlock, race condition): avere una comprensione di base di questi concetti aiuta a usare TPL efficacemente.
Task in C#
Sezione intitolata âTask in C#âUn Task è uno degli elementi centrali del pattern asincrono basato su task in .NET. Un oggetto Task tipicamente esegue in modo asincrono su un thread del thread pool invece che sincronicamente sul thread principale dellâapplicazione. Come visto nel capitolo precedente, possiamo spostare il lavoro su thread del pool usando QueueUserWorkItem, ma quel metodo ha limiti: non consente di sapere facilmente quando lâoperazione è terminata nĂŠ di ottenere un valore di ritorno. Un Task è utile perchĂŠ può indicare se il lavoro è completato e può restituire un risultato. Un Task rappresenta unâoperazione di lavoro da eseguire.
I Task forniscono strumenti avanzati per gestire operazioni asincrone o parallele offrendo, fra lâaltro:
- la possibilitĂ di cancellare unâoperazione in corso
- il ritorno di un valore risultante dallâoperazione
- gestione semplice delle eccezioni
- costrutti di alto livello come cicli paralleli
- continuazioni dei task
I Task possono rendere lâapplicazione piĂš reattiva: se il thread dellâinterfaccia delega il lavoro a un thread del pool, può continuare a elaborare eventi utente. I Task permettono anche di parallelizzare operazioni CPU-bound su piĂš processori.
C# Task: Programmazione asincrona e/o parallela basata su attivitĂ
Sezione intitolata âC# Task: Programmazione asincrona e/o parallela basata su attivitĂ âLa libreria TPL si basa sul concetto di attivitĂ (task), che rappresenta unâoperazione asincrona. In qualche modo un task è analogo a un thread o a un elemento di lavoro del ThreadPool, ma a un livello di astrazione piĂš elevato. Per âparallelismo delle attivitĂ â si intende lâesecuzione contemporanea di una o piĂš attivitĂ indipendenti. I vantaggi principali delle attivitĂ sono:
- Leggerezza a livello di sistema: le attivitĂ vengono accodate nel
ThreadPool, che dispone di algoritmi migliorati per determinare e adattare il numero di thread, bilanciando il carico e rendendo le attività relativamente leggere; è possibile crearne molte per ottenere parallelismo efficiente. - Maggior controllo a livello applicativo: le API delle attività forniscono funzionalità di attesa, annullamento, continuazioni, gestione delle eccezioni, stato dettagliato e pianificazione personalizzata.
Per questi motivi, TPL è lâAPI preferita in .NET per codice multithreading, asincrono e parallelo.
Il metodo Parallel.Invoke consente di eseguire simultaneamente piĂš azioni; si passano delegati Action, spesso come espressioni lambda. Esempio:
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());UnâattivitĂ senza valore di ritorno è rappresentata da Task. UnâattivitĂ che restituisce un valore è Task<TResult> che eredita da Task.
Lâoggetto Task gestisce i dettagli dellâinfrastruttura e mette a disposizione metodi e proprietĂ (es. Status) per interrogare lo stato dellâattivitĂ (avviata, completata, annullata, faulted). Lo stato è rappresentato dallâenumerazione TaskStatus.
Quando si crea un task si assegna un delegato che incapsula il codice da eseguire: può essere un metodo nominato, un metodo anonimo o una lambda. Spesso si usa Task.Wait per assicurarsi che il task sia completato prima che termini lâapplicazione console.
using System;using System.Threading;using System.Threading.Tasks;
namespace TaskIntro{ class Program { static void Main(string[] args) { Thread.CurrentThread.Name = "Main";
// Create a task and supply a user delegate by using a lambda expression. Task taskA = new Task(() => Console.WriteLine("Hello from taskA.")); // Start the task. taskA.Start();
// Output a message from the calling thread. Console.WriteLine("Hello from thread '{0}'.", Thread.CurrentThread.Name); taskA.Wait();
} }}// The example displays output like the following:// Hello from thread 'Main'.// Hello from taskA.Ă possibile usare Task.Run per creare e avviare un task in unâunica operazione; Run usa lo scheduler predefinito e rappresenta il modo preferito quando non serve un controllo avanzato sulla creazione/schedulazione.
using System;using System.Threading;using System.Threading.Tasks;
namespace TaskRunDemo{ class Program { static void Main(string[] args) { Thread.CurrentThread.Name = "Main";
// Define and run the task. Task taskA = Task.Run(() => Console.WriteLine("Hello from taskA."));
// Output a message from the calling thread. Console.WriteLine("Hello from thread '{0}'.", Thread.CurrentThread.Name); taskA.Wait();
} }}// The example displays output like the following:// Hello from thread 'Main'.// Hello from taskA.TaskFactory.StartNew è unâaltra opzione per creare e avviare un task in unâunica operazione quando servono opzioni avanzate o uno scheduler personalizzato, oppure per passare uno stato aggiuntivo accessibile tramite Task.AsyncState.
using System;using System.Threading;using System.Threading.Tasks;
namespace TaskWithCustomData{ class CustomData { public long CreationTime; public int Name; public int ThreadNum; }
class Program { public static void Main() { Task[] taskArray = new Task[10]; for (int i = 0; i < taskArray.Length; i++) { taskArray[i] = Task.Factory.StartNew((object obj) => { CustomData data = obj as CustomData; if (data == null) return;
data.ThreadNum = Thread.CurrentThread.ManagedThreadId; },new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks }); } Task.WaitAll(taskArray); foreach (var task in taskArray) { //AsyncState restituisce l'oggetto che è stato passato quando il Task è stato creato, oppure null se non è stato passato nulla var data = task.AsyncState as CustomData; if (data != null) Console.WriteLine($"Task Name = {data.Name}, Task Id = {task.Id}, Task status = {task.Status}, created at {data.CreationTime}, ran on Thread Id = {data.ThreadNum}."); } }
}}// The example displays output like the following:// Task Name = 0, Task Id = 1, Task status = RanToCompletion, created at 637737183135068576, ran on Thread Id = 5.// Task Name = 1, Task Id = 2, Task status = RanToCompletion, created at 637737183135317927, ran on Thread Id = 6.// Task Name = 2, Task Id = 3, Task status = RanToCompletion, created at 637737183135345356, ran on Thread Id = 4.// Task Name = 3, Task Id = 4, Task status = RanToCompletion, created at 637737183135369204, ran on Thread Id = 7.// Task Name = 4, Task Id = 5, Task status = RanToCompletion, created at 637737183135391197, ran on Thread Id = 11.// Task Name = 5, Task Id = 6, Task status = RanToCompletion, created at 637737183135415846, ran on Thread Id = 13.// Task Name = 6, Task Id = 7, Task status = RanToCompletion, created at 637737183135794745, ran on Thread Id = 8.// Task Name = 7, Task Id = 8, Task status = RanToCompletion, created at 637737183135879399, ran on Thread Id = 10.// Task Name = 8, Task Id = 9, Task status = RanToCompletion, created at 637737183135879724, ran on Thread Id = 12.// Task Name = 9, Task Id = 10, Task status = RanToCompletion, created at 637737183135879799, ran on Thread Id = 9.Ogni task ha un Id univoco accessibile tramite Task.Id.
Modello di esecuzione di un Task
Sezione intitolata âModello di esecuzione di un TaskâIl delegato di un task viene eseguito in un thread separato. In macchine single-core ciò può implicare molti context switch; su macchine multicore i task possono essere distribuiti sui core disponibili e offrire vantaggio rispetto allâesecuzione sequenziale.
Nota di utilizzo: chiamare t.Wait(); è equivalente a chiamare Join su un thread: il chiamante aspetta che il task termini.
Task C# che restituisce un valore
Sezione intitolata âTask C# che restituisce un valoreâ.NET dispone di Task<TResult> per task che restituiscono valori. Lâaccesso alla proprietĂ Result forza lâattesa fino al completamento del task (comportamento equivalente a Join/Wait).
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace TaskReturnsValue01{ class Program { static void Main(string[] args) { //qui int è il tipo restituito dal Task //si noti che in questo caso si è usato il costrutto Task.Run(lambda) e non new Task(lambda) Task<int> t = Task.Run(() => { return 32; });
//qui leggiamo il valore restituito dal Task //è come fare una Join sul thread del Task oppure come invocare la Wait sul Task Console.WriteLine(t.Result);
Console.WriteLine(); Console.WriteLine("Press Enter to terminate!"); Console.ReadLine(); } }}Leggere Result su un Task blocca il thread fino al termine del task.
Aggiunta di continuazioni
Sezione intitolata âAggiunta di continuazioniâNella programmazione asincrona è comune che, dopo il completamento di unâoperazione, venga eseguita una seconda operazione che riceve i dati prodotti. Tradizionalmente ciò si realizzava con callback; in TPL si usano le continuazioni tramite Task.ContinueWith. Le continuazioni permettono, fra lâaltro:
- passare dati dallâattivitĂ precedente alla continuazione;
- specificare condizioni per lâesecuzione della continuazione;
- annullare una continuazione;
- suggerire come schedulare la continuazione;
- invocare piĂš continuazioni da una stessa attivitĂ precedente;
- concatenare continuazioni arbitrariamente;
- gestire eccezioni dellâattivitĂ precedente.
Per creare una continuazione chiamare Task.ContinueWith. Esempio base:
using System;using System.Threading.Tasks;
namespace ContinuationDemo01{ class Program { static void Main(string[] args) { // Execute the antecedent. Task<DayOfWeek> taskA = Task.Run(() => DateTime.Today.DayOfWeek);
// Execute the continuation when the antecedent finishes. taskA.ContinueWith(antecedent => Console.WriteLine("Today is {0}.", antecedent.Result)); Console.ReadLine(); } }}Questo metodo è simile a chiamare un metodo di callback al termine di unâoperazione. Esempio:
using System;using System.Threading.Tasks;
namespace TaskContinuation01{ class Program { static void Main(string[] args) { Task<int> t = Task.Run(() => { return 32; }).ContinueWith((i) => { return i.Result * 2; });
t.ContinueWith((i) => { Console.WriteLine(i.Result); });
Console.WriteLine("Press Enter to terminate!"); Console.ReadLine(); } }}Ă possibile creare una continuazione che verrĂ eseguita al termine di una o tutte le attivitĂ di un gruppo di attivitĂ . Per eseguire una continuazione al termine di tutte le attivitĂ precedenti, chiamare il metodo statico Task.WhenAll o il metodo TaskFactory.ContinueWhenAll dellâistanza. Per eseguire una continuazione al termine di almeno una delle attivitĂ precedenti, chiamare il metodo statico Task.WhenAny o il metodo TaskFactory.ContinueWhenAny dellâistanza.
Nellâesempio seguente viene chiamato il metodo Task.WhenAll(IEnumerable<Task>) per creare unâattivitĂ di continuazione che riflette il risultato delle 10 attivitĂ precedenti. Ogni attivitĂ precedente eleva al quadrato un valore di indice compreso tra uno e 10. Se le attivitĂ precedenti vengono completate correttamente (ovvero la relativa proprietĂ Task.Status è TaskStatus.RanToCompletion), la proprietĂ Task<TResult>.Result della continuazione è una vettore dei valori Task<TResult>.Result restituiti da ogni attivitĂ precedente. Lâesempio somma tali valori per calcolare la somma dei quadrati per tutti i numeri compresi tra uno e 10.
using System;using System.Collections.Generic;using System.Threading.Tasks;
namespace ContinuationAllDemo01{ class Program { static void Main(string[] args) { List<Task<int>> tasks = new List<Task<int>>(); for (int ctr = 1; ctr <= 10; ctr++) { int baseValue = ctr; tasks.Add(Task.Factory.StartNew((b) => { int i = (int)b; return i * i; }, baseValue)); } //continuation è di tipo Task<int[]> var continuation = Task.WhenAll(tasks);
long sum = 0; for (int ctr = 0; ctr <= continuation.Result.Length - 1; ctr++) { Console.Write($"{continuation.Result[ctr]}{(ctr == continuation.Result.Length - 1 ? " = " : " + ")}"); sum += continuation.Result[ctr]; } Console.WriteLine(sum);
} }}Pianificazione di continuazioni diverse
Sezione intitolata âPianificazione di continuazioni diverseâContinueWith offre overload per configurare quando la continuazione verrĂ eseguita (es. solo in caso di fault, solo se cancellata, solo se completata con successo), permettendo di definire comportamenti distinti per ciascun esito.
using System;using System.Threading.Tasks;
namespace ContinuationWithExceptionHandling01{ class Program { static void Main(string[] args) { var t = Task.Run(() => { DateTime dat = DateTime.Now; if (dat == DateTime.MinValue) throw new ArgumentException("The clock is not working.");
if (dat.Hour > 17) return "evening"; else if (dat.Hour > 12) return "afternoon"; else return "morning"; }); var c = t.ContinueWith((antecedent) => { if (antecedent.Status == TaskStatus.RanToCompletion) { Console.WriteLine("Good {0}!", antecedent.Result); Console.WriteLine("And how are you this fine {0}?", antecedent.Result); } else if (antecedent.Status == TaskStatus.Faulted) { Console.WriteLine(t.Exception.GetBaseException().Message); } }); c.Wait(); } }}Altro esempio: continuazione condizionale.
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace DifferentContinuationTasks{ class Program { static void Main(string[] args) { Task<int> t = Task.Run(() => { //se il numero è messo a zero viene sollevata un'eccezione non gestita int numero = 1; if (numero==0) { throw new Exception(); } return 32; }); t.ContinueWith((i) => { Console.WriteLine("Faulted"); }, TaskContinuationOptions.OnlyOnFaulted); t.ContinueWith((i) => { Console.WriteLine("Canceled"); }, TaskContinuationOptions.OnlyOnCanceled);
var completedTask = t.ContinueWith((i) => { Console.WriteLine(i.Result); Console.WriteLine("Completed"); }, TaskContinuationOptions.OnlyOnRanToCompletion);
Console.WriteLine("Press Enter to terminate!"); Console.ReadLine(); } }}Per lâesempio precedente, nel caso in cui viene sollevata unâeccezione Visual Studio blocca il programma nel punto in cui câè lâeccezione non gestita. Se si fa partire lâapplicazione direttamente dallâeseguibile il programma non si blocca e va direttamente nel continuation task che ha come impostazione TaskContinuationOptions.OnlyOnFaulted. Si può eventualmente andare nelle opzioni di gestione dellâeccezione di Visual Studio per modificarne il comportamento.
Costrutti per la programmazione concorrente â uso di Task
Sezione intitolata âCostrutti per la programmazione concorrente â uso di Taskâ Costrutto fork/join
Sezione intitolata â Costrutto fork/joinâGli esempi di fork/join visti nella sezione thread possono essere realizzati con TPL, usando Task.Factory.StartNew e Wait/Task.WaitAll.
using System;using System.Threading.Tasks;
namespace CostruttiProgrammazioneConcorrenteTask{ class Program { static int A, B, C, D, E, F, G, H; static void Main(string[] args) { // Metodo classico di avvio di un nuovo task Task tsk1 = Task.Factory.StartNew(Proc1); Task tsk2 = Task.Factory.StartNew(Proc2);
A = 10; D = A + 5; Console.WriteLine("D: {0}", D); tsk1.Wait(); // Attende il completamento del task tsk1 G = E - D; Console.WriteLine("G: {0}", G); tsk2.Wait(); // Attende il completamento del task tsk2 H = F + G; Console.WriteLine("H: {0}", H); }
static void Proc1() { B = 20; E = B * 2; Console.WriteLine("E: {0}", E); } static void Proc2() { C = 30; F = C * C; Console.WriteLine("F: {0}", F); } }}Costrutto join(count)
Sezione intitolata âCostrutto join(count)âAnche join(count) è realizzabile con CountdownEvent e Task.Factory.StartNew; il codice è analogo allâimplementazione con thread.
using System;using System.Threading;using System.Threading.Tasks;
namespace JoinCountDemoTask{ class Program { static int A, B, C, D, E, F, Z; static CountdownEvent L = new CountdownEvent(3); static void Main(string[] args) { A = 10; Task t = Task.Factory.StartNew(Proc1); D = A * 4; Console.WriteLine("D: {0}", D); L.Signal(); // Decrementa il contatore L L.Wait(); // Attende che il contatore L sia uguale a zero Z = D + E + F; Console.WriteLine("Z: {0}", Z); } static void Proc1() { Console.WriteLine("Procedura n. 1"); B = A + 20; Task t = Task.Factory.StartNew(Proc2); E = B - 5; Console.WriteLine("E: {0}", E); L.Signal(); // Decrementa il contatore L } static void Proc2() { Console.WriteLine("Procedura n. 2"); C = A + B; F = C + 6; Console.WriteLine("F: {0}", F); L.Signal(); // Decrementa il contatore L } }}Costrutto cobegin/coend
Sezione intitolata âCostrutto cobegin/coendâCobegin/coend possono essere implementati con Task e Task.WaitAll o con Parallel.Invoke.
using System;using System.Threading.Tasks;
namespace ParallelDemoTask{ class Program { static int A, B, C, D, E, F, G, Z; static void Main(string[] args) { A = 1; // Esecuzione parallela delle tre procedure, equivalente a: // cobegin // Proc1(); // Proc2(); // Proc3(); // coend // Il metodo Invoke esegue le tre procedure, normalmente in parallelo. // Il main thread attende il completamento di tutte le procedure indicate. Task tsk1 = Task.Factory.StartNew(Proc1); Task tsk2 = Task.Factory.StartNew(Proc2); Task tsk3 = Task.Factory.StartNew(Proc3); // Il main thread attende il completamento di tutti i task Task.WaitAll(tsk1, tsk2, tsk3); Z = E + F + G; Console.WriteLine("Z: {0}", Z); }
static void Proc1() { B = 2; E = A + B; Console.WriteLine("E: {0}", E); } static void Proc2() { C = 3; F = A + 3 * C; Console.WriteLine("F: {0}", F); } static void Proc3() { D = 4; G = 2 * D - A; Console.WriteLine("G: {0}", G); } }}Un esempio di utilizzo di Semafori con i Task:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading;using System.Threading.Tasks;
namespace SemaforoDemo01{ class Program { private static SemaphoreSlim semaphore; // A padding interval to make the output more orderly. private static int padding;
public static void Main() { // Create the semaphore. semaphore = new SemaphoreSlim(0, 3); Console.WriteLine("{0} tasks can enter the semaphore.", semaphore.CurrentCount); Task[] tasks = new Task[5];
// Create and start five numbered tasks. for (int i = 0; i <= 4; i++) { tasks[i] = Task.Run(() => { // Each task begins by requesting the semaphore. Console.WriteLine("Task {0} begins and waits for the semaphore.", Task.CurrentId);
//blocca il thread/task corrente in attesa di entrare nel semaforo semaphore.Wait(); //https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked?view=netframework-4.8 //Adds two integers and replaces the first integer with the sum, as an atomic operation. Interlocked.Add(ref padding, 100);
Console.WriteLine("Task {0} enters the semaphore.", Task.CurrentId);
// The task just sleeps for 1+ seconds. Task.Delay(1000 + padding).Wait();
Console.WriteLine("Task {0} releases the semaphore; previous count: {1}.", Task.CurrentId, semaphore.Release());//rilascio il semaforo e incrementa il contatore }); }
// Wait for half a second, to allow all the tasks to start and block. Thread.Sleep(500);
// Restore the semaphore count to its maximum value. Console.Write("Main thread calls Release(3) --> "); //rilascia il semaforo del numero specificato di valori semaphore.Release(3); Console.WriteLine("{0} tasks can enter the semaphore.", semaphore.CurrentCount); // Main thread waits for the tasks to complete. Task.WaitAll(tasks);
Console.WriteLine("Main thread exits."); Console.ReadLine(); } }}Parallelismo dei dati (Task Parallel Library)
Sezione intitolata âParallelismo dei dati (Task Parallel Library)âCon âparallelismo dei datiâ si intende eseguire contemporaneamente la stessa operazione sugli elementi di una matrice o raccolta. La raccolta viene partizionata in segmenti eseguiti da thread diversi. TPL supporta il parallelismo dei dati tramite System.Threading.Tasks.Parallel, che fornisce cicli paralleli Parallel.For e Parallel.ForEach. La scrittura della logica è simile ai cicli sequenziali; non è necessario creare thread o gestire manualmente il lavoro di basso livello.
Esempi Parallel.For e Parallel.ForEach
Sezione intitolata âEsempi Parallel.For e Parallel.ForEachâusing System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading;using System.Threading.Tasks;using System.IO;
namespace ParallelForDemo{ class Program { public static void Main() { long totalSize = 0;
String[] args = Environment.GetCommandLineArgs(); if (args.Length == 1) { Console.WriteLine("There are no command line arguments."); return; } if (!Directory.Exists(args[1])) { Console.WriteLine("The directory does not exist."); return; }
String[] files = Directory.GetFiles(args[1]); Parallel.For(0, files.Length, (index) => { FileInfo fi = new FileInfo(files[index]); long size = fi.Length; Interlocked.Add(ref totalSize, size); }); Console.WriteLine("Directory '{0}':", args[1]); Console.WriteLine("{0:N0} files, {1:N0} bytes", files.Length, totalSize); } }}Nei cicli paralleli è possibile usare variabili partition-local per accumuli locali e poi combinare il risultato in modo thread-safe (es. Interlocked.Add).
using System;using System.Diagnostics;using System.Runtime.InteropServices;using System.Threading.Tasks;
namespace ParallelMatricesMultiply{ class Program { #region Sequential_Loop static void MultiplyMatricesSequential(double[,] matA, double[,] matB, double[,] result) { int matACols = matA.GetLength(1); int matBCols = matB.GetLength(1); int matARows = matA.GetLength(0);
for (int i = 0; i < matARows; i++) { for (int j = 0; j < matBCols; j++) { double temp = 0; for (int k = 0; k < matACols; k++) { // Accesso a matB[k, j] è lento per grandi matrici (salta righe) temp += matA[i, k] * matB[k, j]; } result[i, j] = temp; } } } #endregion
#region Parallel_Loop_Optimized
// Helper per trasporre la matrice. // Trasforma le colonne in righe per migliorare la "Cache Locality". static double[,] TransposeMatrixParallel(double[,] matrix) { int rows = matrix.GetLength(0); int cols = matrix.GetLength(1); double[,] result = new double[cols, rows];
Parallel.For(0, rows, i => { for (int j = 0; j < cols; j++) { result[j, i] = matrix[i, j]; } });
return result; }
static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result) { int matACols = matA.GetLength(1); int matBCols = matB.GetLength(1); int matARows = matA.GetLength(0);
// PASSO 1: Trasposizione // Costo iniziale aggiuntivo, ma guadagno massiccio durante i calcoli. // Ora scorrere una "colonna" di B equivale a scorrere una riga di matBTransposed. double[,] matBTransposed = TransposeMatrixParallel(matB);
// PASSO 2: Moltiplicazione Parallela Parallel.For(0, matARows, i => { for (int j = 0; j < matBCols; j++) { double temp = 0; for (int k = 0; k < matACols; k++) { // OTTIMIZZAZIONE QUI: // Invece di matB[k, j], usiamo matBTransposed[j, k]. // Entrambi gli accessi (matA e matBTransposed) ora incrementano 'k' (l'indice destro). // Questo significa che stiamo leggendo memoria contigua. temp += matA[i, k] * matBTransposed[j, k]; } result[i, j] = temp; } }); } #endregion
#region Main static void Main(string[] args) { // Set up matrices. int colCount = 1000; int rowCount = 1000; int colCount2 = 1000;
Console.WriteLine($"Inizializzazione matrici {rowCount}x{colCount}..."); double[,] m1 = InitializeMatrix(rowCount, colCount); double[,] m2 = InitializeMatrix(colCount, colCount2); double[,] result = new double[rowCount, colCount2];
// --- SEQUENTIAL --- Console.Error.WriteLine("Executing sequential loop..."); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start();
MultiplyMatricesSequential(m1, m2, result); stopwatch.Stop(); long sequentialTime = stopwatch.ElapsedMilliseconds; Console.Error.WriteLine("Sequential loop time in milliseconds: {0}", sequentialTime);
// Reset timer and results matrix. stopwatch.Reset(); result = new double[rowCount, colCount2];
// Garbage Collection forzato per pulire la memoria prima del test parallelo GC.Collect(); GC.WaitForPendingFinalizers();
// --- PARALLEL (Optimized) --- Console.Error.WriteLine("Executing parallel loop (with Transposition)..."); stopwatch.Start();
// Nota: Il tempo includerĂ anche il tempo necessario per trasporre la matrice! MultiplyMatricesParallel(m1, m2, result);
stopwatch.Stop(); long parallelTime = stopwatch.ElapsedMilliseconds; Console.Error.WriteLine("Parallel loop time in milliseconds: {0}", parallelTime);
// --- RESULTS --- double speedup = (double)sequentialTime / parallelTime; Console.WriteLine($"Speedup = {speedup:F2}; il calcolo parallelo ottimizzato è {speedup:F2} volte piÚ veloce.");
Console.Error.WriteLine("Press any key to exit."); Console.ReadKey(); } #endregion
#region Helper_Methods static double[,] InitializeMatrix(int rows, int cols) { double[,] matrix = new double[rows, cols]; Random r = new Random(); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { matrix[i, j] = r.Next(100); } } return matrix; } #endregion }}Child task attached e detached
Sezione intitolata âChild task attached e detachedâUn child task (o nested task) è un Task creato allâinterno del delegato di un altro Task (parent). Un child può essere detached o attached. Un detached child esegue indipendentemente dal parent; un attached child creato con TaskCreationOptions.AttachedToParent fa sĂŹ che il parent attenda il completamento dei figli e propaghi le eccezioni. Per la maggior parte degli scenari si raccomandano detached child perchĂŠ relazioni meno complesse: i task creati dentro altri task sono detached per default.
using System;using System.Threading;using System.Threading.Tasks;
public class Example{ public static void Main() { var parent = Task.Factory.StartNew(() => { Console.WriteLine("Outer task executing.");
var child = Task.Factory.StartNew(() => { Console.WriteLine("Nested task starting."); Thread.SpinWait(500000); Console.WriteLine("Nested task completing."); }); });
parent.Wait(); Console.WriteLine("Outer has completed."); }}// The example produces output like the following:// Outer task executing.// Nested task starting.// Outer has completed.// Nested task completing.Il metodo Thread.SpinWait(500000) serve a simulare un carico di lavoro (o un ritardo) mantenendo la CPU occupata.
-
Che cosa fa tecnicamente
Thread.SpinWaitforza il processore a eseguire un ciclo stretto (loop) per un numero specifico di iterazioni (in questo caso, 500.000).-
Busy Waiting: A differenza di una pausa âclassicaâ, il thread non si âaddormentaâ. Rimane attivo e continua a consumare cicli della CPU.
-
Iterazioni, non Tempo: Il numero
500000non rappresenta millisecondi o secondi, ma cicli di iterazione. La durata reale in termini di tempo dipende dalla velocitĂ del processore del computer su cui gira il codice.
-
-
Differenza tra
SpinWaiteSleepĂ fondamentale distinguere questo metodo da
Thread.Sleep(), che è molto piĂš comune.Caratteristica Thread.Sleep(x) Thread.SpinWait(x) Stato della CPU Il thread cede la CPU ad altri processi (âriposaâ). Il thread tiene la CPU impegnata al 100% (âgira a vuotoâ). Utilizzo ideale Attese lunghe (es. database, download). Attese brevissime (pochi microsecondi). Costo Alto overhead (context switch). Basso overhead (nessun context switch). -
PerchĂŠ viene usato in questo esempio?
In questo specifico codice,
SpinWaitviene usato per uno scopo didattico preciso: Creare un ritardo prevedibile senza sospendere il thread.Lâobiettivo dellâesempio è dimostrare che il Parent Task (Task esterno) termina prima del Child Task (Task annidato).
-
Il
Child Taskinizia. -
Esegue
SpinWait(simula un lavoro breve). -
Nel frattempo, il
Parent Taskfinisce la sua esecuzione perchĂŠ, di default, i task annidati sono indipendenti (Detached).
Se si usasse
Thread.Sleep, il sistema operativo potrebbe decidere di mettere in pausa il thread del Child e dare prioritĂ al Parent, rendendo lâordine di output meno deterministico o semplicemente ârallentandoâ lâesecuzione in modo diverso.SpinWaitassicura che quel thread stia âlavorandoâ per quel breve lasso di tempo, rendendo evidente che il Parent non lo sta aspettando. -
-
Sintesi dellâoutput
Il codice produce quellâoutput perchĂŠ il
parent.Wait()aspetta solo il task genitore. PoichĂŠ il task figlio non è âattaccatoâ (AttachedToParent), il genitore finisce, stampa âOuter has completedâ, e solo dopo il figlio finisce il suo âSpinWaitâ e stampa âNested task completingâ.
Se il child task è un oggetto Task<TResult> invece di un Task, è possibile assicurarsi che il parent attenda il completamento del child accedendo alla proprietĂ Task<TResult>.Result del child anche se è un detached child task. La proprietĂ Result blocca fino al completamento del task, come nellâesempio seguente.
using System;using System.Threading;using System.Threading.Tasks;
class Example{ static void Main() { var outer = Task<int>.Factory.StartNew(() => { Console.WriteLine("Outer task executing.");
var nested = Task<int>.Factory.StartNew(() => { Console.WriteLine("Nested task starting."); Thread.SpinWait(5000000); Console.WriteLine("Nested task completing."); return 42; });
// Parent will wait for this detached child. return nested.Result; });
Console.WriteLine("Outer has returned {0}.", outer.Result); }}// The example displays the following output:// Outer task executing.// Nested task starting.// Nested task completing.// Outer has returned 42.Attached child tasks
Sezione intitolata âAttached child tasksâA differenza dei detached child tasks, gli attached child tasks sono strettamente sincronizzati con il parent. Ă possibile modificare il detached child task nellâesempio precedente in un attached child task usando lâopzione TaskCreationOptions.AttachedToParent nella dichiarazione di creazione del task, come mostrato nel seguente esempio. In questo codice, lâattivitĂ figlia allegata viene completata prima del suo genitore. Di conseguenza, lâoutput dellâesempio è lo stesso ogni volta che si esegue il codice.
using System;using System.Threading;using System.Threading.Tasks;
public class Example{ public static void Main() { var parent = Task.Factory.StartNew(() => { Console.WriteLine("Parent task executing."); var child = Task.Factory.StartNew(() => { Console.WriteLine("Attached child starting."); Thread.SpinWait(5000000); Console.WriteLine("Attached child completing."); }, TaskCreationOptions.AttachedToParent); }); parent.Wait(); Console.WriteLine("Parent has completed."); }}// The example displays the following output:// Parent task executing.// Attached child starting.// Attached child completing.// Parent has completed.Ă possibile utilizzare attached child tasks per creare grafi di operazioni asincrone strettamente sincronizzate.
Tuttavia, unâattivitĂ figlia può attaccarsi al suo genitore solo se questâultimo non lo proibisce. I task genitore possono esplicitamente impedire lâattacco di task figli a loro specificando lâopzione TaskCreationOptions.DenyChildAttach nel costruttore della classe del task genitore o nel metodo TaskFactory.StartNew. I task genitore impediscono implicitamente lâattacco di task figli se vengono creati chiamando il metodo Task.Run. Lâesempio seguente illustra questo concetto. Ă identico al precedente, tranne per il fatto che il task genitore è creato chiamando il metodo Task.Run(Action) invece del metodo TaskFactory.StartNew(Action). PoichĂŠ il task figlio non può attaccarsi al suo genitore, lâoutput dellâesempio è imprevedibile. Questo esempio è funzionalmente equivalente al primo esempio nella sezione âDetached child tasksâ.
using System;using System.Threading;using System.Threading.Tasks;
public class Example{ public static void Main() { var parent = Task.Run(() => { Console.WriteLine("Parent task executing."); var child = Task.Factory.StartNew(() => { Console.WriteLine("Child starting."); Thread.SpinWait(5000000); Console.WriteLine("Child completing."); }, TaskCreationOptions.AttachedToParent); }); parent.Wait(); Console.WriteLine("Parent has completed."); }}// The example displays output like the following:// Parent task executing// Parent has completed.// Attached child starting.Cancellazione di unâattivitĂ
Sezione intitolata âCancellazione di unâattivitĂ âhttps://docs.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads
A partire da .NET Framework 4 si usa un modello unificato per lâannullamento cooperativo basato su CancellationTokenSource e CancellationToken. Chi crea il token chiede lâannullamento; le operazioni che ricevono il token devono cooperare per osservare la richiesta e terminare in modo appropriato.
Linee guida generali:
- creare unâistanza di un oggetto CancellationTokenSource, che gestisce e invia la notifica di annullamento ai singoli token di annullamento.
- passare il token restituito dalla proprietĂ CancellationTokenSource.Token a ogni attivitĂ o thread in attesa di annullamento.
- specificare un meccanismo per ogni attivitĂ o thread per rispondere allâannullamento.
- chiamare il metodo CancellationTokenSource.Cancel per fornire la notifica di annullamento.
Esempi di cancellazione
Sezione intitolata âEsempi di cancellazioneâusing System;using System.Threading;using System.Threading.Tasks;
namespace TaskCancellationDemo{ class Program {//usare async Task al posto di void se si usa await task al posto di task.Wait() static async Task Main(string[] args) { var tokenSource = new CancellationTokenSource(); CancellationToken ct = tokenSource.Token;
var task = Task.Run(() => { // Were we already canceled? ct.ThrowIfCancellationRequested();
bool moreToDo = true; while (moreToDo) { // Poll on this property if you have to do // other cleanup before throwing. if (ct.IsCancellationRequested) { // Clean up here, then... ct.ThrowIfCancellationRequested(); }
} }, ct); // Pass same token to Task.Run. Thread.Sleep(1000); tokenSource.Cancel();
// Just continue on this thread, or await with try-catch: try { //https://stackoverflow.com/questions/13140523/await-vs-task-wait-deadlock //https://stackoverflow.com/questions/7340309/throw-exception-inside-a-task-await-vs-wait //task.Wait(); await task; } catch (OperationCanceledException e) { Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {e.Message}"); } catch (AggregateException e) { Console.WriteLine($"{nameof(AggregateException)} thrown with message: {e.Message}"); } finally { tokenSource.Dispose(); } }
}}Cancellare un task e i suoi figli
Sezione intitolata âCancellare un task e i suoi figliâĂ possibile creare task cancellabili passando il token al delegato e al metodo Task.Run; il delegato deve rilevare la richiesta e lanciare OperationCanceledException se opportuno. Se si usa Wait o WhenAll su task cancellati, è necessario gestire le eccezioni in try/catch (AggregateException/OperationCanceledException).
using System;using System.Threading;using System.Threading.Tasks;using System.Collections.Concurrent;
namespace TaskAndSonsCancellationDemo{ public class Program { public static async Task Main() { var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token;
// Store references to the tasks so that we can wait on them and // observe their status after cancellation. Task t; //https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentbag-1 var tasks = new ConcurrentBag<Task>();
Console.WriteLine("Press any key to begin tasks..."); Console.ReadKey(true); Console.WriteLine("To terminate the example, press 'c' to cancel and exit..."); Console.WriteLine();
// Request cancellation of a single task when the token source is canceled. // Pass the token to the user delegate, and optionally to the task instance. t = Task.Run(() => DoSomeWork(1, token), token); Console.WriteLine("Task {0} executing", t.Id); tasks.Add(t);
// Request cancellation of a task and its children. Note the token is passed // to (1) the user delegate and (2) as the second argument to Task.Run, so // that the task instance can correctly handle the OperationCanceledException. t = Task.Run(() => { // Create some cancelable child tasks. Task tc; for (int i = 3; i <= 10; i++) { // For each child task, pass the same token // to each user delegate and to Task.Run. tc = Task.Run(() => DoSomeWork(i, token), token); Console.WriteLine("Task {0} executing", tc.Id); tasks.Add(tc); // Pass the same token again to do work on the parent task. // All will be signaled by the call to tokenSource.Cancel below. //DoSomeWork(2, token); } }, token);
Console.WriteLine("Task {0} executing", t.Id); tasks.Add(t);
// Request cancellation from the UI thread. char ch = Console.ReadKey().KeyChar; if (ch == 'c' || ch == 'C') { tokenSource.Cancel(); Console.WriteLine("\nTask cancellation requested.");
// Optional: Observe the change in the Status property on the task. // It is not necessary to wait on tasks that have canceled. However, // if you do wait, you must enclose the call in a try/catch block to // catch the TaskCanceledExceptions that are thrown. If you do // not wait, no exception is thrown if the token that was passed to the // Task.Run method is the same token that requested the cancellation. }
try { await Task.WhenAll(tasks.ToArray()); } //in alternativa allâuso di await si può usare una Wait, ma in tal caso bisogna gestire lâAggregateException //try //{ // Task myTask = Task.WhenAll(tasks.ToArray()); // myTask.Wait(); //} //catch (AggregateException ae) //{ // foreach (var e in ae.Flatten().InnerExceptions) // { // if (e is TaskCanceledException) // { // Console.WriteLine($"\n{nameof(TaskCanceledException)} thrown\n"); // } // else if (e is CustomException) // { // Console.WriteLine($"\n{nameof(CustomException)} thrown\n"); // } // else if (e is CustomSonException) // { // Console.WriteLine($"\n{nameof(CustomSonException)} thrown\n"); // } // else // { // throw; // } // } //} catch (OperationCanceledException) { Console.WriteLine($"\n{nameof(OperationCanceledException)} thrown\n"); } finally { tokenSource.Dispose(); }
// Display status of all tasks. foreach (var task in tasks) Console.WriteLine("Task {0} status is now {1}", task.Id, task.Status); }
static void DoSomeWork(int taskNum, CancellationToken ct) { // Was cancellation already requested? if (ct.IsCancellationRequested) { Console.WriteLine("Task {0} was cancelled before it got started.", taskNum); ct.ThrowIfCancellationRequested(); }
int maxIterations = 100;
// NOTE!!! A "TaskCanceledException was unhandled // by user code" error will be raised here if "Just My Code" // is enabled on your computer. On Express editions JMC is // enabled and cannot be disabled. The exception is benign. // Just press F5 to continue executing your code. for (int i = 0; i <= maxIterations; i++) { // Do a bit of work. Not too much. var sw = new SpinWait(); for (int j = 0; j <= 100; j++) //https://stackoverflow.com/questions/1091135/whats-the-purpose-of-thread-spinwait-method sw.SpinOnce();
if (ct.IsCancellationRequested) { Console.WriteLine("Task {0} cancelled", taskNum); ct.ThrowIfCancellationRequested(); } } } }
}// The example displays output like the following:// Press any key to begin tasks...// To terminate the example, press 'c' to cancel and exit...//// Task 1 executing// Task 2 executing// Task 3 executing// Task 4 executing// Task 5 executing// Task 6 executing// Task 7 executing// Task 8 executing// c// Task cancellation requested.// Task 2 cancelled// Task 7 cancelled//// OperationCanceledException thrown//// Task 2 status is now Canceled// Task 1 status is now RanToCompletion// Task 8 status is now Canceled// Task 7 status is now Canceled// Task 6 status is now RanToCompletion// Task 5 status is now RanToCompletion// Task 4 status is now RanToCompletion// Task 3 status is now RanToCompletionAttached child tasks e AggregateException annidate
Sezione intitolata âAttached child tasks e AggregateException annidateâSe un task ha un attached child task che genera unâeccezione, tale eccezione viene incapsulata in unâAggregateException prima di essere propagata al task genitore, che a sua volta la incapsula in una propria AggregateException prima di propagarsi al chiamante. In questi casi, la proprietĂ InnerExceptions dellâeccezione AggregateException catturata contiene una o piĂš istanze di AggregateException, non le eccezioni originali. Per evitare di dover iterare su AggregateException annidate, è possibile utilizzare il metodo Flatten per rimuovere tutte le AggregateException annidate, in modo che la proprietĂ InnerExceptions contenga le eccezioni originali.
using System;using System.Threading.Tasks;
public class Example{ public static void Main() { var task1 = Task.Run(() => { var nested1 = Task.Run(() => { throw new CustomException("Attached child task faulted."); });
// Here the exception will be escalated back to the calling thread. // We could use try/catch here to prevent that. nested1.Wait(); });
try { task1.Wait(); } catch (AggregateException ae) { foreach (var e in ae.Flatten().InnerExceptions) { if (e is CustomException) { Console.WriteLine(e.Message); } else { throw; } } } }}
public class CustomException : Exception{ public CustomException(String message) : base(message) {}}// The example displays the following output:// Attached child task faulted.Altro esempio:
using System;using System.Threading.Tasks;namespace TaskExceptionsDemos{ internal class Program { static void Main(string[] args) {
var task1 = Task.Factory.StartNew(() => { var childTask1 = Task.Factory.StartNew(()=> { throw new CustomSonException("This son exception is expected!"); }, TaskCreationOptions.AttachedToParent); throw new CustomException("This exception is expected!"); });
try { task1.Wait(); } catch (AggregateException ae) { foreach (var e in ae.Flatten().InnerExceptions) { if (e is TaskCanceledException) { Console.WriteLine($"{nameof(TaskCanceledException)} thrown"); Console.WriteLine(e.Message); } else if (e is CustomException) { Console.WriteLine($"{nameof(CustomException)} thrown"); Console.WriteLine(e.Message); } else if (e is CustomSonException) { Console.WriteLine($"{nameof(CustomSonException)} thrown"); Console.WriteLine(e.Message); } else { throw; } } }
} }
internal class CustomSonException : Exception {
public CustomSonException(string? message) : base(message) { }
}
internal class CustomException : Exception {
public CustomException(string? message) : base(message) { }
}}Eccezioni da detached child tasks
Sezione intitolata âEccezioni da detached child tasksâPer default, i task figli sono creati come detached. Le eccezioni generate da task detached devono essere gestite o rilanciate nel task genitore immediato; non vengono propagate al chiamante allo stesso modo delle eccezioni generate da task attached. Il task genitore può rilanciare manualmente unâeccezione da un task detached per far sĂŹ che venga incapsulata in unâAggregateException e propagata al chiamante.
using System;using System.Threading.Tasks;
public class Example{ public static void Main() { var task1 = Task.Run(() => { var nested1 = Task.Run(() => { throw new CustomException("Detached child task faulted."); });
// Here the exception will be escalated back to the calling thread. // We could use try/catch here to prevent that. nested1.Wait(); });
try { task1.Wait(); } catch (AggregateException ae) { foreach (var e in ae.Flatten().InnerExceptions) { if (e is CustomException) { Console.WriteLine(e.Message); } } } }}
public class CustomException : Exception{ public CustomException(String message) : base(message) {}}// The example displays the following output:// Detached child task faulted.