c# 高级编程 21章480页 【任务和并行编程】【lock语句】【Monitor类】【SpinLock】

110 阅读3分钟

lock 语句

为了线程同步

在一个地方使用lock语句,并不意味着 访问对象的其他线程都在等待

必须对 每个访问共享状态的线程 显式地 使用同步


lock(共享的 引用类型的 实例)

    public class SharedState
    {
        public int State { get; set; }
    }
    class Program
    {
        static void Main()
        {
            int numTasks = 20;
            var state = new SharedState();
            var tasks = new Task[numTasks];

            for (int i = 0; i < numTasks; i++)
            {
                tasks[i] = Task.Run(() => new Job(state).DoTheJob());
            }

            Task.WaitAll(tasks);

            Console.WriteLine($"summarized {state.State}");
        }
    }
    public class Job
    {
        private SharedState _sharedState;

        public Job(SharedState sharedState)
        {
            _sharedState = sharedState;
        }

        public void DoTheJob()
        {
            for (int i = 0; i < 50000; i++)
            {
                lock (_sharedState)
                {
                    _sharedState.State += 1;
                }
            }
        }
    }

输出:

summarized 1000000

对比一: 这里的另一种做法 依然 无法消除 争用条件这另一种做法是

仅把 共享的 引用类型 变成 线程安全的,如下:

public class SharedState 
{
    private int _state = 0;
    private object _syncRoot = new object();
    public int State
    {
        get { lock(_syncRoot) { return _state; }}
        set { lock(_sycnRoot) { return _state = value; } }
    }
}

对比二: 这里的另另一种做法 可以消除 争用条件这另另一种做法是

修改 共享的 引用类型的 设计,使其封装 _state的递增,并且让_state的递增 是原子的

public class SharedState 
{
    private int _state = 0;
    private object _syncRoot = new object();
    public int State => _state;
    public int IncrementState()
    {
        lock (_syncRoot) 
        {
            return ++_state;
        }
    }
}

lock(typeof(StaticClass)) 要锁定静态成员

放在object类型或静态成员

lock(typeof(StaticClass))
{
}

lock(this) 将 类的成员 设置为 线程安全的

  • 一次只能有一个线程 可以访问 相同实例 的 DoThis(),DoThat()方法
public class Demo
{
    public void DoThis()
    {
        lock(this)
        {
        }
    }
    
    public void DoThat()
    {
        lock(this)
        {
        }
    }
}

lock(_syncRoot)

当 类的实例 需要从外部 被同步访问,但又不适宜在 类自身中 进行这种控制 的时候,就可以用以下写法...

public class Demo
{
    private object _syncRoot = new object();
    public void DoThis()
    {
        lock(_syncRoot)
        {
        }
    }
    
    public void DoThat()
    {
        lock(_syncRoot)
        {
        }
    }
}

对比 lock(this)lock(_syncRoot)

自我感觉:

  • lock(this)
    • DoThis(),DoThat() 方法的调用同步
  • lock(_syncRoot):
    • DoThis(),DoThat() 方法同步
    • DoThis(),DoThat() 方法的调用 不是同步

实现一个类:让它可以 有时是同步的,有时是异步的

public class Demo
{
    public virtual bool IsSynchronized => false;
    public virtual void DoThis()
    {
    }
    public virtual void DoThat()
    {
    }
    public static Demo Synchronized(Demo demo)
    {
        if(!demo.IsSynchronized)
        {
            return new SynchronizedDemo(demo);
        }
        return demo;
    }
    
    private class SynchronizedDemo : Demo
    {
        private object _syncRoot = new object();
        private Demo _demo;
        public SynchronizedDemo(Demo demo)
        {
            _demo = demo;
        }
        public override bool IsSynchronized => true;
        public override void DoThis()
        {
            lock(_syncRoot)
            {
                _demo.DoThis();
            }
        }
        public override void DoThat()
        {
            lock(_syncRoot)
            {
                _demo.DoThat();
            }
        }
    }
}

Monitor类

lock语句 由C#编译为Monitor

  • Monitor.Enter(): 一直等待,直到线程锁定对象为止
  • Monitor.Exit():
    • 解除锁定
    • 如果同步访问的区域有了异常,就会在finally里调用此方法解除锁定
lock(obj)
{
}

编译为

Monitor.Enter(obj);
try
{
    //obj的同步访问区域
}
finally
{
    Monitor.Exit(obj);
}

lock相比,Monitor类的主要优点是:可以添加一个等待锁定的超时值

  • Monitor.Enter(_obj, 500, ref _lockTaken)
    • 超时值:500毫秒
    • 是否锁定:_lockTaken
      • 如果锁定,则被置为true
      • 如果未锁定 且超时,则被置为false
bool _lockTaken = false;

Monitor.Enter(_obj, 500, ref _lockTaken);
if(_lockTaken)
{
    try
    {
        //锁定
        //obj的同步访问区域
    }
    finally
    {
        Monitor.Exit(obj);
    }
}
else
{
    //未锁定
    //做其他事
}


SpinLock

以下情况可以使用SpinLock:

如果 被锁定的对象 由于垃圾收集 系统开销过高

如果 有大量的锁定锁定的时间总是非常短

使用SpinLock需要注意:

避免使用多个 SpinLock

不要调用 任何可能阻塞 的内容

传递SpinLock实例的时候要小心。因为SpinLock是结构,把一个变量赋予另一个变量会创建一个副本。要总是通过引用传递SpinLock实例

SpinLock的用法:

  • Enter()TryEnter()获得锁定
  • Exit()释放锁定
  • 属性IsHeld: 检查当前是否锁定
  • 属性IsHeldByCurrentThread:检查当前是否锁定