太阳集团游戏官方网站 10

线程同步【太阳集团游戏官方网站】,进阶系列

目录

七个线程同期访谈共享数据时,线程同步能幸免数据损坏。之所以要重申还要,是因为线程同步难点其实正是计时难点。

  • 1.1
    简介
  • 1.2
    实行基本原子操作
  • 1.3
    使用Mutex类
  • 1.4
    使用SemaphoreSlim类
  • 1.5
    使用AutoResetEvent类
  • 1.6
    使用ManualResetEventSlim类
  • 1.7
    使用CountDownEvent类
  • 1.8
    使用Barrier类
  • 1.9
    使用ReaderWriterLockSlim类
  • 1.10
    使用SpinWait类
  • 参谋书籍
  • 小编水平有限,假如不当接待各位争论指正!

无需线程同步是最杰出的图景,因为线程同步日常很麻烦,涉及到线程同步锁的获取和释放,轻巧遗漏,并且锁会消耗质量,获取和假释锁都须要时日,最终锁的游戏的方法就在于贰次只可以让二个线程访谈数据,那么就能够堵塞线程,梗塞线程就能让额外的线程发生,梗塞更加多,线程愈来愈多,线程过多的害处就不谈了。


故此能够制止线程同步的话就应当去幸免,尽量不要去行使静态字段那样的分享数据。


类库和线程安全

1.1 简介

本章介绍在C#中完成线程同步的两种办法。因为多少个线程同有的时候间访谈分享数据时,恐怕会促成分享数据的毁坏,从而引致与预期的结果不契合。为了解决那些标题,所以需求用到线程同步,也被俗称为“加锁”。然则加锁相对不对进步质量,最多也便是不增不减,要促成品质不增不减还得靠高水平的同步源语(Synchronization
Primitive)。不过因为科学永久比速度更要紧,所以线程同步在有个别场景下是必须的。

线程同步有三种源语(Primitive)构造:顾客方式(user –
mode)
根基形式(kernel –
mode)
,当能源可用时间短的图景下,顾客格局要优于基本形式,但是风度翩翩旦长日子不可能获得能源,或许说长日子处于“自旋”,那么内核形式是相对来讲好的挑肥拣瘦。

只是大家盼望保有客户格局和水源方式的优点,大家把它称作掺杂构造(hybrid
construct)
,它装有了三种方式的长处。

在C#中有两种线程同步的体制,平时能够固守以下顺序实行抉择。

  1. 后生可畏经代码能经过优化能够不开展协同,那么就不要做风度翩翩道。
  2. 应用原子性的Interlocked方法。
  3. 使用lock/Monitor类。
  4. 利用异步锁,如SemaphoreSlim.WaitAsync()
  5. 运用别的加锁机制,如ReaderWriterLockSlim、Mutex、Semaphore等。
  6. 假使系统提供了*Slim本子的异步对象,那么请选择它,因为*Slim本子全是混合锁,在进入根基方式前落到实处了某种格局的自旋。

在一齐中,应当要专一防止死锁的爆发,死锁的爆发必得满意以下4个主导准绳,所以只须求破坏放肆二个典型化,就可防止产生死锁。

  1. 排他或互斥(Mutual
    exclusion):一个线程(ThreadA)独自据有三个能源,未有其余线程(ThreadB)能得到相像的资源。
  2. 攻克并听候(Hold and
    wait):互斥的一个线程(ThreadA)央求获取另叁个线程(ThreadB)占领的财富.
  3. 不安妥先(No
    preemption):贰个线程(ThreadA)据有财富不可能被威迫拿走(只好等待ThreadA主动释放它的能源)。
  4. 巡回等待条件(Circular wait
    condition):七个或多个线程构成一个循环等待链,它们锁定八个或四个相通的能源,每个线程都在等待链中的下贰个线程据有的能源。

.net类库保证了有着静态方法都是线程安全的,也正是说多少个线程同期调用二个静态方法,不会生出多少被弄坏的场地。

1.2 试行基本原子操作

CL普拉多保障了对那个数据类型的读写是原子性的:Boolean、Char、(S)Byte、(U)Int16、(U)Int32、(U)IntPtr和Single。可是要是读写Int64可能会爆发读取撕裂(torn
read)的难点,因为在三十一位操作系统中,它必要试行四回Mov操作,无法在三个年华内实行到位。

那么在本节中,就能够首要的牵线System.Threading.Interlocked太阳集团游戏官方网站,类提供的章程,Interlocked类中的每一个方法皆以进行叁遍的读取以至写入操作。越来越多与Interlocked类相关的资料请参见链接,戳一戳.aspx)本文不在赘述。

演示代码如下所示,分别使用了三种情势开展计数:错误计数方式、lock锁方式和Interlocked原子情势。

private static void Main(string[] args)
{
    Console.WriteLine("错误的计数");

    var c = new Counter();
    Execute(c);

    Console.WriteLine("--------------------------");


    Console.WriteLine("正确的计数 - 有锁");

    var c2 = new CounterWithLock();
    Execute(c2);

    Console.WriteLine("--------------------------");


    Console.WriteLine("正确的计数 - 无锁");

    var c3 = new CounterNoLock();
    Execute(c3);

    Console.ReadLine();
}

static void Execute(CounterBase c)
{
    // 统计耗时
    var sw = new Stopwatch();
    sw.Start();

    var t1 = new Thread(() => TestCounter(c));
    var t2 = new Thread(() => TestCounter(c));
    var t3 = new Thread(() => TestCounter(c));
    t1.Start();
    t2.Start();
    t3.Start();
    t1.Join();
    t2.Join();
    t3.Join();

    sw.Stop();
    Console.WriteLine($"Total count: {c.Count} Time:{sw.ElapsedMilliseconds} ms");
}

static void TestCounter(CounterBase c)
{
    for (int i = 0; i < 100000; i++)
    {
        c.Increment();
        c.Decrement();
    }
}

class Counter : CounterBase
{
    public override void Increment()
    {
        _count++;
    }

    public override void Decrement()
    {
        _count--;
    }
}

class CounterNoLock : CounterBase
{
    public override void Increment()
    {
        // 使用Interlocked执行原子操作
        Interlocked.Increment(ref _count);
    }

    public override void Decrement()
    {
        Interlocked.Decrement(ref _count);
    }
}

class CounterWithLock : CounterBase
{
    private readonly object _syncRoot = new Object();

    public override void Increment()
    {
        // 使用Lock关键字 锁定私有变量
        lock (_syncRoot)
        {
            // 同步块
            Count++;
        }
    }

    public override void Decrement()
    {
        lock (_syncRoot)
        {
            Count--;
        }
    }
}


abstract class CounterBase
{
    protected int _count;

    public int Count
    {
        get
        {
            return _count;
        }
        set
        {
            _count = value;
        }
    }

    public abstract void Increment();

    public abstract void Decrement();
}

运维结果如下所示,与预期结果基本相符。

太阳集团游戏官方网站 1

并不能够有限援救具备实例方法线程安全。因为相符情状下实例成立后唯有创设的线程能访谈到,除非后来将实例的援引传给了一个静态变量,可能将援引传给了线程池的队列可能职责,那么那时或许就要思虑用线程同步了。

1.3 使用Mutex类

System.Threading.Mutex在概念上和System.Threading.Monitor大概同风流洒脱,不过Mutex联机对文本也许其余跨进程的财富扩充拜访,也等于说Mutex是可跨进度的。因为其特色,它的一个用项是节制应用程序不能够并且运营七个实例。

Mutex指标帮助递归,相当于说同叁个线程可一再次获取取同二个锁,那在后头演示代码中可观望到。由于Mutex的基类System.Theading.WaitHandle实现了IDisposable接口,所以当没有必要在利用它时要小心进行财富的自由。越来越多材质:戳一戳

演示代码如下所示,简单的演示了怎样创设单实例的应用程序和Mutex递归获取锁的贯彻。

const string MutexName = "CSharpThreadingCookbook";

static void Main(string[] args)
{
    // 使用using 及时释放资源
    using (var m = new Mutex(false, MutexName))
    {
        if (!m.WaitOne(TimeSpan.FromSeconds(5), false))
        {
            Console.WriteLine("已经有实例正在运行!");
        }
        else
        {

            Console.WriteLine("运行中...");

            // 演示递归获取锁
            Recursion();

            Console.ReadLine();
            m.ReleaseMutex();
        }
    }

    Console.ReadLine();
}

static void Recursion()
{
    using (var m = new Mutex(false, MutexName))
    {
        if (!m.WaitOne(TimeSpan.FromSeconds(2), false))
        {
            // 因为Mutex支持递归获取锁 所以永远不会执行到这里
            Console.WriteLine("递归获取锁失败!");
        }
        else
        {
            Console.WriteLine("递归获取锁成功!");
        }
    }
}

运作结果如下图所示,张开了七个应用程序,因为使用Mutex完结了单实例,所以第2个应用程序不能得到锁,就能够显得本来就有实例正在周转

太阳集团游戏官方网站 2

Console类满含二个静态字段,类的广大办法都要收获和刑释这么些指标上的锁,确定保证唯有多个线程访谈调控台。

1.4 使用SemaphoreSlim类

SemaphoreSlim类与事先提到的同步类有锁差异,早先涉嫌的同步类都以排挤的,相当于说只允许多少个线程实行拜见资源,而SemaphoreSlim是能够允许五个访谈。

在事先的片段有涉及,以*Slim提及底的线程同步类,都以做事在混合情势下的,相当于说在此之前它们都是在客商格局下”自旋”,等发出第1回角逐时,才切换成根本形式。但是SemaphoreSlim不同于Semaphore类,它不支持系统时域信号量,所以它无法用来进度之间的风流浪漫道

此类应用比较简单,演示代码演示了6个线程角逐访谈只同意4个线程同一时候做客的数据库,如下所示。

static void Main(string[] args)
{
    // 创建6个线程 竞争访问AccessDatabase
    for (int i = 1; i <= 6; i++)
    {
        string threadName = "线程 " + i;
        // 越后面的线程,访问时间越久 方便查看效果
        int secondsToWait = 2 + 2 * i;
        var t = new Thread(() => AccessDatabase(threadName, secondsToWait));
        t.Start();
    }

    Console.ReadLine();
}

// 同时允许4个线程访问
static SemaphoreSlim _semaphore = new SemaphoreSlim(4);

static void AccessDatabase(string name, int seconds)
{
    Console.WriteLine($"{name} 等待访问数据库.... {DateTime.Now.ToString("HH:mm:ss.ffff")}");

    // 等待获取锁 进入临界区
    _semaphore.Wait();

    Console.WriteLine($"{name} 已获取对数据库的访问权限 {DateTime.Now.ToString("HH:mm:ss.ffff")}");
    // Do something
    Thread.Sleep(TimeSpan.FromSeconds(seconds));

    Console.WriteLine($"{name} 访问完成... {DateTime.Now.ToString("HH:mm:ss.ffff")}");
    // 释放锁
    _semaphore.Release();
}

运行结果如下所示,可以见到前4个线程即刻就赢获得了锁,进入了临界区,而除此以外多个线程在等待;等有锁被保释时,工夫走入临界区。太阳集团游戏官方网站 3

基元顾客格局和基本形式协会(这一片段看不清楚能够先看看前面包车型大巴顾客格局和水源格局的授课,就能够分晓了卡塔 尔(英语:State of Qatar)

1.5 使用AutoResetEvent类

AutoResetEvent叫自动重新苏醒设置事件,固然名称中有事件后生可畏词,可是重新初始化事件和C#中的委托未有其余关联,这里的风云只是由基本功维护的Boolean变量,当事件为false,那么在事件上伺机的线程就短路;事件形成true,那么窒碍清除。

在.Net中有二种此类事件,即AutoResetEvent(自动重置事件)ManualResetEvent(手动重置事件)。那二者均是接受根本格局,它的分别在于当重新设置事件为true时,电动重新载入参数事件它只唤醒一个封堵的线程,会活动将事件重新初始化回false,变成别的线程继续梗塞。而手动重新设置事件不会自动重新苏醒设置,必需通过代码手动重新设置回false

因为以上的缘故,所以在广大篇章和本本中不推荐使用AutoResetEvent(自动重置事件),因为它相当的轻巧在编辑生产者线程时发生失误,产生它的迭代次数多余费用者线程。

示范代码如下所示,该代码演示了通过AutoResetEvent落到实处五个线程的并行同步。

static void Main(string[] args)
{
    var t = new Thread(() => Process(10));
    t.Start();

    Console.WriteLine("等待另一个线程完成工作!");
    // 等待工作线程通知 主线程阻塞
    _workerEvent.WaitOne();
    Console.WriteLine("第一个操作已经完成!");
    Console.WriteLine("在主线程上执行操作");
    Thread.Sleep(TimeSpan.FromSeconds(5));

    // 发送通知 工作线程继续运行
    _mainEvent.Set();
    Console.WriteLine("现在在第二个线程上运行第二个操作");

    // 等待工作线程通知 主线程阻塞
    _workerEvent.WaitOne();
    Console.WriteLine("第二次操作完成!");

    Console.ReadLine();
}

// 工作线程Event
private static AutoResetEvent _workerEvent = new AutoResetEvent(false);
// 主线程Event
private static AutoResetEvent _mainEvent = new AutoResetEvent(false);

static void Process(int seconds)
{
    Console.WriteLine("开始长时间的工作...");
    Thread.Sleep(TimeSpan.FromSeconds(seconds));
    Console.WriteLine("工作完成!");

    // 发送通知 主线程继续运行
    _workerEvent.Set();
    Console.WriteLine("等待主线程完成其它工作");

    // 等待主线程通知 工作线程阻塞
    _mainEvent.WaitOne();
    Console.WriteLine("启动第二次操作...");
    Thread.Sleep(TimeSpan.FromSeconds(seconds));
    Console.WriteLine("工作完成!");

    // 发送通知 主线程继续运行
    _workerEvent.Set();
}

运维结果如下图所示,与预期结果适合。

太阳集团游戏官方网站 4

基元是指能够在代码中央银行使的最简便的构造。

1.6 使用ManualResetEventSlim类

ManualResetEventSlim使用和ManualResetEvent类基本生机勃勃致,只是ManualResetEventSlim工作在掺杂格局下,而它与AutoResetEventSlim不等之处便是必要手动重新苏醒设置事件,也正是调用Reset()技能将事件重新初始化为false

示范代码如下,形象的将ManualResetEventSlim比喻成大门,当事件为true时大门张开,线程消弭窒碍;而事件为false时大门关闭,线程拥塞。

static void Main(string[] args)
        {
            var t1 = new Thread(() => TravelThroughGates("Thread 1", 5));
            var t2 = new Thread(() => TravelThroughGates("Thread 2", 6));
            var t3 = new Thread(() => TravelThroughGates("Thread 3", 12));
            t1.Start();
            t2.Start();
            t3.Start();

            // 休眠6秒钟  只有Thread 1小于 6秒钟,所以事件重置时 Thread 1 肯定能进入大门  而 Thread 2 可能可以进入大门
            Thread.Sleep(TimeSpan.FromSeconds(6));
            Console.WriteLine($"大门现在打开了!  时间:{DateTime.Now.ToString("mm:ss.ffff")}");
            _mainEvent.Set();

            // 休眠2秒钟 此时 Thread 2 肯定可以进入大门
            Thread.Sleep(TimeSpan.FromSeconds(2));
            _mainEvent.Reset();
            Console.WriteLine($"大门现在关闭了! 时间:{DateTime.Now.ToString("mm: ss.ffff")}");

            // 休眠10秒钟 Thread 3 可以进入大门
            Thread.Sleep(TimeSpan.FromSeconds(10));
            Console.WriteLine($"大门现在第二次打开! 时间:{DateTime.Now.ToString("mm: ss.ffff")}");
            _mainEvent.Set();
            Thread.Sleep(TimeSpan.FromSeconds(2));

            Console.WriteLine($"大门现在关闭了! 时间:{DateTime.Now.ToString("mm: ss.ffff")}");
            _mainEvent.Reset();

            Console.ReadLine();
        }

        static void TravelThroughGates(string threadName, int seconds)
        {
            Console.WriteLine($"{threadName} 进入睡眠 时间:{DateTime.Now.ToString("mm:ss.ffff")}");
            Thread.Sleep(TimeSpan.FromSeconds(seconds));

            Console.WriteLine($"{threadName} 等待大门打开! 时间:{DateTime.Now.ToString("mm:ss.ffff")}");
            _mainEvent.Wait();

            Console.WriteLine($"{threadName} 进入大门! 时间:{DateTime.Now.ToString("mm:ss.ffff")}");
        }

        static ManualResetEventSlim _mainEvent = new ManualResetEventSlim(false);

运营结果如下,与预期结果契合。

太阳集团游戏官方网站 5

有三种基元构造:客商格局和水源情势。应尽量接纳基元客户方式结构,它们的快慢明显大于内核格局的构造。

1.7 使用CountDownEvent类

CountDownEvent类内部协会采纳了多个ManualResetEventSlim对象。这些协会拥塞一个线程,直到它里面流速计(CurrentCount)变为0时,才消逝堵塞。也正是说它并非掣肘对曾经枯竭的财富池的拜候,而是唯有当计数为0时才允许访问。

此地供给在意的是,当CurrentCount变为0时,那么它就不能够被退换了。为0以后,Wait()办法的堵截被消灭。

身体力行代码如下所示,只有当Signal()方法被调用2次随后,Wait()主意的隔绝才被消亡。

static void Main(string[] args)
{
    Console.WriteLine($"开始两个操作  {DateTime.Now.ToString("mm:ss.ffff")}");
    var t1 = new Thread(() => PerformOperation("操作 1 完成!", 4));
    var t2 = new Thread(() => PerformOperation("操作 2 完成!", 8));
    t1.Start();
    t2.Start();

    // 等待操作完成
    _countdown.Wait();
    Console.WriteLine($"所有操作都完成  {DateTime.Now.ToString("mm: ss.ffff")}");
    _countdown.Dispose();

    Console.ReadLine();
}

// 构造函数的参数为2 表示只有调用了两次 Signal方法 CurrentCount 为 0时  Wait的阻塞才解除
static CountdownEvent _countdown = new CountdownEvent(2);

static void PerformOperation(string message, int seconds)
{
    Thread.Sleep(TimeSpan.FromSeconds(seconds));
    Console.WriteLine($"{message}  {DateTime.Now.ToString("mm:ss.ffff")}");

    // CurrentCount 递减 1
    _countdown.Signal();
}

运维结果如下图所示,可以预知唯有当操作1和操作2都成功之后,才实践输出全体操作都形成。

太阳集团游戏官方网站 6

那是因为它们利用特殊的CPU指令来和煦线程,意味着和睦是在硬件上发出的,也表示操作系统恒久检查实验不到四个线程在基元客户形式的构造上围堵了。

1.8 使用Barrier类

Barrier类用于缓慢解决贰个极度稀有的标题,常常貌似用不上。Barrier类调节生龙活虎多种线程实行阶段性的竞相职业。

万一今后相互专门的学问分为2个阶段,每种线程在成功它和煦那部分等第1的专门的学业后,必需停下来等待其余线程落成阶段1的行事;等有着线程均变成阶段1专门的学问后,每一种线程又开头运维,达成阶段2做事,等待别的线程全体产生阶段2行事后,整个工艺流程才停止。

示范代码如下所示,该代码演示了八个线程分品级的成就工作。

static void Main(string[] args)
{
    var t1 = new Thread(() => PlayMusic("钢琴家", "演奏一首令人惊叹的独奏曲", 5));
    var t2 = new Thread(() => PlayMusic("歌手", "唱着他的歌", 2));

    t1.Start();
    t2.Start();

    Console.ReadLine();
}

static Barrier _barrier = new Barrier(2,
 Console.WriteLine($"第 {b.CurrentPhaseNumber + 1} 阶段结束"));

static void PlayMusic(string name, string message, int seconds)
{
    for (int i = 1; i < 3; i++)
    {
        Console.WriteLine("----------------------------------------------");
        Thread.Sleep(TimeSpan.FromSeconds(seconds));
        Console.WriteLine($"{name} 开始 {message}");
        Thread.Sleep(TimeSpan.FromSeconds(seconds));
        Console.WriteLine($"{name} 结束 {message}");
        _barrier.SignalAndWait();
    }
}

运作结果如下所示,当“艺人”线程实现后,并不曾立即甘休,而是等待“钢琴家”线程结束,当”钢琴家”线程甘休后,才开端第2等第的职业。

太阳集团游戏官方网站 7

除非操作系统内核本领停止三个线程的运作。

1.9 使用ReaderWriterLockSlim类

ReaderWriterLockSlim类首假诺解决在少数场景下,读操作多于写操作而利用一些互斥锁当多少个线程同一时候做客能源时,唯有几个线程能采访,引致品质小幅度下落。

若果拥有线程都希望以只读的章程访问数据,就根本未曾需求梗塞它们;假若二个线程希望改过数据,那么这几个线程才供给独自占领访问,那正是ReaderWriterLockSlim的独领风骚应用途景。那几个类就好像上边那样来决定线程。

  • 一个线程向数据写入是,恳求访谈的任何兼具线程都被卡住。
  • 叁个线程读取数据时,央浼读取的线程允许读取,而诉求写入的线程被卡住。
  • 写入线程停止后,要么消释多个写入线程的隔膜,使写入线程能向数据联网,要么撤消所有读取线程的窒碍,使它们能并发读取多少。假如线程未有被打断,锁就足以进来自由使用的事态,可供下八个读线程或写线程获取。
  • 从数额读取的持无线程截至后,四个写线程被消除阻塞,使它能向数据写入。要是线程未有被打断,锁就能够进来自由使用的景色,可供下二个读线程或写线程获取。

ReaderWriterLockSlim还帮衬从读线程晋级为写线程的操作,详情请戳一戳.aspx)。文本不作介绍。ReaderWriterLock类已经过时,何况存在许多主题材料,无需去接纳。

身体力行代码如下所示,创造了3个读线程,2个写线程,读线程和写线程角逐得到锁。

static void Main(string[] args)
{
    // 创建3个 读线程
    new Thread(() => Read("Reader 1")) { IsBackground = true }.Start();
    new Thread(() => Read("Reader 2")) { IsBackground = true }.Start();
    new Thread(() => Read("Reader 3")) { IsBackground = true }.Start();

    // 创建两个写线程
    new Thread(() => Write("Writer 1")) { IsBackground = true }.Start();
    new Thread(() => Write("Writer 2")) { IsBackground = true }.Start();

    // 使程序运行30S
    Thread.Sleep(TimeSpan.FromSeconds(30));

    Console.ReadLine();
}

static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
static Dictionary<int, int> _items = new Dictionary<int, int>();

static void Read(string threadName)
{
    while (true)
    {
        try
        {
            // 获取读锁定
            _rw.EnterReadLock();
            Console.WriteLine($"{threadName} 从字典中读取内容  {DateTime.Now.ToString("mm:ss.ffff")}");
            foreach (var key in _items.Keys)
            {
                Thread.Sleep(TimeSpan.FromSeconds(0.1));
            }
        }
        finally
        {
            // 释放读锁定
            _rw.ExitReadLock();
        }
    }
}

static void Write(string threadName)
{
    while (true)
    {
        try
        {
            int newKey = new Random().Next(250);
            // 尝试进入可升级锁模式状态
            _rw.EnterUpgradeableReadLock();
            if (!_items.ContainsKey(newKey))
            {
                try
                {
                    // 获取写锁定
                    _rw.EnterWriteLock();
                    _items[newKey] = 1;
                    Console.WriteLine($"{threadName} 将新的键 {newKey} 添加进入字典中  {DateTime.Now.ToString("mm:ss.ffff")}");
                }
                finally
                {
                    // 释放写锁定
                    _rw.ExitWriteLock();
                }
            }
            Thread.Sleep(TimeSpan.FromSeconds(0.1));
        }
        finally
        {
            // 减少可升级模式递归计数,并在计数为0时  推出可升级模式
            _rw.ExitUpgradeableReadLock();
        }
    }
}

运作结果如下所示,与预期结果切合。

太阳集团游戏官方网站 8

于是在顾客情势下运作的线程也许被系统抢占。

1.10 使用SpinWait类

SpinWait是叁个常用的插花方式的类,它被设计成选取客商形式等待风流罗曼蒂克段时间,人后切换至基本格局以节约CPU时间。

它的行使极度轻巧,演示代码如下所示。

static void Main(string[] args)
{
    var t1 = new Thread(UserModeWait);
    var t2 = new Thread(HybridSpinWait);

    Console.WriteLine("运行在用户模式下");
    t1.Start();
    Thread.Sleep(20);
    _isCompleted = true;
    Thread.Sleep(TimeSpan.FromSeconds(1));
    _isCompleted = false;

    Console.WriteLine("运行在混合模式下");
    t2.Start();
    Thread.Sleep(5);
    _isCompleted = true;

    Console.ReadLine();
}

static volatile bool _isCompleted = false;

static void UserModeWait()
{
    while (!_isCompleted)
    {
        Console.Write(".");
    }
    Console.WriteLine();
    Console.WriteLine("等待结束");
}

static void HybridSpinWait()
{
    var w = new SpinWait();
    while (!_isCompleted)
    {
        w.SpinOnce();
        Console.WriteLine(w.NextSpinWillYield);
    }
    Console.WriteLine("等待结束");
}

运维结果如下两图所示,首先程序运转在模仿的顾客情势下,使CPU有多少个短间隔赛跑的峰值。然后选用SpinWait行事在混合方式下,首先标记变量为False高居客户方式自旋中,等待今后步入底子方式。

太阳集团游戏官方网站 9

太阳集团游戏官方网站 10

所以也得以用基本形式结构,因为线程通过根基形式的布局获取其余线程具有的资源时,Windows会梗塞线程以制止它浪费CPU时间。当能源变得可用时,Windows会苏醒线程,允许它访问财富。

参照书籍

正文主要参谋了以下几本书,在这里对这个作者表示真心的谢谢您们提供了这么好的材料。

  1. 《CLR via C#》
  2. 《C# in Depth Third Edition》
  3. 《Essential C# 6.0》
  4. 《Multithreading with C# Cookbook Second Edition》

源码下载点击链接
亲自去做源码下载

只是线程从客商情势切换来基本功情势(或相反卡塔尔国会诱致宏大的性质损失。

作者水平有限,要是不当招待各位商议指正!

对于在八个协会上伺机的线程,如若占领构造的这一个线程不自由它,前边多个就大概直接不通。构造是顾客方式的构造情状下,线程会平昔在多个CPU上运转,称为“活锁”。固然是底工格局的布局,线程会平素不通,称为“死锁”。

死锁优于活锁,因为活锁既浪费CPU时间,又浪费内部存款和储蓄器,而死锁只浪费内部存款和储蓄器。

而掺杂构造具有两个之长,在一向不竞争的状态下,那些结构十分的快且不会窒碍(就如顾客形式的布局卡塔尔国,在存在对结构的竞争的情景下,它会被操作系统内核窒碍。(下生龙活虎章讲卡塔 尔(阿拉伯语:قطر‎

客户格局结构

CLRubicon保障对以下数据类型的变量的读写是原子性的:Boolean,Char,S(Byte),U(Int16),U(Int32),U(IntPtr),Single以至援引类型。

这象征变量中的全数字节都是三回性读取或写入。(举个反例,对于一个Int64静态变量初叶化为0,七个线程写它的时候只写了一半,另一个线程读取的时候读取到的是中间状态。然则话说回来,貌似陆拾一位机器三次性读取63位,是否在此个时候Int64也会编制程序原子性呢,未表明,不过不影响大家通晓。卡塔 尔(阿拉伯语:قطر‎

本章解说的基元顾客格局组织就在于规划好那些原子性数据的读取/写入时间。

骨子里这么些构造也能够强制为Int32和Double这一个项目数据开展原子性的策画好时间的拜谒。

有二种基元顾客情势线程同步构造

  • 易变构造
  • 互锁构造

负有易变和互锁构造都务求传递对含蓄轻易数据类型的叁个变量的援用(内部存款和储蓄器地址卡塔尔国。

易变构造

在讲易变构造以前,得先讲叁个难题,正是代码优化的主题材料。

事先大家讲过C#编写翻译器,JIT编写翻译器,CPU都或许会优化代码,规范的例子便是提姆er的接纳,二个Timer对象在持续未有使用的图景下,大概一贯被优化掉了,根本不会按期实施回调函数。

而那么些优化效用是很难在调节和测验的时候看出来,因为调节和测验的时候并不曾对代码举办优化。

而四线程也会促成那样的难点,举例二个线程回调函数用到有些静态变量后,且并不改换这几个变量,那么也许就能够进展优化,认为那些变量的值不改变,让其一向优化成固定的值。而你当然的指标实在另一个线程中更正那几个静态变量的值,今后您的转移也起不断效果看了。

同一时间以下那样的代码来讲只怕因为代码的施行顺序分化而出现超过预想的结果。

        static int you = 0;
        static int me = 0;
        private static void Thread1() {
            me = 2;
            you = 2;
        }
        private static void Thread2()
        {
            if (you == 2) {
                Console.WriteLine(me);

        }    

像上面的代码,Thread1和Thread2方法分别在几个线程中循环运转。

依照大家预测的结果是,当Thread1运转完了,那么Thread2就能够检测到你2了,然后就打字与印刷自己是2.

而是因为编写翻译器优化的来由,you=2和me=2的逐生机勃勃完全部是足以反过来的,那么超越写了you=2后,me=2那句代码还未有实践,这个时候Thread2已经上马检查评定到you==2了,那么当时打印的话,会显示小编不是2,是0.

要么Thread第11中学的顺序未有变,而Thread2中的顺序变了,即you读取到数据和me读取到数据的代码也是足以被优化的,编写翻译器在Thread1未运行时,先读了me的值为0,而此刻Thread1运转了,即便给了me为2,不过线程2的存放器中曾经存为0了,所以未读取,那么当时结果仍然为您是2,而自己不是2;

要解决这么些难点就引进了作者们的易变构造,那须要了然到四个静态类System.Threading.Volatile,它提供了七个静态方法Write和Read。

那四个主意相比较特殊,它们会防止C#编写翻译器,JIT编写翻译器和CPU常常实践的大器晚成对优化。

切实的落实在于,Write方法会保障函数中,全体在Write方法早前实施的多少读写操作都在Write方法写入早前就进行了。

而Read方法会保障函数中,全体在Read方法试行之后的数码读写操作,一定实在Read方法实践后才开展。

改革代码后

        static int you = 0;
        static int me = 0;
        private static void Thread1() {
            me= 2;
            Volatile.Write(ref you,2);
        }
        private static void Thread2()
        {
            if (Volatile.Read(ref you) == 2) {
                Console.WriteLine(me);

        }

那时候因为Volatile.Write使编写翻译器会确定保障函数中,全数在Write方法在此之前施行的数额读写操作都在Write方法写入早先就实践了。

也正是说编写翻译器不会在试行的时候将you=2放在me=2后边了。消除了前边说的第风姿罗曼蒂克种状态。

而Volatile.Read保证函数中,全数在Read方法推行之后的数量读写操作,一定实在Read方法实行后才进行。

也正是说me读取明确在有读取数据的末端,也就解决了前边说的第三种情景。

然则正如您所看见的,那很难掌握,关键是投机用到项目中都会感觉真蛋疼,还得百度时而探问是否Read和Write的保证记混了。

为此为了简化编制程序,C#编译器提供了volatile关键字,它能够动用于事先提到的那么些原子性的简易类型。

volatile申明后,JIT编写翻译器会确定保证易变字段都以以易变读取和写入的艺术举办,不必突显调用Read和Write。(也正是说只要用了volatile,那么me=2的机能正是Volatile.Write(ref
me,2),同理读也是相仿卡塔 尔(阿拉伯语:قطر‎

并且volatile会告诉C#编写翻译器和JIT编写翻译器不将字段缓存到CPU寄放器,确定保障字段的享有读写操作都在内部存款和储蓄器中张开。

这段日子再改写此前的代码:

        static volatile int you = 0;
        static int me = 0;
        private static void Thread1() {
            me= 2;
            you=2;
        }
        private static void Thread2()
        {
            if (you == 2) {
                Console.WriteLine(me);

        }

唯独小编却表示并恶感volatile关键字,因为现身上述所说的情状的概率超低,何况volatile幸免优化后对品质会有影响。且C#不援救以传援引的措施传递volatile变量给有些函数。

互锁构造

协商互锁构造,就要说System.Threading.Interlocked类提供的法子。

其意气风发类中的每一种方法都实施一遍原子性的读可能写操作。

以此类中的全体办法都创造了完全的内部存款和储蓄器栅栏,也正是说调用有些Interlocked方法在此以前的别的变量写入都在此个Interlocked方法调用以前施行,而这么些调用之后的任何变量读取都在那些调用之后读取。

它的功力就卓殊以前的Volilate的Read和Write的出力加在一齐。

笔者推荐使用Interlocked的方法,它们不唯有快,而且也能做过多事务,比轻巧的加(Add卡塔 尔(英语:State of Qatar),自增(Increment卡塔尔国,自减(Decrement卡塔 尔(阿拉伯语:قطر‎,沟通(Exchange卡塔尔。

Interlocked的章程就算好用,但第生龙活虎用以操作Int类型。

要是想要原子性地操作类对象中的意气风发组字段,那么能够用以下情势达成:

/// <summary>
    /// 简单的自旋锁
    /// </summary>
    struct SimpleSpinLock {
        private Int32 m_ResourceInUse;//0表示false,1表示true

        public void Enter() {
            while (true) {
                //将资源设为正在使用,Exchange方法的意思是,将m_ResourceInUse赋值为1,并返回原来的m_ResourceInUse的值
                if (Interlocked.Exchange(ref m_ResourceInUse, 1) == 0) return;

            }
        }

        public void Leave() {
            Volatile.Write(ref m_ResourceInUse, 0);
        }
    }
    public class SomeResource {
        private SimpleSpinLock m_sl = new SimpleSpinLock();
        public void AccessResource() {
            m_sl.Enter();
            /*每次只有一个线程能访问到这里的代码*/
            m_sl.Leave();
        }
    }

上边的代码原理正是,当二个线程调用Enter后,那么就能return,并置m_ResourceInUse为1,此时意味着财富被占用了。

风华正茂旦其它二个线程再调用Enter,那么获得的m_ResourceInUse为1,所以不会再次回到,就不断实施循环,直到第八个线程调用Leave函数,将m_ResourceInUse置为0。

规律非常粗略,但相信看那个方式的人也理应很明白了,也等于说只要第三个线程不脱离,其余具备的线程都要不停拓宽巡回操作(术语为自旋卡塔尔。

故而自旋锁应该是用来保证那么些会实行得这么些快的代码区域。(且不要用在单CPU机器上,因为据有锁的线程不可能飞速释放锁卡塔尔国

设若占领锁的线程优先级鬼世界想要获取锁的线程,那么那就引致占领锁的线程恐怕一贯没机缘运营,更别提释放锁了。(那正是活锁,前边也涉嫌了卡塔尔

事实上FCL就提供了二个像样的自旋锁,相当于System.Threading.SpinLock结构,而且仍旧用了SpinWait结构来增进质量。

由于SpinLock和早前大家本人写的SimpleSpinLock都以结构体,相当于说他们都以值类型,都以轻量级且内存友好的。

不过而不是传递它们的实例,因为值类型会复制,而你将错失全数的一块儿。

骨子里Interlocked.CompareExchange本来就足以不止用于操作整数,还足以用来操作其余原子性的基元类型,他还会有贰个泛型方法。

它的成效是,相比较第4个参数和第一个参数,如果两岸对等,那么将第4个参数的值赋给第四个参数,并回到第一个参数早先的值。

根基形式协会

幼功格局比顾客方式慢,这些是足以预知的,因为线程要从托管代码转为本机客户形式代码,再转为内核格局代码,然后原路返回,也就精通怎么慢了。

只是此前也介绍过了,内核格局也颇有客商格局所不有所的独特之处:

  • 水源方式的布局检查评定到一个财富上的角逐,windows会窒碍输掉的线程,使他不会像在此以前介绍的顾客形式那样“自旋”(也正是特不断循环的鬼卡塔 尔(阿拉伯语:قطر‎,那样也就不会一贯占着叁个CPU了,浪费能源。
  • 水源方式的布局可落成本机和托管线程相互之间的三只
  • 水源形式的布局可同步在同后生可畏台机器的例外进程中运转的线程。
  • 水源情势的结构可采纳安全性设置,制止未经授权的帐户访谈它们。
  • 线程可径直不通,直到会集中具备内核格局组织可用,或直到集合中的任何内核形式结构可用
  • 在根本方式的协会上过不去的线程可钦赐超时值;钦赐时期内访问不到梦想的能源,线程就能够解除堵塞并实行职责。

事件和随机信号量是二种基元内核格局线程同步构造,至于互斥体什么的则是在此二者幼功上树立而来的。

System.Threading命名空间提供了三个虚幻基类WaitHandle。这些大约的类唯生机勃勃的功能正是包裹贰个Windows内核查象句柄。(它有意气风发部分派生类伊夫ntWaitHandle,AutoResetEvent,马努alResetEvent,Semaphore,Mutex卡塔 尔(阿拉伯语:قطر‎

WaitHandle基类内部有三个SafeWaitHandle字段,它包容三个Win32根本对象句柄。

那么些字段在构造三个具体的WaitHandle派生类时发轫化。

在五个幼功情势的组织上调用的每个方法都意味着三个总体的内部存款和储蓄器栅栏。(从前也说过了,表示调用那个主意以前的任何变量的写入都不得不在那措施前完成,调用那一个艺术之后的其余变量的读取都必须要在那格局后产生卡塔尔。

那几个类中的方法就不具体介绍了,基本上那几个主意的要紧成效吗个正是调用线程等待叁个或多个底层底工对象吸取时域信号。

只是要留意在等待多少个的点子(即WaitAll和WiatAny这种卡塔 尔(英语:State of Qatar)中,传递的木本数组参数,数组最大因素数不可能高出64,不然会那些。

重要讲一下八个内核构造,也是前面WaitHandle的多个少年老成直接轨派生类:

  • EventHandle(Event构造)

    • 事件实际正是由根基维护的Boolean变量。为false就不通,为true就撤销堵塞。
    • 有二种事件,即自动重新设置事件(AutoReset伊夫nt卡塔尔和手动重新恢复生机设置事件(马努alResetEvent卡塔尔。差距就在于是还是不是在触发一个线程的封堵后,将事件自动重新载入参数为false。
    • 用自动重新初始化事件写个锁示举例下:

        /// <summary>
          /// 简单的阻塞锁
          /// </summary>
          class SimpleWaitLock {
              private readonly AutoResetEvent m_ResourceInUse;
      
              public SimpleWaitLock() {
                  m_ResourceInUse = new AutoResetEvent(true);//初始化事件,表示事件构造可用
              }
      
              public void Enter() {
                  //阻塞内核,直到资源可用
                  m_ResourceInUse.WaitOne();
              }
      
              public void Leave() {
                  //解除当前线程阻塞,让另一个线程访问资源
                  m_ResourceInUse.Set();
              }
              public void Dispose() {
                  m_ResourceInUse.Dispose();
              }
          }
      

      此示例能够和后边的丰裕自旋锁绝相比,调用方法同样。

  • Semaphore(Semaphore构造)

    • Semaphore的保加孟菲斯语正是时域信号量,其实是由底子维护的Int32变量。时域信号量为0时,在功率信号量上等候的线程拥塞,频限信号量大于0时触及堵塞。随机信号量上等候的线覆灭窒碍时,信号量自动减1.
    • 长久以来二个例证来代表,与地点代码相比之后更清晰:(时限信号量最大值设置为1的话,且释放的时候也只释放三个来讲,那么实际上和事件效果等同卡塔 尔(阿拉伯语:قطر‎

       /// <summary>
          /// 简单的阻塞锁
          /// </summary>
          class SimpleWaitLock {
              private readonly Semaphore m_ResourceInUse;
      
              public SimpleWaitLock(Int32 maxCount) {
                  m_ResourceInUse = new Semaphore(maxCount, maxCount);
              }
      
              public void Enter() {
                  //阻塞内核,直到资源可用
                  m_ResourceInUse.WaitOne();
              }
      
              public void Leave() {
                  //解除当前线程阻塞,让另外2个线程访问资源
                  m_ResourceInUse.Release(2);
              }
              public void Dispose() {
                  m_ResourceInUse.Close();
              }
          }
      
  • Mutex(Mutex构造)

    • Mutex的华语就是互斥体。代表了叁个倾轧的锁。
    • 互斥体有二个外加的逻辑,Mutex会记录下线程的ID值,若是释放的时候不是以此线程释放的,那么就不会释放掉,并且还恐怕会抛非常。
    • 互斥体实际上在有限协理一个递归计数,四个线程当前怀有三个Mutex,而后该线程再一次在Mutex等待,那么此计数就能依次增加,而线程调用ReleaseMutex会招致依次减少,只有计数依次减少为0,那么这些线程才会去掉拥塞。另三个线程才会称呼该Mutex的持有者
    • Mutex对象急需相当的内存来包容这几个记录下来的ID值和计数音讯,而且锁也会变得更加慢了。所以重重人制止用Mutex对象。
    • 平日说来几个艺术在动用三个锁时调用了另八个方式,那么些格局也要用到锁,那么就能够设想用互斥体。因为用事件这种功底构造方法的话,在调用的另贰个主意中用到锁就能够促成短路,从而死锁。例子:

       public class SomeResource {
              private readonly Mutex m_lock = new Mutex();
              public void Method1() {
                  m_lock.WaitOne();
                  Method2();//递归获取锁
                  m_lock.ReleaseMutex();
              }
              public void Method2()
              {
                  m_lock.WaitOne();
                  /*做点什么*/
                  m_lock.ReleaseMutex();
              }
          }
      

      像上述的这种组织即便轻易得用事件来写就能够有标题,然则而不是不可能用事件去递归落成,而且借使用以下的法子递归实现效果与利益反而会更加好:

    • 用事件措施落实递归锁:

      /// <summary>
          /// 事件构造实现的递归锁,效率比Mutex高很多
          /// </summary>
          class ComplexWaitLock:IDisposable {
              private  AutoResetEvent m_lock=new AutoResetEvent(true);
              private Int32 m_owningThreadId = 0;
              private Int32 m_lockCount = 0;
      
              public void Enter() {
                  //获取当前线程ID
                  Int32 currentThreadId = Thread.CurrentThread.ManagedThreadId;
                  //当前线程再次进入就会递增计数
                  if (m_owningThreadId == currentThreadId) {
                      m_lockCount++;
                      return;
                  }
                  m_lock.WaitOne();
                  m_owningThreadId = currentThreadId;
                  m_lockCount = 1;
      
              }
      
              public void Leave() {
                  //获取当前线程ID
                  Int32 currentThreadId = Thread.CurrentThread.ManagedThreadId;
                  if (m_owningThreadId != currentThreadId)
                      throw new InvalidOperationException();
      
                  if (--m_lockCount == 0) {
                      m_owningThreadId = 0;
                      m_lock.Set();
                  } 
              }
              public void Dispose() {
                  m_lock.Dispose();
              }
          }
      

      上边的代码其实很好搞懂,正是用事件把Mutex的游戏的方法本身完毕了。可是上边的代码之所以比Mutex快,是因为这几个代码都以用托管代码在落实,实际不是像Mutex同样用基本代码,仅仅唯有调用事件组织的点午时才会用到底工代码。

发表评论

电子邮件地址不会被公开。 必填项已用*标注