如何利用谷歌云做自己的网站,企业网站备案网地址,石家庄心雨网站建设,网站建设云主机云服务器多线程编程时#xff0c;如果涉及同时读写共享数据#xff0c;就要格外小心。如果共享数据是独占资源#xff0c;则要对共享数据的读写进行排它访问#xff0c;最简单的方式就是加锁。锁也不能随便用#xff0c;否则可能会造成死锁和活锁。本文将通过示例详细讲解死锁和活… 多线程编程时如果涉及同时读写共享数据就要格外小心。如果共享数据是独占资源则要对共享数据的读写进行排它访问最简单的方式就是加锁。锁也不能随便用否则可能会造成死锁和活锁。本文将通过示例详细讲解死锁和活锁是如何发生的以及如何避免它们。避免多线程同时读写共享数据在实际开发中难免会遇到多线程读写共享数据的需求。比如在某个业务处理时先获取共享数据比如是一个计数再利用共享数据进行某些计算和业务处理最后把共享数据修改为一个新的值。由于是多个线程同时操作某个线程取得共享数据后紧接着共享数据可能又被其它线程修改了那么这个线程取得的数据就是错误的旧数据。我们来看一个具体代码示例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 值应为 4count 值的变化过程应该是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 占用 resourceA 或 resourceB 的频率非常高比如外面再嵌套一个类似 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 代码块虽然都在运行但却没有真正地干活。我们把这种线程一直处于运行状态但其任务却一直无法进展的现象称为活锁。活锁和死锁的区别在于处于活锁的线程是运行状态而处于死锁的线程表现为等待活锁有可能自行解开死锁则不能。要避免活锁就要合理预估各线程对独占资源的占用时间并合理安排任务调用时间间隔要格外小心。现实中这种业务场景很少见。示例中这种复杂的资源占用逻辑很容易把人搞蒙而且极不容易维护。推荐的做法是使用信号量机制代替锁这是另外一个话题后面单独写文章讲。总结我们应该避免多线程同时读写共享数据避免的方式最简单的就是加锁把共享数据作为独占资源来进行排它使用。多个线程在一次任务中需要对多个独占资源加锁时就可能因相互循环等待而出现死锁。要避免死锁就至少得有一个线程作出让步。即在发现自己需要的资源得不到满足时就要主动释放已占有的资源以让别的线程可以顺利执行完成。大部分情况安排一个线程让步便可避免死锁但在复杂业务中可能会有多个线程互相让步的情况造成活锁。为了避免活锁需要合理安排线程任务调用的时间间隔而这会使得业务代码变得非常复杂。更好的做法是放弃使用锁而换成使用信号量机制来实现对资源的独占访问。-精致码农带你洞悉编程与架构↑长按图片识别二维码关注不要错过网海相遇的缘分