任务同步
要避免同步问题,最好不要在线程之间共享数据。如果需要共享数据,就必须使用同步技术确保一次只有一个线程访问和改变共享状态。
如果不注意同步,就会出现竞争条件和死锁。
线程问题
竞争条件(Race Condition)
如果两个或多个线程访问相同对象,并且对共享状态没有同步,就会出现竞争条件(Race Condition)。
Race Condition是指两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
要避免问题,可以锁定共享的对象。或者将共享对象设置为线程安全的对象。
public static class StateObject
{
private int _state = 5;
private object _sync = new object();
public void ChangeState(int loop)
{
lock(_sync)
{
if(_state == 5)
{
_state++;
Trace.Assert(_state == 6, $"Race condition occurred after {loop} loops");
}
_state = 5;
}
}
}
死锁(Dead Lock)
过多的锁定也会有麻烦。在死锁中,至少有两个线程被挂起,并等待对方解除锁定。线程将无限等待下去。
死锁问题并不是总是很明显,在较大的应用程序中,锁定可能隐藏在方法的深处。
要避免这个问题,从一开始就要设计好锁定顺序,也可以为锁定定义超时时间。
lock语句和线程安全
C#为多线程同步提供了lock
关键字。lock语句是设置锁定和解除锁定的一种简单方式。
//锁定对象
Object obj = new Object;
lock(obj)
{
}
//锁定静态成员
lock(typeof(StaticClass))
{
}
用lock语句定义的对象表示,要等待指定对象的锁定,只能传递引用类型。锁定值类型是无意义的。
进行锁定后,就可以运行lock语句块,在lock语句块的最后,对象的锁定被解除,另一个等待锁定的线程就可以获得该锁定块了。
Interlocked类
Interlocked类用于使变量的简单语句原子化。
i++不是线程安全的,它的操作包括从内存中获取一个值,递增1,再将它存储回内存。这些操作都可能会被线程调度器打断。
Interlocked类提供了以线程安全的方式递增、递减、交换和读取值的方法。
使用Interlocked类会快很多,但是它只能用于简单的同步问题。
//1:
lock(this)
{
if(someState == null) someState = newState;
}
//功能相同但比较快:
Interlocked.CompareExchange<SomeState>(ref someState, newState, null);
//2:
public int State()
{
lock(this)
{
return ++_state;
}
}
//较快的方法啊:
public int State()
{
return Interlocked.Increment(ref _state);
}
Monitor类
lock语句由C#编译器解析为使用Monitor
类。
//lock
lock(obj)
{
//synchronized region for obj
}
//Monitor
Monitor.Enter(obj);
try
{
//synchronized region for obj
}
finally
{
Monitor.Exit(obj);
}
与lock相比,Monitor的主要优点是可以添加一个等待被锁定的超时值。
bool lockToken = false;
Monitor.TryEnter(obj, 500, ref lockToken);
if(lockToken)
{
try
{
//acquired the lock
//synchronized region for obj
}
finally
{
Monitor.Exit();
}
}
else
{
//didnt get the lock, do something else
}
//TryEnter方法给它传递一个超时值,指定等待被锁定的最长时间。如果obj被锁定,TryEnter方法就把bool类型的引用参数设置为true,并同步地访问由对象obj锁定的状态。
SpinLock结构
如果Monitor的系统开销由于垃圾回收而过高,就可以使用SpinLock结构。如果有大量锁定,且锁定的时间非常短,SpinLock结构就很有用。
SpinLock结构的用法类似Monitor类。
WaitHandle基类
WaitHandle抽象基类用于等待一个信号的设置。可以等待不同的信号,因为WaitHandle是一个基类。
使用WaitHandle基类可以等待一个信号的出现(WaitOne()
方法)、等待必须发出信号的多个对象(WaitAll()
方法)、或者等待多个对象中的一个(WaitAny()
方法)。
因为Mutex
、EventWaitHandle
、Semaphore
类都派生自WaitHandle基类。所以可以在等待时使用它们。
- 委托异步执行示例
WaitHandle也由简单的异步委托使用:
System.Action foo = () =>
{
for (int i = 0; i < 100; i++) Console.WriteLine("out1:" + i);
}
var result = foo.BeginInvoke(null, null);
for (int i = 0; i < 100; i++) Console.WriteLine("out2:" + i);
foo.EndInvoke(result);//未完成的话就阻塞调用线程直到完成。
Console.WriteLine("IsComplete:" + result.IsCompleted); //打印结果为True
- 关于signaling
让某线程一直处于等待状态,直到接收到其他线程发来的通知。这称为signaling。实现:用WaitOne()
方法阻塞当前线程,直到另一个线程用Set()
方法来开启信号。(调用完Set()
后信号会处于打开状态,需要Reset()
关闭。)
- 注意事项
?:WPF\Winform\UWP的控件的
BeginInvoke/Invoke
方法跟委托的BeginInvoke/Invoke
方法不一样。
?:WPF\Winform\UWP的控件的BeginInvoke/Invoke
方法可以实现UI线程外操作UI控件。
Mutex类
Mutex(mutual exclusion, 互斥)是提供跨多个进程同步访问的一个类。它非常类似Monitor类。只有一个线程能获得互斥锁定,访问受互斥保护的同步代码区域。
bool createdMutex = false;
var mutex = new Mutex(false, "xxx", out createdMutex);
if(mutext.WaitOne())//由于Mutex派生自WaitHandle类,因此可以用WaitOne()获得互斥锁定
{
try
{
//synchronized region
}
finally
{
mutext.ReleaseMutex();
}
}
else
{
//some problem happened in waiting
}
Semaphore类
信号量(Semaphore)非常类似互斥,区别是,信号量可以同时由多个线程使用。
信号量是一种计数的互斥锁定。使用信号量,可以定义允许同时访问受信号量保护的资源的线程个数。如果需要限制可以访问可用资源的线程数,信号量就很有用。
static void Main()
{
int samaphoreCount = 3;
var semaphore = new SemaphoreSlim(semaphoreCount, semaphoreCount);
var tasks = new Task[6];
for(int i = 0; i < tasks.Length; i++)
{
task[i] = Task.Run(() => Test(semaphore));
}
Task.WaitAll(tasks);
Console.WriteLine("finished...")
}
static void Test(SemaphoreSlim semaphore)
{
bool isComplete = false;
while(!isComplete)
{
if(semaphore.Wait(600))
{
try
{
Task.Delay(2000).Wait();
}
finally
{
semaphore.Release();
isComplete = true;
}
}
else
{
WriteLine("Time out");
}
}
}
其他的线程同步相关类
Events
类
这也是一个系统范围内的资源同步方法。为了从托管代码中使用系统事件(本机事件),dotnet framework提供了ManualResetEvent
、AutoSetEvent
两个类。
这两个类和C#的
event
关键字没有任何关系。
Barrier
类
对于同步,Barrier类非常适合用于其中多个工作有多个任务分支且以后又需要合并工作的情况。
Timer
类
使用计时器,可以重复调用方法。
(END)