C# Language
threading
Ricerca…
Osservazioni
Un thread è una parte di un programma che può essere eseguito indipendentemente dalle altre parti. Può eseguire attività contemporaneamente con altri thread. Il multithreading è una funzionalità che consente ai programmi di eseguire l'elaborazione simultanea in modo che sia possibile eseguire più di una operazione alla volta.
Ad esempio, è possibile utilizzare il threading per aggiornare un timer o un contatore in background mentre si eseguono contemporaneamente altre attività in primo piano.
Le applicazioni multithread sono più reattive all'input dell'utente e sono anche facilmente scalabili, poiché lo sviluppatore può aggiungere thread come e quando il carico di lavoro aumenta.
Per impostazione predefinita, un programma C # ha un thread, il thread del programma principale. Tuttavia, i thread secondari possono essere creati e utilizzati per eseguire codice in parallelo con il thread principale. Tali thread sono chiamati thread di lavoro.
Per controllare l'operazione di un thread, CLR delega una funzione al sistema operativo noto come Thread Scheduler. Uno scheduler di thread assicura che tutti i thread siano assegnati al tempo di esecuzione corretto. Controlla inoltre che i thread bloccati o bloccati non consumino gran parte del tempo di CPU.
.NET Framework System.Threading
namespace semplifica l'utilizzo dei thread. System.Threading consente il multithreading fornendo un numero di classi e interfacce. Oltre a fornire tipi e classi per un particolare thread, definisce anche i tipi per contenere una collezione di thread, una classe timer e così via. Fornisce inoltre il suo supporto consentendo l'accesso sincronizzato ai dati condivisi.
Thread
è la classe principale nello spazio dei nomi System.Threading
. Altre classi includono AutoResetEvent
, Interlocked
, Monitor
, Mutex
e ThreadPool
.
Alcuni dei delegati presenti nello spazio dei nomi System.Threading
includono ThreadStart
, TimerCallback
e WaitCallback
.
Le enumerazioni nello spazio dei nomi System.Threading
includono ThreadPriority
, ThreadState
e EventResetMode
.
In .NET Framework 4 e versioni successive, la programmazione multithreading è resa più semplice e più semplice attraverso le classi System.Threading.Tasks.Parallel
e System.Threading.Tasks.Task
, Parallel LINQ (PLINQ), nuove classi di raccolta simultanee in System.Collections.Concurrent
nomi System.Collections.Concurrent
e un nuovo modello di programmazione basato sulle attività.
Semplice demo di threading completa
class Program
{
static void Main(string[] args)
{
// Create 2 thread objects. We're using delegates because we need to pass
// parameters to the threads.
var thread1 = new Thread(new ThreadStart(() => PerformAction(1)));
var thread2 = new Thread(new ThreadStart(() => PerformAction(2)));
// Start the threads running
thread1.Start();
// NB: as soon as the above line kicks off the thread, the next line starts;
// even if thread1 is still processing.
thread2.Start();
// Wait for thread1 to complete before continuing
thread1.Join();
// Wait for thread2 to complete before continuing
thread2.Join();
Console.WriteLine("Done");
Console.ReadKey();
}
// Simple method to help demonstrate the threads running in parallel.
static void PerformAction(int id)
{
var rnd = new Random(id);
for (int i = 0; i < 100; i++)
{
Console.WriteLine("Thread: {0}: {1}", id, i);
Thread.Sleep(rnd.Next(0, 1000));
}
}
}
Demo di threading completo semplice tramite l'attività
class Program
{
static void Main(string[] args)
{
// Run 2 Tasks.
var task1 = Task.Run(() => PerformAction(1)));
var task2 = Task.Run(() => PerformAction(2)));
// Wait (i.e. block this thread) until both Tasks are complete.
Task.WaitAll(new [] { task1, task2 });
Console.WriteLine("Done");
Console.ReadKey();
}
// Simple method to help demonstrate the threads running in parallel.
static void PerformAction(int id)
{
var rnd = new Random(id);
for (int i = 0; i < 100; i++)
{
Console.WriteLine("Task: {0}: {1}", id, i);
Thread.Sleep(rnd.Next(0, 1000));
}
}
}
Esplicito compito Parallism
private static void explicitTaskParallism()
{
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 task {nameof(taskA)}."));
Task taskB = new Task(() => Console.WriteLine($"Hello from task {nameof(taskB)}."));
// Start the task.
taskA.Start();
taskB.Start();
// Output a message from the calling thread.
Console.WriteLine("Hello from thread '{0}'.",
Thread.CurrentThread.Name);
taskA.Wait();
taskB.Wait();
Console.Read();
}
Parallelismo di attività implicite
private static void Main(string[] args)
{
var a = new A();
var b = new B();
//implicit task parallelism
Parallel.Invoke(
() => a.DoSomeWork(),
() => b.DoSomeOtherWork()
);
}
Creazione e avvio di una seconda discussione
Se stai eseguendo più calcoli lunghi, puoi eseguirli contemporaneamente su diversi thread sul tuo computer. Per fare ciò, creiamo un nuovo Thread e puntiamo a un metodo diverso.
using System.Threading;
class MainClass {
static void Main() {
var thread = new Thread(Secondary);
thread.Start();
}
static void Secondary() {
System.Console.WriteLine("Hello World!");
}
}
Avvio di una discussione con parametri
using System.Threading;
class MainClass {
static void Main() {
var thread = new Thread(Secondary);
thread.Start("SecondThread");
}
static void Secondary(object threadName) {
System.Console.WriteLine("Hello World from thread: " + threadName);
}
}
Creazione di un thread per processore
Environment.ProcessorCount
Ottiene il numero di processori logici sulla macchina corrente.
Il CLR pianificherà quindi ogni thread su un processore logico, questo teoricamente potrebbe significare ogni thread su un processore logico diverso, tutti i thread su un singolo processore logico o qualche altra combinazione.
using System;
using System.Threading;
class MainClass {
static void Main() {
for (int i = 0; i < Environment.ProcessorCount; i++) {
var thread = new Thread(Secondary);
thread.Start(i);
}
}
static void Secondary(object threadNumber) {
System.Console.WriteLine("Hello World from thread: " + threadNumber);
}
}
Evitare di leggere e scrivere dati contemporaneamente
A volte, vuoi che i tuoi thread condividano simultaneamente i dati. Quando ciò accade, è importante conoscere il codice e bloccare le parti che potrebbero andare storte. Di seguito viene mostrato un semplice esempio di conteggio di due thread.
Ecco un codice pericoloso (errato):
using System.Threading;
class MainClass
{
static int count { get; set; }
static void Main()
{
for (int i = 1; i <= 2; i++)
{
var thread = new Thread(ThreadMethod);
thread.Start(i);
Thread.Sleep(500);
}
}
static void ThreadMethod(object threadNumber)
{
while (true)
{
var temp = count;
System.Console.WriteLine("Thread " + threadNumber + ": Reading the value of count.");
Thread.Sleep(1000);
count = temp + 1;
System.Console.WriteLine("Thread " + threadNumber + ": Incrementing the value of count to:" + count);
Thread.Sleep(1000);
}
}
}
Noterai, invece di contare 1,2,3,4,5 ... contiamo 1,1,2,2,3 ...
Per risolvere questo problema, dobbiamo bloccare il valore del conteggio, in modo che più thread diversi non possano leggere e scrivere allo stesso tempo. Con l'aggiunta di un blocco e una chiave, possiamo impedire ai thread di accedere ai dati simultaneamente.
using System.Threading;
class MainClass
{
static int count { get; set; }
static readonly object key = new object();
static void Main()
{
for (int i = 1; i <= 2; i++)
{
var thread = new Thread(ThreadMethod);
thread.Start(i);
Thread.Sleep(500);
}
}
static void ThreadMethod(object threadNumber)
{
while (true)
{
lock (key)
{
var temp = count;
System.Console.WriteLine("Thread " + threadNumber + ": Reading the value of count.");
Thread.Sleep(1000);
count = temp + 1;
System.Console.WriteLine("Thread " + threadNumber + ": Incrementing the value of count to:" + count);
}
Thread.Sleep(1000);
}
}
}
Parallelo. Per ogni ciclo
Se si dispone di un ciclo foreach che si desidera velocizzare e non si preoccupa di quale ordine è l'output, è possibile convertirlo in un ciclo foreach parallelo attenendosi alla seguente procedura:
using System;
using System.Threading;
using System.Threading.Tasks;
public class MainClass {
public static void Main() {
int[] Numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Single-threaded
Console.WriteLine("Normal foreach loop: ");
foreach (var number in Numbers) {
Console.WriteLine(longCalculation(number));
}
// This is the Parallel (Multi-threaded solution)
Console.WriteLine("Parallel foreach loop: ");
Parallel.ForEach(Numbers, number => {
Console.WriteLine(longCalculation(number));
});
}
private static int longCalculation(int number) {
Thread.Sleep(1000); // Sleep to simulate a long calculation
return number * number;
}
}
Deadlock (due thread in attesa l'uno dell'altro)
Un deadlock è ciò che si verifica quando due o più thread sono in attesa di completamento o di rilascio di una risorsa in modo tale da attendere per sempre.
Uno scenario tipico di due thread in attesa di completamento è quando un thread GUI di Windows Form attende un thread di lavoro e il thread worker tenta di richiamare un oggetto gestito dal thread della GUI. Osservare che con questo codice exmaple, facendo clic su button1 si bloccherà il programma.
private void button1_Click(object sender, EventArgs e)
{
Thread workerthread= new Thread(dowork);
workerthread.Start();
workerthread.Join();
// Do something after
}
private void dowork()
{
// Do something before
textBox1.Invoke(new Action(() => textBox1.Text = "Some Text"));
// Do something after
}
workerthread.Join()
è una chiamata che blocca il thread chiamante fino al completamento di workthread. textBox1.Invoke(invoke_delegate)
è una chiamata che blocca il thread chiamante finché il thread della GUI non ha elaborato invoke_delegate, ma questa chiamata provoca deadlock se il thread della GUI è già in attesa del completamento del thread chiamante.
Per ovviare a questo, si può usare un modo non bloccante di invocare la casella di testo invece:
private void dowork()
{
// Do work
textBox1.BeginInvoke(new Action(() => textBox1.Text = "Some Text"));
// Do work that is not dependent on textBox1 being updated first
}
Tuttavia, ciò causerà problemi se è necessario eseguire il codice che dipende dalla casella di testo che viene aggiornata per prima. In tal caso, eseguilo come parte del richiamo, ma tieni presente che questo lo farà girare sul thread della GUI.
private void dowork()
{
// Do work
textBox1.BeginInvoke(new Action(() => {
textBox1.Text = "Some Text";
// Do work dependent on textBox1 being updated first,
// start another worker thread or raise an event
}));
// Do work that is not dependent on textBox1 being updated first
}
In alternativa, avvia un nuovo thread intero e lascia che quello faccia l'attesa sul thread della GUI, in modo che workthread possa essere completato.
private void dowork()
{
// Do work
Thread workerthread2 = new Thread(() =>
{
textBox1.Invoke(new Action(() => textBox1.Text = "Some Text"));
// Do work dependent on textBox1 being updated first,
// start another worker thread or raise an event
});
workerthread2.Start();
// Do work that is not dependent on textBox1 being updated first
}
Per ridurre al minimo il rischio di imbattersi in un deadlock di attesa reciproca, evitare sempre i riferimenti circolari tra i thread quando possibile. Una gerarchia di thread in cui i thread di livello inferiore lasciano solo i messaggi per i thread di livello superiore e non li attendono mai non si imbatteranno in questo tipo di problema. Tuttavia, sarebbe comunque vulnerabile ai deadlock basati sul blocco delle risorse.
Deadlock (mantieni la risorsa e aspetta)
Un deadlock è ciò che si verifica quando due o più thread sono in attesa di completamento o di rilascio di una risorsa in modo tale da attendere per sempre.
Se thread1 contiene un blocco sulla risorsa A e attende che la risorsa B venga rilasciata mentre thread2 contiene la risorsa B e attende che la risorsa A venga rilasciata, sono bloccati.
Facendo clic sul pulsante 1 per il seguente codice di esempio, la tua applicazione entrerà in uno stato di stallo imprevisto e si bloccherà
private void button_Click(object sender, EventArgs e)
{
DeadlockWorkers workers = new DeadlockWorkers();
workers.StartThreads();
textBox.Text = workers.GetResult();
}
private class DeadlockWorkers
{
Thread thread1, thread2;
object resourceA = new object();
object resourceB = new object();
string output;
public void StartThreads()
{
thread1 = new Thread(Thread1DoWork);
thread2 = new Thread(Thread2DoWork);
thread1.Start();
thread2.Start();
}
public string GetResult()
{
thread1.Join();
thread2.Join();
return output;
}
public void Thread1DoWork()
{
Thread.Sleep(100);
lock (resourceA)
{
Thread.Sleep(100);
lock (resourceB)
{
output += "T1#";
}
}
}
public void Thread2DoWork()
{
Thread.Sleep(100);
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
output += "T2#";
}
}
}
}
Per evitare di essere bloccati in questo modo, è possibile utilizzare Monitor.TryEnter (lock_object, timeout_in_milliseconds) per verificare se un blocco è già presente su un oggetto. Se Monitor.TryEnter non riesce ad acquisire un lock su lock_object prima di timeout_in_milliseconds, restituisce false, dando al thread la possibilità di rilasciare altre risorse detenute e cedendo, dando così agli altri thread la possibilità di completare come in questa versione leggermente modificata di quanto sopra :
private void button_Click(object sender, EventArgs e)
{
MonitorWorkers workers = new MonitorWorkers();
workers.StartThreads();
textBox.Text = workers.GetResult();
}
private class MonitorWorkers
{
Thread thread1, thread2;
object resourceA = new object();
object resourceB = new object();
string output;
public void StartThreads()
{
thread1 = new Thread(Thread1DoWork);
thread2 = new Thread(Thread2DoWork);
thread1.Start();
thread2.Start();
}
public string GetResult()
{
thread1.Join();
thread2.Join();
return output;
}
public void Thread1DoWork()
{
bool mustDoWork = true;
Thread.Sleep(100);
while (mustDoWork)
{
lock (resourceA)
{
Thread.Sleep(100);
if (Monitor.TryEnter(resourceB, 0))
{
output += "T1#";
mustDoWork = false;
Monitor.Exit(resourceB);
}
}
if (mustDoWork) Thread.Yield();
}
}
public void Thread2DoWork()
{
Thread.Sleep(100);
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
output += "T2#";
}
}
}
}
Si noti che questa soluzione alternativa si basa sul fatto che thread2 è testardo sui blocchi e che thread1 è disposto a fornire, in modo che thread2 abbia sempre la precedenza. Si noti inoltre che thread1 deve rifare il lavoro che ha svolto dopo aver bloccato la risorsa A, quando produce. Pertanto, sii cauto nell'implementare questo approccio con più di un thread di rendimento, poiché rischierai di entrare in un cosiddetto livelock - uno stato che si verificherebbe se due thread continuassero a fare il primo bit del loro lavoro e poi si cedere reciprocamente , ricominciare più volte.