C# lock 关键字的一些理解

 C# lock 关键字的一些理解

问题1:谁是锁?

lock 这个关键字,并不是“锁”,真正的“锁”是那个被lock的Object类型的“对象”,请注意,这里为“对象”加了双引号着重强调被lock的是对象类型。

问题2:这个锁有什么用?

在C# lock关键字定义如下:

lock(expression) statement_block  //其中expression代表你希望跟踪的对象,通常是对象引用。

根据lock的定义,它有两种作用

作用1:锁住括号中的对象

只让当前线程拥有该对象的变量、方法、属性等的使用权,lock后一个时刻只可能被一个线程操作。

如果你想保护一个类的实例,一般地,你可以使用this;如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。

作用2:锁住()后面的代码块(一般放在{}中)

就是定义中的statement_block,这里代表互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

举个例子,多数商场厕所的蹲位都是小单间型的,也就是一次只能进去一个人,商如何确保每次只能进去一个人呢?不就是一个人进去之后顺手把门锁上么?这样你在里面干啥事,外边的人也只能等待你解放完了,才能进入。以此类推,某个 object 对象被lock之后,lock这个对象的那个线程就拥有了执行lock()后面{}的完整的独立执行权,完整且独立的执行,不可分割的执行,也就是说,{}是一块临界代码段。

看下面代码

private static object objlock = new object();
lock (objlock)
{
    //这里要做一些事情
}

根据问题1的答案进行推导,objlock才是那把锁,lock 一下,当前线程就获得了objlock对象紧跟在lock()后面的{}的独立使用权,在此期间,其他谁都得等着拥有objlock钥匙的线程执行完之后才能使用{}里面的代码。

更专业一点的说法是:在.Net中,每个对象都有一个与之关联的锁,对象可以得到并释放它以便在任意时间只有一个线程可以访问对象实例变量和方法。同样.Net中的每一个对象都可以提供一个允许自己进入等待状态的机制。上面提到的独立使用权,就是对象的互斥锁。(关于这个说法,求证无果,只是从博客中找到的,msdn上暂时没找到相关说法)。

问题3.为什么只能lock引用类型?

因为只有引用类型才有互斥锁,如果强行lock值类型,c#会把值装箱成引用类型,下一次再lock,还会装箱,这两次装箱实际上是装成了两个箱子,就不是用一个内存区间了,所以锁的概念就没有意义。

问题4.避免lock public类型的对象是为什么?

还是以厕所为例子吧,私有就好比,这把锁只有你能访问到,而且最好这把锁不会因为外力而有所改变,别人访问不到,这样才能保证你进去了,别人就进不去了,如果是公有的,就好比你蹲位小单间的锁不是安装在里面而是安装在外边的,别人想不想进就不是你所能控制的了,这样也不安全。

问题5.下面代码中_dic到底能不能被其他线程操作?

private static Dictionary<int, BoKwdCueItem> _dic=new Dictionary<int, BoKwdCueItem>();
static object _obj = new object();
lock (_obj)
{
///对_dic的写操作
}

个人理解上 _dic 存在被其他线程修改的可能。

private static Dictionary<int, BoKwdCueItem> _dic=new Dictionary<int, BoKwdCueItem>();
static object _obj = new object();
lock (_obj)
{
    ///对_dic的写操作
}

public void AddToDic(int data)
{
    _dic.Add(data);
}

 假设A线程执行到lock(_obj)的{}的一半的时候,B线程获取的CPU的使用权,然后B线程调用了AddToDic方法,就会修改_dic的数据。

以上内容为网上搜集与个人理解,我也不保证一定对。 

问题6.为什么msdn建议的是使用lock锁住private static readonly的对象?

private static readonly object obj = new object();

为什么要设置成只读的呢?这时因为如果在lock代码段中改变obj的值,其它线程就畅通无阻了,因为互斥锁的对象变了,object.ReferenceEquals必然返回false。

至于private,上面已经提到了。

问题7.lock究竟是什么?

lock关键字其实就是封装了Moniter类的一些操作,详情介绍见msdn上关于lock的解释,下面是原文摘录。

lock 语句具有以下格式

lock (x)
{
    // Your code...
}

其中 x 是引用类型的表达式。 它完全等同于

object __lockObj = x;
bool __lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
    // Your code...
}
finally
{
    if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}

关于Monitor类这里暂时不做过多的解释。

倒是msdn上的一个示例非常值得借鉴,应该算是lock最简单的应用方式了。以下示例定义了一个 Account 类,该类通过锁定专用的 balanceLock 实例来同步对其专用 balance 字段的访问。 使用相同的实例进行锁定可确保尝试同时调用 Debit 或 Credit 方法的两个线程无法同时更新 balance 字段。

using System;
using System.Threading.Tasks;

public class Account
{
    private readonly object balanceLock = new object();
    private decimal balance;

    public Account(decimal initialBalance)
    {
        balance = initialBalance;
    }

    public decimal Debit(decimal amount)
    {
        lock (balanceLock)
        {
            if (balance >= amount)
            {
                Console.WriteLine($"Balance before debit :{balance, 5}");
                Console.WriteLine($"Amount to remove     :{amount, 5}");
                balance = balance - amount;
                Console.WriteLine($"Balance after debit  :{balance, 5}");
                return amount;
            }
            else
            {
                return 0;
            }
        }
    }

    public void Credit(decimal amount)
    {
        lock (balanceLock)
        {
            Console.WriteLine($"Balance before credit:{balance, 5}");
            Console.WriteLine($"Amount to add        :{amount, 5}");
            balance = balance + amount;
            Console.WriteLine($"Balance after credit :{balance, 5}");
        }
    }
}

class AccountTest
{
    static void Main()
    {
        var account = new Account(1000);
        var tasks = new Task[100];
        for (int i = 0; i < tasks.Length; i++)
        {
            tasks[i] = Task.Run(() => RandomlyUpdate(account));
        }
        Task.WaitAll(tasks);
    }

    static void RandomlyUpdate(Account account)
    {
        var rnd = new Random();
        for (int i = 0; i < 10; i++)
        {
            var amount = rnd.Next(1, 100);
            bool doCredit = rnd.NextDouble() < 0.5;
            if (doCredit)
            {
                account.Credit(amount);
            }
            else
            {
                account.Debit(amount);
            }
        }
    }
}

我想着重说明的一些部分:

a、balanceLock的用法与命名。balanceLock在逻辑上是人为绑定给balance用的“锁”。

b、考虑这样一个问题:如果上面代码再加两个方法直接在不lock balanceLock对象的情况下操作balance,那么这两个方法在异步操作的时候,会出现什么情况??如果一个带lock的方法和一个不带lock的方法对balance异步操作又会出现什么情况??

答案在本文中找。


后记:好多重复,逻辑也有点乱,不打算改了,以上顺序就是在思考lock关键字的时候就是不断出现的问题的顺序,留作思路。

参考文章:

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/lock-statement

https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor?redirectedfrom=MSDN&view=netframework-4.7.2

http://www.cnblogs.com/promise-7/articles/2354077.html

posted @ 2018-09-15 11:02  feipeng8848  阅读(2569)  评论(0编辑  收藏  举报