mercredi 8 octobre 2025

Q/A Multithreading



🔹 1. Quelle est la différence entre un Thread et un Task en C# ?

✅ Thread :

  • Représente une unité d’exécution du système d’exploitation.

  • Créé et géré manuellement (via la classe Thread).

  • Plus bas-niveau : on contrôle directement le cycle de vie du thread.

  • Utilise plus de ressources (chaque thread a sa propre pile mémoire).

  • Exemple :

    Thread t = new Thread(() => Console.WriteLine("Thread running"));
    t.Start();
    t.Join(); // attendre la fin du thread
    

✅ Task :

  • Représente une unité de travail logique, non nécessairement liée à un thread unique.

  • Gérée par le ThreadPool.

  • Plus haut niveau : gère la synchronisation, les exceptions et le retour de valeur (Task<T>).

  • Plus adaptée à la programmation asynchrone et parallèle moderne.

  • Exemple :

    Task task = Task.Run(() => Console.WriteLine("Task running"));
    await task;
    

🧠 En résumé :

Thread = contrôle manuel, plus coûteux.
Task = abstraction haut niveau optimisée, intégrée au modèle async/await.


🔹 2. Comment créer et exécuter un thread en C# ?

Exemple :

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread t = new Thread(new ThreadStart(DoWork));
        t.Start(); // Démarre le thread
        t.Join();  // Attend la fin
    }

    static void DoWork()
    {
        Console.WriteLine($"Running on thread {Thread.CurrentThread.ManagedThreadId}");
    }
}

➡️ On peut aussi passer des paramètres avec ParameterizedThreadStart ou une lambda.


🔹 3. Expliquez l’utilisation de lock, Monitor et Mutex en C#.

Ces trois mécanismes servent à synchroniser l’accès à des ressources partagées entre plusieurs threads.

Mécanisme Portée Utilisation typique Exemple
lock Intra-processus Section critique simple lock(obj) { ... }
Monitor Intra-processus Équivalent à lock, mais plus flexible (Wait, Pulse) Monitor.Enter(obj); try {...} finally { Monitor.Exit(obj); }
Mutex Inter-processus Synchronisation entre plusieurs processus new Mutex(false, "Global\\MyMutex")

Exemple lock :

private static object _sync = new object();

lock (_sync)
{
    // Code thread-safe
}

Exemple Mutex :

using (var mutex = new Mutex(false, "Global\\MyApp"))
{
    if (!mutex.WaitOne(1000))
        Console.WriteLine("Instance déjà en cours");
    else
        Console.WriteLine("Instance unique");
}

🔹 4. Quelle est la différence entre Parallel.For et Task.Run ?

Critère Parallel.For Task.Run
Objectif Exécuter plusieurs itérations en parallèle Lancer une tâche asynchrone
Niveau Très haut niveau (boucles parallèles) Moyen niveau (unités de travail)
Gestion Automatique des threads via le ThreadPool Contrôle explicite de chaque tâche
Retour Bloquant (synchrone) Asynchrone (attendable avec await)

Exemple :

// Parallel.For : boucle parallèle
Parallel.For(0, 5, i =>
{
    Console.WriteLine($"Iteration {i} - Thread {Thread.CurrentThread.ManagedThreadId}");
});

// Task.Run : tâche asynchrone
await Task.Run(() => DoWork());

🔹 5. Qu’est-ce qu’un ThreadPool et quand l’utiliser ?

Le ThreadPool est un pool de threads réutilisables géré par le CLR.
Les Task, Parallel, async/await utilisent le ThreadPool.

Avantages :

  • Réduit le coût de création/destruction de threads.

  • Optimisé pour un grand nombre de petites tâches courtes.

Exemple :

ThreadPool.QueueUserWorkItem(_ => Console.WriteLine("ThreadPool work item"));

🧠 À utiliser pour : tâches courtes, nombreuses, indépendantes.
À éviter pour : tâches longues, bloquantes (préférer un Thread dédié).


🔹 6. Comment utiliser async et await pour la programmation asynchrone en C# ?

  • async : marque une méthode comme asynchrone.

  • await : suspend l’exécution jusqu’à la fin d’une tâche, sans bloquer le thread courant.

Exemple :

public async Task DownloadFileAsync()
{
    using (HttpClient client = new HttpClient())
    {
        string content = await client.GetStringAsync("https://example.com");
        Console.WriteLine(content);
    }
}

➡️ await libère le thread pendant l’attente et reprend l’exécution quand la tâche est terminée.


🔹 7. Qu’est-ce que la classe CancellationToken et comment l’utiliser ?

Permet d’annuler proprement des tâches asynchrones ou des opérations parallèles.

Exemple :

var cts = new CancellationTokenSource();
var token = cts.Token;

var task = Task.Run(() =>
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        Console.WriteLine(i);
        Thread.Sleep(500);
    }
}, token);

Thread.Sleep(2000);
cts.Cancel();

try { await task; }
catch (OperationCanceledException)
{
    Console.WriteLine("Tâche annulée");
}

🔹 8. Quelle est la différence entre le modèle synchrone et asynchrone ?

Type Comportement Avantage Inconvénient
Synchrone Le thread attend la fin de chaque opération Simple à comprendre Bloquant, peu scalable
Asynchrone Le thread continue pendant qu’une opération se termine Réactif, scalable Complexité accrue

🧠 Exemple :

// Synchrone
var data = client.GetString("https://api.com");

// Asynchrone
var data = await client.GetStringAsync("https://api.com");

🔹 9. Comment éviter les deadlocks dans une application multi-thread ?

Bonnes pratiques :

  1. Toujours acquérir les verrous dans le même ordre.

  2. Minimiser la durée du verrou (lock le moins longtemps possible).

  3. Éviter les appels bloquants dans une section critique.

  4. Utiliser await plutôt que .Result ou .Wait() sur une Task.

  5. Préférer ConfigureAwait(false) dans les bibliothèques pour éviter les captures de contexte UI.

Exemple de piège :

// ❌ Deadlock potentiel
var result = Task.Run(() => GetDataAsync()).Result;

🔹 10. Expliquez Semaphore et SemaphoreSlim avec des exemples.

✅ Semaphore :

  • Permet de limiter le nombre de threads simultanés accédant à une ressource.

  • Fonctionne entre processus différents.

Semaphore semaphore = new Semaphore(2, 2); // 2 threads max

for (int i = 0; i < 5; i++)
{
    new Thread(() =>
    {
        semaphore.WaitOne();
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} entre");
        Thread.Sleep(1000);
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} sort");
        semaphore.Release();
    }).Start();
}

✅ SemaphoreSlim :

  • Version plus légère, intra-processus, utilisée dans les scénarios async.

SemaphoreSlim semaphore = new SemaphoreSlim(2, 2);

var tasks = Enumerable.Range(0, 5).Select(async i =>
{
    await semaphore.WaitAsync();
    Console.WriteLine($"Task {i} running");
    await Task.Delay(1000);
    semaphore.Release();
});

await Task.WhenAll(tasks);




🔹 1. Qu'est-ce que le SynchronizationContext en C# et comment fonctionne-t-il ?

✅ Définition :

SynchronizationContext représente un contexte d’exécution logique qui définit où et comment le code asynchrone doit reprendre après un await.

  • Dans une application UI (WinForms, WPF), le SynchronizationContext permet de reprendre l’exécution sur le thread principal UI après un await.

  • Dans une application console ou ASP.NET Core, le contexte est souvent nul ou neutre, donc le code reprend sur n’importe quel thread du pool.

🧩 Exemple :

private async void Button_Click(object sender, EventArgs e)
{
    // Capturer le SynchronizationContext (thread UI)
    await Task.Delay(1000); 
    // Reprise sur le thread UI (grâce au SynchronizationContext)
    label1.Text = "Done!";
}

⚙️ Fonctionnement :

  1. Lors du await, le runtime capture le SynchronizationContext actuel.

  2. Quand la tâche reprend, elle poste le reste du code dans ce contexte.

  3. Cela permet d’éviter les accès concurrents à l’UI.

🧠 Résumé :

Le SynchronizationContext garantit que le code asynchrone reprend sur le bon thread (souvent le thread UI).


🔹 2. Différence entre ConcurrentBag, ConcurrentQueue et ConcurrentStack

Collection Type Ordre d’accès Utilisation typique
ConcurrentQueue File (FIFO) Premier entré, premier sorti Files d’attente de tâches
ConcurrentStack Pile (LIFO) Dernier entré, premier sorti Undo/Redo, historiques
ConcurrentBag Sac (non ordonné) Ordre indéterminé Partage d’objets entre threads

Toutes ces collections :

  • Font partie de System.Collections.Concurrent

  • Sont thread-safe

  • Utilisent des verrous fins ou lock-free structures

🧩 Exemple :

ConcurrentQueue<int> queue = new();
queue.Enqueue(1);
queue.Enqueue(2);
queue.TryDequeue(out int result); // 1 (FIFO)

🧠 Résumé :

Choisir la collection selon la politique d’accès (FIFO, LIFO, ou libre).


🔹 3. Qu’est-ce qu’un race condition et comment l’éviter en C# ?

✅ Définition :

Une race condition se produit lorsque plusieurs threads accèdent à une même ressource partagée, sans coordination, provoquant un comportement imprévisible.

🧩 Exemple problématique :

int counter = 0;

Parallel.For(0, 1000, i => counter++); // ❌ accès concurrent
Console.WriteLine(counter); // Résultat incorrect

✅ Solutions :

  1. Utiliser un lock :

    object locker = new object();
    Parallel.For(0, 1000, i => 
    {
        lock (locker)
            counter++;
    });
    
  2. Utiliser Interlocked :

    Parallel.For(0, 1000, i => Interlocked.Increment(ref counter));
    

🧠 Résumé :

Les race conditions sont évitées via des mécanismes de synchronisation (lock, Monitor, Interlocked, etc.).


🔹 4. Comment utiliser Task.WhenAll() et Task.WhenAny() ?

Task.WhenAll() :

Exécute plusieurs tâches en parallèle et attend qu’elles soient toutes terminées.

var t1 = Task.Delay(1000);
var t2 = Task.Delay(2000);
await Task.WhenAll(t1, t2); // Attend les deux
Console.WriteLine("All tasks done");

Task.WhenAny() :

Attend que la première tâche terminée renvoie un résultat.

var t1 = Task.Delay(1000);
var t2 = Task.Delay(3000);
await Task.WhenAny(t1, t2);
Console.WriteLine("First task done");

🧠 Résumé :

WhenAll → synchronisation globale,
WhenAny → synchronisation partielle (utile pour les timeouts ou priorités).


🔹 5. Différence entre ConfigureAwait(true) et ConfigureAwait(false)

Option Description Comportement
true (par défaut) Capture le SynchronizationContext courant Reprise sur le thread d’origine (ex : UI)
false Ignore le contexte capturé Reprise sur n’importe quel thread du pool

🧩 Exemple :

await Task.Delay(1000).ConfigureAwait(false);

👉 Ici, la suite du code s’exécute sur un autre thread, utile pour optimiser les performances côté serveur (ASP.NET Core).

🧠 Bonnes pratiques :

  • UI appsConfigureAwait(true)

  • Backend/serverConfigureAwait(false)


🔹 6. Comment utiliser BlockingCollection pour synchroniser des threads ?

✅ Définition :

BlockingCollection<T> est une collection thread-safe qui bloque les producteurs quand elle est pleine et bloque les consommateurs quand elle est vide.

✅ Cas d’usage :

Idéale pour les modèles producteur-consommateur.

🧩 Exemple :

BlockingCollection<int> collection = new(5);

// Producteur
Task.Run(() =>
{
    for (int i = 0; i < 10; i++)
    {
        collection.Add(i);
        Console.WriteLine($"Produced {i}");
    }
    collection.CompleteAdding();
});

// Consommateur
foreach (var item in collection.GetConsumingEnumerable())
{
    Console.WriteLine($"Consumed {item}");
}

🧠 Résumé :

BlockingCollection synchronise automatiquement la production et la consommation entre plusieurs threads sans code de verrou explicite.


🔹 7. Qu’est-ce qu’un SpinLock et quand faut-il l’utiliser ?

✅ Définition :

SpinLock est un verrou bas niveau qui occupe activement le CPU en boucle tant que le verrou n’est pas libéré.

✅ Fonctionnement :

Contrairement à lock, il ne met pas le thread en sommeil : il reste actif (busy-wait).

🧩 Exemple :

SpinLock spinLock = new SpinLock();

bool taken = false;
try
{
    spinLock.Enter(ref taken);
    // Section critique
}
finally
{
    if (taken) spinLock.Exit();
}

⚠️ Utilisation :

  • À éviter sauf pour sections critiques très courtes (< 1 µs).

  • Utilisé dans des systèmes temps réel ou très bas niveau.

🧠 Résumé :

SpinLock évite le coût de planification des threads, mais consomme du CPU.
À réserver pour les verrous très courts.


🔹 8. Qu’est-ce que le thread affinity et pourquoi est-il important dans les applications UI ?

✅ Définition :

Le thread affinity signifie qu’un objet ou une ressource doit être accédé uniquement par un thread spécifique.

Exemple : dans WPF ou WinForms, les éléments UI ne peuvent être modifiés que depuis le thread principal UI.

🧩 Exemple :

// ❌ Provoque une exception
Task.Run(() => label1.Text = "Test");

// ✅ Correction
Task.Run(() => this.Invoke(() => label1.Text = "Test"));

🧠 Pourquoi important :

Cela évite les cross-thread exceptions et garantit la cohérence de l’état graphique.


🔹 9. Différence entre volatile, lock et Interlocked

Mot-clé / Classe Type Rôle principal Garanties
volatile Modificateur de champ Empêche la mise en cache de la variable entre threads Visibilité ✅ / Atomicité ❌
lock Instruction C# Exclusivité d’accès (mutex interne) Atomicité ✅ / Visibilité ✅
Interlocked Classe statique Opérations atomiques sur types primitifs Atomicité ✅ / Visibilité ✅

🧩 Exemple :

volatile bool flag = false;
lock(obj) { counter++; }
Interlocked.Increment(ref counter);

🧠 Résumé :

  • volatile → visibilité mémoire

  • lock → exclusivité complète

  • Interlocked → opérations atomiques légères


🔹 10. Pourquoi await Task.Delay(1000) est-il préférable à Thread.Sleep(1000) dans une application asynchrone ?

Aspect Thread.Sleep() Task.Delay()
Type Bloquant Non bloquant
Thread Bloqué pendant la durée Libéré pour d’autres tâches
Utilisation Code synchrone Code asynchrone (async/await)
Performance Inefficace (bloque un thread du pool) Optimisée (utilise un timer interne)

🧩 Exemple :

await Task.Delay(1000); // Laisse le thread libre ✅
Thread.Sleep(1000);     // Bloque le thread ❌

🧠 Conclusion :

Task.Delay() est non bloquant → idéal dans les apps asynchrones (UI, serveur).
Thread.Sleep() bloque le thread → à éviter dans tout code async.


📘 RÉCAPITULATIF SYNTHÉTIQUE

Concept Description Niveau
SynchronizationContext Gère le contexte de reprise des tâches asynchrones Intermédiaire
Concurrent* Collections Structures thread-safe (Queue, Stack, Bag) Avancé
Race Condition Accès non coordonné à une ressource Fondamental
Task.WhenAll/WhenAny Coordination de plusieurs tâches asynchrones Moyen
ConfigureAwait(false) Ignore le contexte capturé Avancé
BlockingCollection Synchronisation producteur/consommateur Avancé
SpinLock Verrou actif performant (rare) Expert
Thread Affinity Accès UI sur thread principal uniquement Intermédiaire
volatile / lock / Interlocked Synchronisation mémoire et atomicité Fondamental
Task.Delay vs Thread.Sleep Différence entre blocage et attente asynchrone Fondamental


Aucun commentaire:

Enregistrer un commentaire

Q/A Multithreading Lượt xem: