参考: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;
}
}
}