继续上一篇c#线程初探(一),这里介绍线程同步的常见概念和注意事项。3、同步使用线程的一个重要方面是同步访问多个线程访问的任何变量。(1)、“同步”:所谓同步,是指在某一时刻只有一个线程可以访问变量。同步问题只会发生在下述场景:至少有一个线程要写入一个变量,而与此同时,其他线程正在读取或者写入同一个变量。这和大学课程《操作系统》教的线程同步是一个道理。c#为同步访问变量提供了一种非常简单的方式,即使用关键字lock。Code is cheap.举例来说,在“保证一个类仅有一个实例:单例模式”就已经用了这种方式:
lock语句把变量放在括号中,以包装对象,被称为独占锁或者排它锁。当执行带有lock关键字的复合语句时,独占锁会保留下来。当变量被包装在独占锁中时,其他线程就不能访问该变量。如果在上面代码中使用独占锁,在执行复合语句时,这个线程就会失去其“时间片”。如果下一个获得时间片的线程试图访问变量syncRoot,就会被拒绝。Windows会让其他线程处于睡眠状态,直到解除了独占锁为止。PS:上述代码中,我们lock的是一个object对象,对于string这个特殊类型的对象,我们一定要慎用。比如下面的代码:
本来,LockTestOne和LockTestOne2类中各自的方法一点关系没有,但在这里,DoSomething2方法执行时,若另一个线程在执行DoSomething方法,那它得等待!两个变量(strSync和strSync2)都是在编译时赋值为""(空字符串),.NET会让这两个变量指向同一个拘留池的对象(strSync和strSync2在拘留池中。)。于是,两个lock看似lock两个毫不相干的对象,但其实是在lock同一个对象。所以,准确的讲,我们不要lock拘留池中的字符串。(2)同步引起的问题:死锁(dead lock)和竞态条件(race condition)线程同步非常重要,但是要慎用,因为这会降低性能。原因有两个,首先,在对象上放置和解开锁会带来某些系统开销。第二个原因更重要,线程使用的越多,等待释放对象的线程就越多。如果一个线程在对象上放置了一个锁,需要访问该对象的其他线程就只能暂停执行,直到该锁被解开才能继续执行。因此,在lock块内编写的代码越少越好,以免出现线程同步错误。lock语句某种意义上就是临时禁用应用程序的多线程功能,也就删除了多线程的各种优势。使用线程同步有潜在的危险,主要表现就是死锁和竞态条件。a、死锁:死锁是一个错误,在两个线程都需要访问该被互锁的资源时发生。比如下面的代码:
在上面代码中,根据线程1和线程2遇到不同语句的时间,可能会出现下述情况:线程1在a上加锁,同时线程2在b上加锁。不久,线程1开始遇到lock(b)语句,立即进入睡眠状态,等待b上的锁被释放。之后,第二个线程遇到lock(a)语句,也立即进入睡眠状态,等待a上的锁被释放。但是,a上的锁永远不会解开,因为线程1拥有这个锁,目前正处于睡眠状态,在b上的锁被解开前是不会“醒过来”的。而在线程2被叫醒之前,b上的锁不会解开,这样线程1和线程2就互相等待对方释放资源(最后就耗上了),这样就形成一个死锁。解决死锁的方法:让这两个线程以相同的顺序在对象上声明加锁。正确的代码如下:
b、竞态条件竞态条件比死锁更微妙。它很少中断进程的执行,但可能导致数据损坏。当几个线程视图访问同一个数据,但没有考虑其他线程的执行情况时,就会发生竞态条件。注:关于竞态条件不是一两句话就可以说的清楚的,读者可以参考相关资料,大学教材《操作系统》有详细讲解,这里不在赘述了。