在开发多线程的应用程序时,我们会大量用到 lock (...) {} 块。如果 lock 的对象比较多,非常容易发生死锁。死锁的发生很难预料,而且一旦发生在界面线程上,界面就不再刷新响和应用户输入;如果发生在后台线程,后台线程也就阻塞不工作了,死锁必然会导致应用程序不可用。在.NET里发生死锁的原因是什么?
以 C# 为例,通常 lock 语句是被转化为对一个资源的无限长时间的等待,所以一旦资源被占用而又永不释放,那么必然死锁。
那么如何规避的危害呢?应用程序应该避免 lock(obj) 块,推荐使用 Monitor.TryEnter(obj, millisecondsTimeout) 代替,二者的第一个参数意义相同,而后者还可以设置等待超时时间,一旦在限定的时间内无法获得锁,那么 TryEnter 就会返回 false。这样就不会造成死锁,无法获得资源,业务程序可以采取重试或抛异常的方式进行善后处理。
Monitor.TryEnter 和 Monitor.Exit 必须成对出现,为了简化代码,可以用一个实现IDisposeable的类来封装这个过程:
/// <summary>
/// 会自动释放的锁,可设置等待超时
/// </summary>
public class Lock : IDisposable
{
/// <summary>
/// 默认超时设置
/// </summary>
public static int DefaultMillisecondsTimeout = 15000; // 15S
private object _obj;
/// <summary>
/// 构造
/// </summary>
/// <param name="obj">想要锁住的对象</param>
public Lock(object obj)
{
TryGet(obj, DefaultMillisecondsTimeout, true);
}
/// <summary>
/// 构造
/// </summary>
/// <param name="obj">想要锁住的对象</param>
/// <param name="millisecondsTimeout">超时设置</param>
public Lock(object obj, int millisecondsTimeout)
{
TryGet(obj, millisecondsTimeout, true);
}
/// <summary>
/// 构造
/// </summary>
/// <param name="obj">想要锁住的对象</param>
/// <param name="millisecondsTimeout">超时设置</param>
/// <param name="throwTimeoutException">是否抛出超时异常</param>
public Lock(object obj, int millisecondsTimeout, bool throwTimeoutException)
{
TryGet(obj, millisecondsTimeout, throwTimeoutException);
}
private void TryGet(object obj, int millisecondsTimeout, bool throwTimeoutException)
{
if (Monitor.TryEnter(obj, millisecondsTimeout))
{
_obj = obj;
}
else
{
if (throwTimeoutException)
{
throw new TimeoutException();
}
}
}
/// <summary>
/// 销毁,并释放锁
/// </summary>
public void Dispose()
{
if (_obj != null)
{
Monitor.Exit(_obj);
}
}
/// <summary>
/// 获取在获取锁时是否发生等待超时
/// </summary>
public bool IsTimeout
{
get
{
return _obj == null;
}
}
}
调用例子:
using (Lock l = new Lock(obj, 10000))
{
....
}
这样在代码离开 using 块后,会自动执行 Monitor.Exit释放锁。
是不是很棒? :)