c# 死锁和活锁的发生及避免

 更新时间:2020年12月8日 11:34  点击:2437

避免多线程同时读写共享数据

在实际开发中,难免会遇到多线程读写共享数据的需求。比如在某个业务处理时,先获取共享数据(比如是一个计数),再利用共享数据进行某些计算和业务处理,最后把共享数据修改为一个新的值。由于是多个线程同时操作,某个线程取得共享数据后,紧接着共享数据可能又被其它线程修改了,那么这个线程取得的数据就是错误的旧数据。我们来看一个具体代码示例:

static int count { get; set; }

static void Main(string[] args)
{
 for (int i = 1; i <= 2; i++)
 {
  var thread = new Thread(ThreadMethod);
  thread.Start(i);
  Thread.Sleep(500);
 }
}

static void ThreadMethod(object threadNo)
{
 while (true)
 {
  var temp = count;
  Console.WriteLine("线程 " + threadNo + " 读取计数");
  Thread.Sleep(1000); // 模拟耗时工作
  count = temp + 1;
  Console.WriteLine("线程 " + threadNo + " 已将计数增加至: " + count);
  Thread.Sleep(1000);
 }
}

示例中开启了两个独立的线程开始工作并计数,假使当 ThreadMethod 被执行第 4 次的时候(即此刻 count 值应为 4),count 值的变化过程应该是:1、2、3、4,而实际运行时计数的的变化却是:1、1、2、2...。也就是说,除了第一次,后面每次,两个线程读取到的计数都是旧的错误数据,这个错误数据我们把它叫作脏数据。

因此,对共享数据进行读写时,应视其为独占资源,进行排它访问,避免同时读写。在一个线程对其进行读写时,其它线程必须等待。避免同时读写共享数据最简单的方法就是加锁。

修改一下示例,对 count 加锁:

static int count { get; set; }
static readonly object key = new object();

static void Main(string[] args)
{
 ...
}

static void ThreadMethod(object threadNumber)
{
 while (true)
 {
  lock(key)
  {
   var temp = count;
   ...
    count = temp + 1;
   ...
  }
  Thread.Sleep(1000);
 }
}

这样就保证了同时只能有一个线程对共享数据进行读写,避免出现脏数据。

死锁的发生

上面为了解决多线程同时读写共享数据问题,引入了锁。但如果同一个线程需要在一个任务内占用多个独占资源,这又会带来新的问题:死锁。简单来说,当线程在请求独占资源得不到满足而等待时,又不释放已占有资源,就会出现死锁。死锁就是多个线程同时彼此循环等待,都等着另一方释放其占有的资源给自己用,你等我,我待你,你我永远都处在彼此等待的状态,陷入僵局。下面用示例演示死锁是如何发生的:

class Program
{
 static void Main(string[] args)
 {
  var workers = new Workers();
  workers.StartThreads();
  var output = workers.GetResult();
  Console.WriteLine(output);
 }
}

class Workers
{
 Thread thread1, thread2;

 object resourceA = new object();
 object resourceB = new object();

 string output;

 public void StartThreads()
 {
  thread1 = new Thread(Thread1DoWork);
  thread2 = new Thread(Thread2DoWork);
  thread1.Start();
  thread2.Start();
 }

 public string GetResult()
 {
  thread1.Join();
  thread2.Join();
  return output;
 }

 public void Thread1DoWork()
 {
  lock (resourceA)
  {
   Thread.Sleep(100);
   lock (resourceB)
   {
    output += "T1#";
   }
  }
 }

 public void Thread2DoWork()
 {
  lock (resourceB)
  {
   Thread.Sleep(100);
   lock (resourceA)
   {
    output += "T2#";
   }
  }
 }
}

示例运行后永远没有输出结果,发生了死锁。线程 1 工作时锁定了资源 A,期间需要锁定使用资源 B;但此时资源 B 被线程 2 独占,恰巧资线程 2 此时又在待资源 A 被释放;而资源 A 又被线程 1 占用......,如此,双方陷入了永远的循环等待中。

死锁的避免

针对以上出现死锁的情况,要避免死锁,可以使用 Monitor.TryEnter(obj, timeout) 方法来检查某个对象是否被占用。这个方法尝试获取指定对象的独占权限,如果 timeout 时间内依然不能获得该对象的访问权,则主动“屈服”,调用 Thread.Yield() 方法把该线程已占用的其它资源交还给 CUP,这样其它等待该资源的线程就可以继续执行了。即,线程在请求独占资源得不到满足时,主动作出让步,避免造成死锁。

把上面示例代码的 Workers 类的 Thread1DoWork 方法使用 Monitor.TryEnter 修改一下:

// ...(省略相同代码)
public void Thread1DoWork()
{
 bool mustDoWork = true;
 while (mustDoWork)
 {
  lock (resourceA)
  {
   Thread.Sleep(100);
   if (Monitor.TryEnter(resourceB, 0))
   {
    output += "T1#";
    mustDoWork = false;
    Monitor.Exit(resourceB);
   }
  }
  if (mustDoWork) Thread.Yield();
 }
}

public void Thread2DoWork()
{
 lock (resourceB)
 {
  Thread.Sleep(100);
  lock (resourceA)
  {
   output += "T2#";
  }
 }
}

再次运行示例,程序正常输出 T2#T1# 并正常结束,解决了死锁问题。

注意,这个解决方法依赖于线程 2 对其所需的独占资源的固执占有和线程 1 愿意“屈服”作出让步,让线程 2 总是优先执行。同时注意,线程 1 在锁定 resourceA 后,由于争夺不到 resourceB,作出了让步,把已占有的 resourceA 释放掉后,就必须等线程 2 使用完 resourceA 重新锁定 resourceA 再重做工作。

正因为线程 2 总是优先,所以,如果线程 2 占用 resourceAresourceB 的频率非常高(比如外面再嵌套一个类似 while(true) 的循环 ),那么就可能导致线程 1 一直无法获得所需要的资源,这种现象叫线程饥饿,是由高优先级线程吞噬低优先级线程 CPU 执行时间的原因造成的。线程饥饿除了这种的原因,还有可能是线程在等待一个本身也处于永久等待完成的任务。

我们可以继续开个脑洞,上面示例中,如果线程 2 也愿意让步,会出现什么情况呢?

活锁的发生和避免

我们把上面示例改造一下,使线程 2 也愿意让步:

public void Thread1DoWork()
{
 bool mustDoWork = true;
 Thread.Sleep(100);
 while (mustDoWork)
 {
  lock (resourceA)
  {
   Console.WriteLine("T1 重做");
   Thread.Sleep(1000);
   if (Monitor.TryEnter(resourceB, 0))
   {
    output += "T1#";
    mustDoWork = false;
    Monitor.Exit(resourceB);
   }
  }
  if (mustDoWork) Thread.Yield();
 }
}

public void Thread2DoWork()
{
 bool mustDoWork = true;
 Thread.Sleep(100);
 while (mustDoWork)
 {
  lock (resourceB)
  {
   Console.WriteLine("T2 重做");
   Thread.Sleep(1100);
   if (Monitor.TryEnter(resourceA, 0))
   {
    output += "T2#";
    mustDoWork = false;
    Monitor.Exit(resourceB);
   }
  }
  if (mustDoWork) Thread.Yield();
 }
}

注意,为了使我要演示的效果更明显,我把两个线程的 Thread.Sleep 时间拉开了一点点。运行后的效果如下:

通过观察运行效果,我们发现线程 1 和线程 2 一直在相互让步,然后不断重新开始。两个线程都无法进入 Monitor.TryEnter 代码块,虽然都在运行,但却没有真正地干活。

我们把这种线程一直处于运行状态但其任务却一直无法进展的现象称为活锁。活锁和死锁的区别在于,处于活锁的线程是运行状态,而处于死锁的线程表现为等待;活锁有可能自行解开,死锁则不能。

要避免活锁,就要合理预估各线程对独占资源的占用时间,并合理安排任务调用时间间隔,要格外小心。现实中,这种业务场景很少见。示例中这种复杂的资源占用逻辑,很容易把人搞蒙,而且极不容易维护。推荐的做法是使用信号量机制代替锁,这是另外一个话题,后面单独写文章讲。

总结

我们应该避免多线程同时读写共享数据,避免的方式,最简单的就是加锁,把共享数据作为独占资源来进行排它使用。

多个线程在一次任务中需要对多个独占资源加锁时,就可能因相互循环等待而出现死锁。要避免死锁,就至少得有一个线程作出让步。即,在发现自己需要的资源得不到满足时,就要主动释放已占有的资源,以让别的线程可以顺利执行完成。

大部分情况安排一个线程让步便可避免死锁,但在复杂业务中可能会有多个线程互相让步的情况造成活锁。为了避免活锁,需要合理安排线程任务调用的时间间隔,而这会使得业务代码变得非常复杂。更好的做法是放弃使用锁,而换成使用信号量机制来实现对资源的独占访问。

作者:精致码农

出处:http://cnblogs.com/willick

以上就是c# 死锁和活锁的发生及避免的详细内容,更多关于c# 死锁和活锁的资料请关注猪先飞其它相关文章!

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#中list用法实例

    这篇文章主要介绍了C#中list用法,结合实例形式分析了C#中list排序、运算、转换等常见操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25