乐趣区

关于c#:线程安全Ⅱ

混合模式

因为用户模式和内核模式各有优劣,为了利用两者各自的有点,因而能够同时应用两种模式来进行结构,在没有线程竞争的时候能够具备用户模式的性能劣势,而在多个线程同时竞争一个结构的时候又能提供不产生自旋的长处,使应用程序的性能失去晋升。

示例代码
class HybridLock : IDisposable
{
    private int m_Waiters = 0;

    private AutoResetEvent m_WaiterLock = new AutoResetEvent(false);

    public void Enter()
    {
        // 线程想要取得锁
        if (Interlocked.Increment(ref this.m_Waiters) == 1)
            return; // 锁能够应用直, 接返回

        // 另一个线程正在期待,阻塞该线程
        this.m_WaiterLock.WaitOne(); // 产生较大的性能影响
        //WaitOne 返回后,这个线程便领有了锁
    }

    public void Leave()
    {
        // 这个线程筹备开释锁
        if (Interlocked.Decrement(ref this.m_Waiters) == 0)
            return; // 没有其它线程阻塞,间接返回

        // 有其它线程正在阻塞,唤醒其中一个
        this.m_WaiterLock.Set(); // 产生较大的性能影响}

    public void Dispose()
    {this.m_WaiterLock.Dispose();
    }
}
剖析代码

HybridLock 对象的 Enter 办法调用了 Interlocked.Increment,让 m_Waiters 字段递增 1,这个线程发现没有线程领有这个锁,因而该线程取得锁并间接返回 (取得锁的速度十分快)。如果另一个线程染指并调用 Enter,此时 m_Waiters 字段再次递增值为 2,发现锁被另一个线程领有,所以这个线程会调用 AutoResetEvent 对象的 WaitOne 来阻塞自身 (这里因为是内核模式所以会产生性能影响),避免自旋。

再看看 Leave 办法,一个线程调用 Leave 时,会调用 Interlocked.Decrement 使 m_Waiters 字段递加 1,如果 m_Waiters 字段是 0,阐明没有其它线程在 Enter 的调用中产生阻塞,这时线程能够间接返回。然而如果线程发现 m_Waiters 字段的值递加后不为 0,阐明存在线程竞争,至多有一个线程在内核中阻塞,这个线程必须只能唤醒一个阻塞的线程,通过调用 AutoResetEvent 对象的 Set 办法实现,

自旋、线程所有权和递归

因为转换成内核代码会造成性能损失,而线程占有一个锁的工夫通常比拟短,所以能够先让线程处于用户模式且自旋一段时间,若还未取得锁的权限便可让它转为内核模式,如果线程在期待期间锁变得可用便可防止转为内核模式了。

示例代码 (提供自旋、线程所有权和递归反对)
class HybridLock : IDisposable
{
    // 用户模式
    private int m_Waiters = 0;

    // 内核模式
    private AutoResetEvent m_WaiterLock = new AutoResetEvent(false);

    // 管制自旋
    private int m_SpinCount = 1000;

    // 用有锁的线程
    private int m_OwningThreadId = 0;

    // 领有次数
    private int m_Recursion = 0;

    public void Enter()
    {
        // 如果线程曾经领有,递增递归次数并返回
        var threadId = Thread.CurrentThread.ManagedThreadId;
        if(this.m_OwningThreadId == threadId)
        {
            this.m_Recursion++;
            return;
        }

        // 尝试获取
        var spinWait = new SpinWait();
        for (int i = 0; i < this.m_SpinCount; i++)
        {
            // 如果锁能够应用了
            if (Interlocked.CompareExchange(ref this.m_Waiters, 1, 0) == 0)
                goto GotLock;

            // 给其它线程运行的机会,心愿锁会被开释
            spinWait.SpinOnce();}

        // 自旋完结,依然没有取得锁则再试一次
        if(Interlocked.Increment(ref this.m_Waiters) > 1)
        {
            // 有其它线程被阻塞,这个线程也必须阻塞
            this.m_WaiterLock.WaitOne(); // 性能损失
            // 期待该线程用有锁醒来
        }

    GotLock:
        // 一个线程用有锁时,记录 ID 并指出线程领有锁一次
        this.m_OwningThreadId = threadId;
        this.m_Recursion = 1;
    }

    public void Leave()
    {
        // 如果线程不必有锁,bug
        var threadId = Thread.CurrentThread.ManagedThreadId;
        if (threadId != this.m_OwningThreadId)
            throw new SynchronizationLockException("线程未领有锁!");

        // 递加递归计数,如果线程依然用有锁,间接返回
        if (--this.m_Recursion > 0)
            return;

        // 当初没有线程领有锁
        this.m_OwningThreadId = 0;

        // 若没有其它线程被阻塞间接返回
        if (Interlocked.Decrement(ref this.m_Waiters) == 0)
            return;

        // 唤醒一个被阻塞的线程
        this.m_WaiterLock.Set(); // 性能损失}

    public void Dispose()
    {this.m_WaiterLock.Dispose();
    }
}
Monitor 类

这个类提供了一个互斥锁,这个锁反对自旋、线程所有权和递归,是线程同步罕用的类。

工作形式

CLR 初始化时调配一个同步块数组,一个对象在堆中创立的时候,都有两个额定的字段与它关联,第一个字段是“类型对象指针”,它蕴含类型对象的内存地址。第二个字段是“同步索引块”,它蕴含同步块数组中的一个整数索引。

一个对象在结构时,对象的同步块索引初始化为 -1,表明不援用任何同步块。而后调用 Monitor.Enter 时,CLR 在数组中找到一个空白同步块,并设置对象的同步块索引来援用该同步块。调用 Exit 时,会查看是否有其它任何线程正在期待应用对象的同步块,如果没有线程在期待,同步块就能自在被应用了,Exit 将对象的同步块索引改回 -1,自在的同步块未来能够和另一个对象关联。

堆中对象的同步块索引和 CLR 的同步块数组元素之间的关系图

Monitor 的应用
class SomeType
{
    // 对象公有锁
    private readonly object m_Lock = new object();

    public void DoSomething()
    {
        // 进入公有锁
        Monitor.Enter(this.m_Lock);
        // 其它代码...

        // 退出公有锁
        Monitor.Exit(this.m_Lock);
    }
}
ReaderWriterLockSlim 类
性能介绍
  • 一个线程写入数据时,申请拜访的其它所有线程都被阻塞
  • 一个线程读取数据时,申请读取的其它线程容许继续执行,但申请写入的线程还是会被阻塞
  • 写入数据的线程完结后,能够解除一个写入线程的阻塞,使它能写入数据,也能够解除所有读取线程的阻塞,让它们并发读取数据。如果没有线程被阻塞,锁就会进入自在状态,容许下一个 reader 或 writer 线程获取
  • 读取数据的所有线程完结后,一个 writer 线程会被解除阻塞,使它能写入数据,如果没有线程被阻塞,锁就会进入自在状态,容许下一个 reader 或 writer 线程获取
ReaderWriterLockSlim 用法
class SomeType : IDisposable
{private readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

    public void DoWrite()
    {this.m_Lock.EnterWriteLock();
        // 其它代码...

        // 退出公有锁
        this.m_Lock.ExitWriteLock();}

    public void DoRead()
    {this.m_Lock.EnterReadLock();
        // 其它代码...

        this.m_Lock.ExitReadLock();}

    public void Dispose()
    {this.m_Lock.Dispose();
    }
}

在结构 ReaderWriterLockSlim 对象的时候容许传递一个 LockRecursionPolicy 标记,如果传递 LockRecursionPolicy.SupportsRecursion,锁就能够反对线程所有权和递归行为 (这些行为会对锁的性能产生负面影响),如果反对这些行为锁必须跟踪容许进入锁的所有 reader 线程,同时为每个线程都独自保护一个递归技术,破费更大的代价,因而倡议结构的时候传入 LockRecursionPolicy.NoRecursion。

双检锁 (提早初始化)
class Singleton
{private static object s_Lock = new object();

    private static Singleton s_Value = null;

    // 公有结构
    private Singleton() {}

    public static Singleton GetSingleton()
    {if (s_Value != null)
            return s_Value;

        // 让一个线程创立它
        Monitor.Enter(s_Lock);
        if (s_Value == null)
        {var temp = new Singleton();
            Interlocked.Exchange(ref s_Value, temp);
        }
        Monitor.Exit(s_Lock);
        return s_Value;
    }
}
为何应用 Interlocked.Exchange

为何不间接 s_Value=new Singleton(),起因在于这种写法编译器可能先为 Singleton 分配内存,再将援用赋值给 s_Value,最初调用结构器。从单线程来看这样并没什么问题,如果是多线程,此时在援用曾经赋值给 s_Value 了,然而却还未调用结构器,这时另一个线程调用了 GetSingleton 办法,发现 s_Value 不为 null,所以开始应用 Singleton 对象,然而对象的结构器还未执行完结,产生 bug。而应用 Interlocked.Exchange 能够修改该问题,办法保障 temp 中的援用只有在结构器执行完结后才赋值给 s_Value。

Monitor 类的 Wait 与 Pulse

如果一个线程心愿一个复合条件为 true 时执行一些代码,便能够应用 Wait 与 Pulse,在条件不满足的时候 Wait,另一个线程更改条件后 Pulse,而非让线程自旋间断检测条件,节约 CPU 工夫。

示例代码
class ConditionVariablePattern
{private readonly object m_Lock = new object();
    private bool m_Condition = false;

    public void Thread1()
    {
        // 获取一个互斥锁
        Monitor.Enter(this.m_Lock);

        // 在锁中原子性地检测复合条件
        while (!this.m_Condition)
        {
            // 条件不满足,期待其它线程批改
            Monitor.Wait(this.m_Lock);
        }

        // 条件满足,解决数据...
        Monitor.Exit(this.m_Lock);
    }

    public void Thread2()
    {
        // 获取一个互斥锁
        Monitor.Enter(this.m_Lock);

        // 解决数据并批改条件
        this.m_Condition = true;

        //Monitor.Pulse(this.m_Lock);     // 开释之后唤醒一个正在期待的线程
        Monitor.PulseAll(this.m_Lock);  // 开释之后唤醒所有正在期待的线程

        Monitor.Exit(this.m_Lock);
    }
}
代码解析

Thread1 获取一个互斥锁,而后对一个条件变量进行检测,如果条件不满足,调用 Monitor.Wait 开释锁并阻塞调用线程,其它线程能取得改锁。

Thread2 获取锁的所有权,解决数据,造成一些状态的扭转,其中包含 Thread1 要检测的条件变量,而后调用 Monitor.PulseAll 或者 Monitor.Pulse,从而解除一个因为调用 Wait 办法而进入阻塞的线程。

其中 Pulse 只解除期待最久的线程的阻塞,PulseAll 解除所有期待线程的阻塞,然而解除阻塞的线程还必须期待 Thread2 线程调用完 Exit 能力领有锁。

Thread1 醒来时,进行下一次循环迭代,再次对条件进行检测,如过条件仍为 false,持续调用 Wait。如果条件满足,解决一些数据,最初调用 Exit 永恒开释锁。

BlockingCollection 类实现生产者 / 消费者模式
static void Main()
{
    //BlockingCollection 类实现生产者 / 消费者模式
    var bl = new BlockingCollection<int>(new ConcurrentQueue<int>());

    // 由一个线程池执行生产
    ThreadPool.QueueUserWorkItem(ConsumeItems, bl);

    // 在汇合中增加数据项
    for (int i = 0; i < 6; i++)
    {Console.WriteLine("production item : {0}", i);
        bl.Add(i);
    }

    // 告诉生产线程不会在汇合中增加更多的 item 了
    bl.CompleteAdding();

    Console.ReadKey();}
运行后果

生产与消费行为可能呈现交织

退出移动版