C#基础之lock

时间:2022-11-13 06:44:11

1.lock的本质

  实现线程同步的第一种方式是我们经常使用的lock关键字,它将包围的语句块标记为临界区,这样一次只有一个线程进入临界区并执行代码。下面第一段的几行代码是关于lock关键字的使用方式,但更重要的是我们可以通过这个例子来看到lock关键字的本质。第二段是这个方法的IL指令集,从中可以看到lock其实也是一个语法糖,它的内部实现是采用了监视器Monitor。第三段代码是我写的lock内部实现的C#代码,由于lock内部有finally关键字,这将保证内部最后一定会执行exit方法。而如果我们自己使用Monitor的话有可能会一不小心忘记调用exit方法,因此使用lock更加可靠。在使用lock关键字时必须使用一个引用类型的参数,我如果将i字段放入lock会提示报错,可见使用lock无法将i进行装箱。当然这只是猜测,本质的原因将在后面解释。在上例中我new了一个o对象,这样锁的范围只包括lock语句块。如果lock中传进来的参数是一个外部对象,那么锁的范围将扩展到这个对象。官方文档上有这样一句话:“严格来说,提供的对象是用来唯一地标识由多个线程共享的资源,所以它可以是任意类型。然而实际上,此对象通常表示需要进行线程同步的资源“。从这句话可以知道选择引用参数时,应该选择多个线程需要操作的共享对象,像我这里随便创建的object对象不是官方推荐的做法。

        public void MyLock()
{
int i = ;
object o=new object();
lock (o)
{
i = ;
}
}
.method public hidebysig instance void MyLock() cil managed
{
.maxstack
.locals init (
[] int32 num,
[] object obj2,
[] bool flag,
[] object obj3,
[] bool flag2)
L_0000: nop
L_0001: ldc.i4.
L_0002: stloc.
L_0003: newobj instance void [mscorlib]System.Object::.ctor()
L_0008: stloc.
L_0009: ldc.i4.
L_000a: stloc.
L_000b: ldloc.
L_000c: dup
L_000d: stloc.
L_000e: ldloca.s flag
L_0010: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
L_0015: nop
L_0016: nop
L_0017: ldc.i4.
L_0018: stloc.
L_0019: nop
L_001a: leave.s L_002e
L_001c: ldloc.
L_001d: ldc.i4.
L_001e: ceq
L_0020: stloc.s flag2
L_0022: ldloc.s flag2
L_0024: brtrue.s L_002d
L_0026: ldloc.
L_0027: call void [mscorlib]System.Threading.Monitor::Exit(object)
L_002c: nop
L_002d: endfinally
L_002e: nop
L_002f: ret
.try L_000b to L_001c finally handler L_001c to L_002e
}
        public void MyLock2()
{
int i = ;
//注意isOk必须设置为false。如果加锁成功则会将isOk设置为true,否则为false。
//在加锁过程中如果没有异常,那么会将isOk设置为true。
bool isOk=false;
object o = new object();
Monitor.Enter(o, ref isOk);
try
{
i = ;
}
finally
{
Monitor.Exit(o);
}
}

2.lock的参数  

  在给lock传递参数时首先要避免使用public对象,因为有可能外部程序也在对这个对象加锁,比如下面第一段代码。可以看到有可能出现一种情况,那就是主线程执行到lock(objA)时,正好线程t执行到lock(objB)。此时objA被t锁住,objB又被主线程锁住,死锁就这样发生了。其次,如果使用public对象,那么程序员可能会使用lock(this)、lock(typeof(myType))、lock("string")。而上述三种情况微软都是不建议我们使用的,对于后两种情况msdn已经解释的很清楚这里我就不写了。可能是一开始看了网上一些错误的帖子,让我困扰了一段时间的是lock(this)。msdn原话是”lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁“。我蛮想知道得到这样一种情况,使用lock(this)会发送死锁,而不使用lock(this)则不会发生死锁。假设要发生死锁,那么有2个多个线程将互相等待。现在我要使用lock(this)来让死锁发生,所谓this就是锁定了当前执行方法的实例对象,而这个实例对象是public的。因此这种情况和上面使用public对象发生死锁的本质是一样的,那就是外部线程也要对该实例对象加锁,从而造成了2个线程相互等待的情况。比如下面第二段代码,结果和第一段代码类似(如下图),只不过在代码中使用了this关键字,同样也发生了死锁。

class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
Thread t = new Thread(myClass.LockFunc);
t.Start(); lock (myClass.objB)
{
Console.WriteLine("我是主线程,已对objB加锁,马上加锁objA");
/*
结果是:我是主线程,已对objB加锁,马上加锁objA
我是线程t,已对objA加锁,马上加锁objB
可见此时发送了死锁,这是一种情况,多试几次会出现顺利执行的结果。
*/
lock (myClass.objA)
{
Console.WriteLine("我是主线程,已对objA、objB都加锁");
}
}
}
} class MyClass
{
public object objA = new object();
public object objB = new object(); public void LockFunc()
{
lock (objA)
{
Console.WriteLine("我是线程t,已对objA加锁,马上加锁objB");
lock (objB)
{
Console.WriteLine("我是线程t,已对objA、objB都加锁");
}
}
}
}
    class Program
{
static void Main(string[] args)
{
object obj=new object();
MyClass myClass = new MyClass();
Thread t = new Thread(myClass.LockFunc);
t.Start(obj); lock (myClass)
{
Console.WriteLine("我是主线程,已对myClass加锁,马上加锁obj");
lock (obj)
{
Console.WriteLine("我是主线程,已对obj、myClass都加锁");
}
}
}
} class MyClass
{
public void LockFunc(object obj)
{
lock (obj)
{
Console.WriteLine("我是线程t,已对obj加锁,马上加锁myClass");
lock (this)
{
Console.WriteLine("我是线程t,已对obj、myClass都加锁");
}
}
}
}

C#基础之lock

  现在如果要设置lock参数,我们知道要首选私有成员。不过除了将对象设置为私有成员外,我们还应该最好将其设置为只读成员。如果没有将对象设置为只读成员,那这种情况一定要注意,因为它会让锁失效。代码如下所示, 这段代码的运行结果是下面的第一张图。从图中可以看到因为使用了lock加锁因此count永远不会大于100且一定连续的先后出现"加20前"、"加20后"。现在将代码中的objA=objB取消注释再执行,得到的结果如第二张图所示,可以发现现在并没有线程同步了。第一句是"线程1执行中count加20前",如果线程同步那么接下来一定是"线程1执行中count加20后",可是结果却是线程2在执行,这说明锁失效了。可以看到表面原因是改变了objA指向的对象,从而导致锁失效。但是再往深处想,我很好奇加锁到底在对象上做了什么?虽然改变了objA指向的对象,但确实不是有一个对象已经被加锁了吗?

  在学习了同步块索引后,这些问题的答案也就出现了。我们知道每一个对象内存中都有一个同步块索引和类型指针,当在堆上创建一个对象时它的同步块索引会被设置为一个负数,这表明现在没有线程对它加锁。在CLR初始化时它会分配一个同步块数组,这个数组的大小是可以动态改变的且不在GC中。当使用lock对一个obj加锁时,将会让obj内存中的同步块索引与CLR中的同步块数组中的某一项关联起来,也就是让同步块索引可以索引到同步块数组中的这一项。当不再有需要对象同步的线程时,这个对象的同步块索引将会被再次重置为负数,同步块数组的这一项将可以继续与对象进行关联。而线程执行时如果发现要锁定的对象中的同步块索引已经指向了同步块数组,那么该线程将会进入等待队列。再来看这个例子,在对objA加锁后,objA的同步块索引已经关联同步块数组。执行objA=objB后,objA执行了堆中的objB堆空间,而objB的同步块索引是没有加锁的,因此线程2执行到lock(objA)时其实是对objB加了锁。所以线程1执行的过程中线程2也进入到了我们希望的同步区,结果中显示的是线程2进入同步区后一口气执行执行完毕。如果线程2只执行了一部分,此时线程1遇到了lock(objA),那么此时它会等待线程2执行完毕,因此结果输出中线程2都是一口气执行完毕。当然在多次运行程序过程中还有不同的结果,一是线程1执行完毕后线程2才进入;二是线程1执行到lock(objA)时正好线程2已经释放对objB的加锁,这样线程1就可以顺利的对objA(实际上是objB)加锁了,那么此时线程2就要等待线程1了。

class Program
{
static void Main(string[] args)
{
MyLock myLock=new MyLock();
Thread thread1 = new Thread(myLock.Thread1Func);
thread1.Start();
Thread thread2 = new Thread(myLock.Thread2Func);
thread2.Start();
}
} class MyLock
{
int count = ;
object objA = new object();
object objB = new object();
public void Thread1Func()
{
for (int i = ; i < ; i++)
StartEatPear("线程1");
}
public void Thread2Func()
{
for (int i = ; i < ; i++)
StartEatPear("线程2");
}
public int StartEatPear(string str)
{
lock (objA)
{
//在临界区中修改objA会导致锁失效
//objA = objB;
if (count <)
{
Console.WriteLine(str+"执行中,count加20前:" + count);
count = count + ;
Console.WriteLine(str + "执行中,count加20后:" + count);
}
else
{
Console.WriteLine(str + "执行中,count将return:" + count);
return count;
}
}
return count;
} }

C#基础之lock

C#基础之lock

3.还是lock(this)

  在写使用this发生死锁时,我写着好玩用了双this,代码如下。刚开始我很好奇线程执行到第二个this时会不会在这里等待,不过我觉得既然是同一个线程执行,内部应该会采取措施来让线程继续执行。最后结果是输出了2条语句,线程执行到第二个this时没有等待而是直接执行。查阅资料知道加锁的流程后这个问题才解决。我们知道lock其实是调用了静态方法Enter,这个方法首先会做判断,如果此时这个对象没有被锁住且没有线程在等待,那么这个对象将与同步块相关联以达到同步的效果。否则,也就是说这个对象已被锁住了,接下来有2种情况。一种情况是当前线程正好是将对象加锁的线程,那么此时会设置一个字段加1;另一种情况是当前线程不是将该对象加锁的线程,因此当前线程只能进入等待队列了。这样只要是同样的线程,即使遇到多个相同的lock语句将会接续执行而不会等待。

  在使用lock(this)时,由于已对这个对象加锁,因此其他线程无法再对这个对象加锁,现在加锁的本质也清楚了,可是这似乎只是在调用Monitor.Enter()方法时进行的。加了锁之后对于这个对象我们只知道无法再次加锁,但是到底还可不可以访问呢?如下面第二段代码,我创建的线程t1在无限循环的执行临界区,除非isgo被设置为false否则将不会有线程可以对myClass对象加锁。接着我在主线程调用NotLockMe方法,程序执行结果如代码中所示,主线程依旧可以访问myClass对象。这说明加锁并不是像我原先理解的那样会完全将这个对象锁住而其他地方无法访问这个对象,加锁仅仅只是让其他线程不可以对已加锁的对象再进行加锁,这个概念要理解清楚。顺其自然的对于写加锁代码我有一个想法,是不是可以设置一个bool值,如果在预期时间内没有出现想要的结果,我们可以通过在外部设置这个bool值让临界区退出执行。小弟新手一枚,代码写的还不够多,学的还不够深入,如有错误还请各位前辈指出!

 class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
Thread t = new Thread(myClass.LockFunc);
t.Start();
}
} class MyClass
{
public void LockFunc()
{
lock (this)
{
Console.WriteLine("我是第一个this");
lock (this)
{
Console.WriteLine("我是第二个this");
}
}
}
}
 class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
Thread t1 = new Thread(myClass.LockMe);
t1.Start();
Thread.Sleep(); //调用没有被lock的方法
myClass.NotLockMe(); //结果是:
// I am locked
// I am not locked
}
}
class MyClass
{
private bool isgo = true;
public void LockMe()
{
lock (this)
{
while (isgo)
{
Console.WriteLine("I am locked");
Thread.Sleep();
}
}
}
//所有线程都可以同时访问的方法
public void NotLockMe()
{
isgo = false;
Console.WriteLine("I am not locked");
}
}

声明:本文原创发表于博客园,作者为方小白,如有错误欢迎指出 。本文未经作者许可不许转载,否则视为侵权。