线程同步技术
参考:https://getpocket.com/a/read/929454653
一、先来看一个争用条件
SharedState类用于保存线程之间的恭喜那个数据,有一个成员State,不同的线程可以共享State。
public class SharedState { public int State { get; set; } }
Job类包含DoTheJob()方法,该方法时新任务的入口点,通过代码实现,将sharedState类的State递增50000次,sharedState在这个类的构造函数中初始化(this.sharedState)
public class Job { SharedState sharedState; public Job(SharedState sharedState) { this.sharedState = sharedState; } private object syncObj = new object(); public void DoTheJob() { for (int i = 0; i < 50000; i++) { sharedState.State += 1; } } }
在Main方法中,创建一个SharedState对象,并把它传递给20个Task对象的构造函数,在启动所有的任务后,Main()方法进入另一个循环,等待20个任务都执行完毕,把共享状态的值写入控制台中,因为执行了50000次循环,有20个任务,因此预期结果为1 000 000,但是事实并非如此。
class Program { static void Main(string[] args) { 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()); } for (int i = 0; i < numTasks; i++) { tasks[i].Wait(); } Console.WriteLine("summarized {0}", state.State); Console.ReadKey(); } }
执行三次的结果为:
summarized 381758
summarized 531860
summarized 316794
不同的机器执行结果可能不同,但是说明一个问题,在多线程并行执行的环境下,恭喜那个的数据有可能被其他线程修改而导致出现非预期结果。
比如共有两个线程t1和t2,这两个线程分别取state的值并给其加1,state初值为 1,当t1和t2从state取值的时候取出来的都是1,各自执行加1操作后将结果返回,那么我们得到的结果是2,而不是3,因为一个线程取出来的state值并不是另一个线程的执行结果,因此造成了结果错误。
二、C#用于多个线程同步的技术
如果需要在线程中共享数据,就需要使用同步技术,C#可以用于多线程同步的技术有:
- lock 语句
- Interlocked 类
- Monitor 类
- SpinLock 结构
- WaitHandle 类
- Mutex 类
- Semaphore 类
- Event 类
- Barrier 类
- ReaderWriterLockSlim
1、lock语句
用lock语句定义的对象表示,要等待指定对象的锁定,只能传递引用类型。锁定类型只是锁定了一个副本,这其实没什么意义,如果对值类型使用了lock语句,C#编译器会发出一个错误。进行了锁定后--只锁定了一个线程,就可以运行lock语句块,在lock语句块的最后,对象的锁定被解锁,另一个等待锁定的线程就能获得该锁定块了。
我们尝试使用lock(this)和lock(obj) 来上锁。
将DoTheJob进行以下改造:
public void DoTheJob() { lock(this) { for (int i = 0; i < 50000; i++) { sharedState.State += 1; } } }
结果还是没有达到我们预期的100 000,这里的lock支队使用相同实例的线程起作用,tasks
[]中每个人物都调用不同的实例,所以它们都能同时使用DoTheJob方法。
将DoTheJob进行以下改造:
private object syncObj = new object(); public void DoTheJob() { lock(syncObj) { for (int i = 0; i < 50000; i++) { sharedState.State += 1; } } }
运行结果也不正确。lock(syncObj)只会导致 DoTheJob() 不能被其他线程访问,但实例的其他成员依然可以被访问。
以下的例子可以更清楚的说明这一点。
lock(this)
public class LockThis { private bool deadLock = true; public void DeadLocked() { lock(this) { while(deadLock) { Console.WriteLine("OMG! I am locked!"); Thread.Sleep(1000); } Console.WriteLine("DeadLocked() End"); } } public void DontLockMe() { deadLock = false; } public static void LockThisMethod() { LockThis lockThis = new LockThis(); Task.Factory.StartNew(lockThis.DeadLocked); Thread.Sleep(5000); lock(lockThis) { lockThis.DontLockMe(); } } }
在Main()中调用LockThis.LockThisMethod即可调用此方法。
运行结果:
在LockThisMethod方法中,开始任务lockThis.DeadLocke
Task.Factory.StartNew(lockThis.DeadLocked);
企图任务开始5s以后通过LockThis.DontLockMe来解除死锁,但是并没有成功,LockThis.DeadLocked一直运行不停止。
因为死锁中lock(this)锁定了整个实例,导致外层想用同步方式访问此实例时,连非同步方法DontLockMe()也不能调用。
lock(syncObj)
public class LockObject { private bool deadLock = true; private object syncObj = new Object(); public void DeadLocked() { lock(syncObj) { while(deadLock) { Console.WriteLine("OMG! I am locked!"); Thread.Sleep(1000); } Console.WriteLine("DeadLocked() End."); } } public void DontLockMe() { deadLock = false; } public static void LockObjectMethod() { LockObject lockObject = new LockObject(); Task.Factory.StartNew(lockObject.DeadLocked); Thread.Sleep(5000); lock(lockObject) { lockObject.DontLockMe(); } } }
在Main()中调用LockObject.LockObjectMethod即可调用此方法。
运行结果:
在LockObjectMethod方法中,开始任务lockObject.DeadLocke
Task.Factory.StartNew(lockObject.DeadLocked);
企图任务开始5s以后通过LockThis.DontLockMe来解除死锁,成功了
因为死锁中lock(syncObj)只锁定了DeadLocked()方法,当外层也用同步方式访问该实例时,非同步方法DontLockMe可以被调用。
总结:因为类的对象也可以用于外部的同步访问( 上面的 lock(lockThis) 和 lock(lockObject) 就模拟了这种访问 ),而且我们不能在类自身中控制这种访问,所以应该尽量使用 lock(obj) 的方式,可以比较精确的控制需要同步的范围。
lock(this)会锁定整个实例
lock(obj)只锁定范围内的代码。
lock(typeod(StaticClass))锁定静态成员
再来看一看,修改SharedState类可行不可行.
public class SharedState { private object syncObj = new object(); private int state=0; public int State { get { lock (syncObj){return _state; } } set { lock (syncObj){state = value;}} } }
直接对共享状态控制同步,但是预期结果还是没有出来。
误区:对同步的理解错了,读和写之间,syncObj并没有被锁定,依然有线程可以在这个期间获得值。
解决问题的办法:
1、将lock放到合适的地方,并采用合适的lock对象
public void DoTheJob() { for (int i = 0; i < 50000; i++) { lock(sharedState) { sharedState.State += 1; } } }
2、修改SharedState类的设计,作为一个原子操作提供递增方式
public class SharedState { private int state = 0; private object syncRoot = new object(); public int State { get { return state; } } public int incrementState() { lock(syncRoot) { return ++state; } } }
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。