Maison  >  Article  >  développement back-end  >  Auto-apprentissage C#12 à partir de 0 - Un résumé des solutions de synchronisation de threads et de leurs avantages et inconvénients

Auto-apprentissage C#12 à partir de 0 - Un résumé des solutions de synchronisation de threads et de leurs avantages et inconvénients

黄舟
黄舟original
2017-02-04 11:11:151459parcourir

Tout d'abord, une chose est sûre : la bibliothèque de classes Framework (FCL) de Microsoft garantit que toutes les méthodes statiques sont thread-safe.

FCL ne garantit pas que les méthodes d'instance sont thread-safe. Parce que si tous les verrous sont ajoutés, cela entraînera une énorme perte de performances. De plus, si chaque méthode d'instance doit acquérir et libérer un verrou, vous vous retrouverez en fait avec un seul thread exécuté dans votre application à un moment donné, ce qui a un impact évident sur les performances.

Ce qui suit présente la construction primitive de synchronisation des threads.

Primitives : font référence aux constructions les plus simples pouvant être utilisées dans le code. Il existe deux constructions primitives : le mode utilisateur et le mode noyau.

Le mode utilisateur

utilise des instructions spéciales du processeur pour coordonner les threads.

Technologie : mot-clé volatile, classe Interlocked (interlock), spinlock (spin lock)

Verrouillage commun ① : mot-clé volatile indique qu'un champ peut être modifié par plusieurs threads s'exécutant simultanément. Les champs déclarés volatils ne sont pas soumis aux optimisations du compilateur (en supposant un accès par un seul thread). Cela garantit que le champ présente la dernière valeur à tout moment.

Classe interverrouillée : fournit des opérations atomiques pour les variables partagées par plusieurs threads. . L'opération dite atomique fait référence à une opération qui ne sera pas interrompue par le mécanisme de planification des threads ; une fois cette opération lancée, elle s'exécutera jusqu'à la fin sans aucun changement de contexte (passage à un autre thread) au milieu.

Verrous communs ② : La structure SpinLock est une primitive de synchronisation mutex de bas niveau qui tourne en attendant d'acquérir un verrou. Sur les ordinateurs multicœurs, SpinLock fonctionnera mieux que les autres types de verrous lorsque les temps d'attente sont censés être courts et que les conditions de conflit sont rares. Même si SpinLock n'acquiert pas le verrou, il génère la tranche de temps du thread. Cela permet d'éviter l'inversion de la priorité des threads et de permettre au garbage collector de continuer à s'exécuter. Lorsque vous utilisez SpinLock, assurez-vous qu'aucun thread ne maintient le verrou pendant plus d'une très courte période de temps et qu'aucun thread ne se bloque pendant le maintien du verrou.

Avantages :

Les constructions primitives en mode utilisateur doivent être utilisées autant que possible, car elles sont nettement plus rapides que les constructions en mode noyau.

La coordination des threads se fait au niveau matériel (c'est pourquoi elle est si rapide).

Mais le système d'exploitation Microsoft Windows ne détecte jamais qu'un thread est bloqué sur une construction primitive en mode utilisateur.

Puisqu'un pool de threads bloquant sur une construction primitive en mode utilisateur n'est jamais considéré comme bloqué, le pool de threads ne créera pas de nouveau thread pour remplacer un tel thread temporaire.

Ces instructions CPU ne bloquent le thread que pendant une période de temps relativement courte.

Inconvénients :

Seul le noyau du système d'exploitation Windows peut arrêter l'exécution d'un thread (l'empêchant de perdre du temps CPU).

Les threads exécutés en mode utilisateur peuvent être préemptés par le système, mais les threads seront à nouveau programmés le plus rapidement possible.

Les threads qui souhaitent obtenir des ressources mais ne peuvent pas les obtenir temporairement continueront à « tourner » en mode utilisateur, ce qui peut faire perdre beaucoup de temps CPU. Les threads s'exécutent toujours sur un seul processeur, que nous appelons « livelock ».

Instance :

using System;using System.Threading;public class Worker
{    // This method is called when the thread is started.
    public void DoWork()
    {        while (!_shouldStop)
        {
            Console.WriteLine("Worker thread: working...");
        }
        Console.WriteLine("Worker thread: terminating gracefully.");
    }    public void RequestStop()
    {
        _shouldStop = true;
    }    // Keyword volatile is used as a hint to the compiler that this data
    // member is accessed by multiple threads.
    private volatile bool _shouldStop;
}public class WorkerThreadExample
{    static void Main()
    {        // Create the worker thread object. This does not start the thread.
        Worker workerObject = new Worker();
        Thread workerThread = new Thread(workerObject.DoWork);        // Start the worker thread.
        workerThread.Start();
        Console.WriteLine("Main thread: starting worker thread...");        // Loop until the worker thread activates.
        while (!workerThread.IsAlive) ;        // Put the main thread to sleep for 1 millisecond to
        // allow the worker thread to do some work.
        Thread.Sleep(1);        // Request that the worker thread stop itself.
        workerObject.RequestStop();        // Use the Thread.Join method to block the current thread 
        // until the object's thread terminates.
        workerThread.Join();
        Console.WriteLine("Main thread: worker thread has terminated.");
    }    // Sample output:
    // Main thread: starting worker thread...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: terminating gracefully.
    // Main thread: worker thread has terminated.}

Mode noyau

Fourni par le système d'exploitation Windows lui-même. Ils nécessitent l'appel de fonctions implémentées par le noyau du système d'exploitation dans le thread de l'application.

Technologie : EventWaitHandle (événement), Semaphore (sémaphore), Mutex (mutex)

System.Object  
System.MarshalByRefObject    
System.Threading.WaitHandle      
System.Threading.EventWaitHandle      
System.Threading.Mutex      
System.Threading.Semaphore

Verrous communs ③ : La classe Mutex est un wrapper de la construction Win32, qui peut être utilisé entre les applications Le marshaling aux limites du domaine du programme peut être utilisé pour plusieurs attentes et peut être utilisé pour synchroniser les threads dans différents processus.

Avantages :

Lorsqu'un thread acquiert des ressources appartenant à d'autres threads via des constructions en mode noyau, Windows bloque le thread pour l'empêcher de perdre du temps CPU. Lorsque la ressource devient disponible, Windows reprend le thread, lui permettant d'accéder à la ressource. Il n'occupe pas de "spin" CPU.

Permet de synchroniser les threads natifs et gérés les uns avec les autres.

Les threads exécutés dans différents processus sur la même machine peuvent être synchronisés.

Des paramètres de sécurité peuvent être appliqués pour empêcher les comptes non autorisés d'y accéder.

Un thread peut bloquer jusqu'à ce que toutes les constructions en mode noyau avec lesquelles il coopère soient disponibles, ou jusqu'à ce qu'une construction en mode noyau de l'ensemble soit disponible.

Les threads bloqués sur les constructions en mode noyau peuvent spécifier une valeur de délai d'attente : si la ressource souhaitée n'est pas accessible dans le délai spécifié, le thread peut être débloqué et effectuer d'autres tâches.

Inconvénients :

Le passage d'un thread du mode utilisateur au mode noyau (ou vice versa) entraîne une énorme pénalité de performances, c'est exactement pourquoi les constructions du noyau doivent être évitées. De plus, si le thread est bloqué tout le temps, cela entraînera un « deadlock » (deadlock).

Le blocage est toujours dû au livelock, car le livelock gaspille du temps CPU et de la mémoire (pile de threads, etc.), tandis que le blocage ne fait que gaspiller de la mémoire.

Construction hybride

兼具上面两者的长处。在没有竞争的情况下,快而且不会阻塞(就像用户模式)。在有竞争的情况,希望它被操作系统内核阻塞。

技术:ManualResetEventSlim类、SemaphoreSlim类、Monitor类、Lock类、ReaderWriterLockSlim类、CountdownEvent类、Barrier类、双检锁.

常见锁④:Monitor 通常更为可取,因为监视器是专门为 .NET Framework 而设计的,因而它比Mutex可以更好地利用资源。尽管 mutex 比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。

常见锁⑤:使用 lock (C#) 或 SyncLock (Visual Basic) 关键字是Monitor的封装。通常比直接使用 Monitor 类更可取,一方面是因为 lock 或 SyncLock 更简洁,另一方面是因为lock 或 SyncLock 确保了即使受保护的代码引发异常,也可以释放基础监视器。

常见锁⑥:ReaderWriterLock 锁,在某些情况下,可能希望只在写入数据时锁定资源,在不更新数据时允许多个客户端同时读取数据。ReaderWriterLock 类在线程修改资源时将强制其独占访问资源,但在读取资源时则允许非独占访问。 ReaderWriter 锁可用于代替排它锁。使用排它锁时,即使其他线程不需要更新数据,也会让这些线程等待。

双检锁

常见锁⑦:双重检查锁定模式(也被称为”双重检查加锁优化”,”锁暗示”(Lock hint)) 是一种软件设计模式用来减少并发系统中竞争和同步的开销。

双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。

它通常用于减少加锁开销,尤其是为多线程环境中的单例模式实现“惰性初始化”。惰性初始化的意思是直到第一次访问时才初始化它的值。

public sealed class Singleton
    {        private static volatile Singleton instance = null;        
private static object syncRoot = new Object();        
private static int count = 100;        
private Singleton() { }        
public static Singleton Instance
        {            get
            {                if (instance == null)
                {                    lock (syncRoot)
                    {                        if (instance == null)
                            instance = new Singleton();
                    }
                }                return instance;
            }
        }        public static Singleton GetSingleton()
        {            if (instance == null)
            {                lock (syncRoot)
                {                    if (instance == null)
                    {
                        instance = new Singleton();
                    }                        
                }
            }            return instance;
        }        public override string ToString()
        {            lock (syncRoot)
            {                int buf = --count;
                Thread.Sleep(20);                return buf + "_" + count.ToString();
            }
        }
    }static void Main(string[] args)
        {
            Task<string>[] tasks = new Task<string>[10];
            Stopwatch watch = new Stopwatch();            object syncRoot = new Object();
            watch.Start();            for (int i = 0; i < tasks.Length; i++)
            {
                tasks[i] = Task.Factory.StartNew<string>(() =>(Singleton.GetSingleton().ToString()));                    
            }
            Task.WaitAll(tasks, 5000);//设置超时5s
            watch.Stop();
            Console.WriteLine("Tasks running need " + watch.ElapsedMilliseconds + " ms." + "\n");            
for (int i = 0; i < tasks.Length; i++)
            {                //超时处理
                if (tasks[i].Status != TaskStatus.RanToCompletion)
                {
                    Console.WriteLine("Task {0} Error!", i + 1);
                }                else
                {                    //save result
                    Console.WriteLine("Tick ID is " + tasks[i].Result);
                    Console.WriteLine();
                }
            }            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("Main thread do work!");
                Thread.Sleep(200);
            }

            Console.ReadKey();
        }

输出:

Tasks running need 298 ms.

Tick ID is 96_96

Tick ID is 99_99

Tick ID is 97_97

Tick ID is 98_98

Tick ID is 95_95

Tick ID is 94_94

Tick ID is 93_93

Tick ID is 92_92

Tick ID is 91_91

Tick ID is 90_90

Main thread do work!
Main thread do work!
Main thread do work!

以上就是从0自学C#12--线程同步解决方法汇总以及优缺点的内容,更多相关内容请关注PHP中文网(www.php.cn)!


Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn