多重影分身——C#中多线程的使用二(争抢共享资源)
只要服务器承受得了,我们可以开任意个线程同时工作以提高效率,然而
两个线程争抢资源可能导致数据混乱。
例如:
public class MyFood { public static int Last { get; set; } public MyFood() { Last = 500; } public void EatFood() { int foods = Last; Thread.Sleep(1000); Last = foods - 1; Console.WriteLine(Last); } public void EatMuchFood() { int foods = Last; Thread.Sleep(3000); Last = foods - 10; Console.WriteLine(Last); } }
这里定义了个MyFood类,里面有个静态变量Last,存储剩余的食物。然后用构造函数假设初始有500的食物。
两个方法,分别是吃一个食物和吃10个食物,用Thread.Sleep()模拟吃东西需要花费时间。
OK,现在开始吃东西了:
MyFood e = new MyFood (); Thread th1 = new Thread(new ThreadStart(e.EatFood)); Thread th2 = new Thread(new ThreadStart(e.EatMuchFood)); th1.Start(); //499 th1.Join(); //这里等待第一个线程结束,再往后面走 th2.Start(); //489 Console.ReadKey();
没有任何问题,吃完第一个食物花费1秒钟,剩余499个。然后花费3秒钟吃10个食物,剩余489个。
假如我们不加th1.Join(), 那就是正常情况下的多线程,看看会发生什么:
MyFood e = new MyFood (); Thread th1 = new Thread(new ThreadStart(e.EatFood)); Thread th2 = new Thread(new ThreadStart(e.EatMuchFood)); th1.Start(); //499 th2.Start(); //490 Console.ReadKey();
可以看到两次输出分别是499和490,这里就有问题了。
EatFood方法,取出剩余Last后休息了1秒,然后再把-1后的数字赋值给Last。恰好,在这1秒期间,我们的th2进来调用EatMuchFood方法了,它也是先取总数,而这个总数却是EatFood方法还没有-1的总数,于是它取到的是500而不是499.
这就是问题发生的原因。
那么在对待总数这种共享资源上,同一时间我们希望只有一个线程来访问(这样才能确保数据正确),其中一个解决方案就是Lock
Lock
public class MyFood { private static readonly object lockHelper = new object(); public static int Last { get; set; } public MyFood() { Last = 500; } public void EatFood() { lock (lockHelper) { int foods = Last; Thread.Sleep(1000); Last = foods - 1; } Console.WriteLine(Last); } public void EatMuchFood() { lock (lockHelper) { int foods = Last; Thread.Sleep(3000); Last = foods - 10; } Console.WriteLine(Last); } }
这里添加了一个静态只读私有变量叫做lockHelper,然后修改了两个吃东西的方法,加了对lockHelper的锁定。
这样每次执行的时候,会判断lockHelper是否被锁定了,如果没有,就进入lock代码块把lockHelper锁定,并且执行其中的代码。出了lock代码块会自动释放锁。
这样就能保证在修改Last的时候,一次只有一个线程了。
本例中直接用lock(this)就行,this代表MyFood e 这个类的实例。
Monitor
Monitor是一个静态类,无法被实例化,有两个常用方法:
Monitor.Enter(object); 锁定传入对象保证只由当前线程操作。
Monitor.Exit(object); 释放被锁定的对象。
lock(obj) 的本质就是调用Monitor。
当我们锁定的是实例时,可能会导致多个实例间互斥不能实现:
public class MyFood { private static readonly object lockHelper = new object(); public static int Last { get; set; } public MyFood() { Last = 500; } public void EatFood() { Monitor.Enter(this);int foods = Last; Thread.Sleep(1000); Last = foods - 1;
Monitor.Exit(this); Console.WriteLine(Last); } } static void Main(string[] args) { MyFood e = new MyFood(); MyFood f = new MyFood(); Thread th1 = new Thread(new ThreadStart(e.EatFood)); Thread th2 = new Thread(new ThreadStart(f.EatFood)); th1.Start(); //499 th2.Start(); //499 Console.ReadKey(); }
因为我们锁定的是this,而this指向了不同的实例。
解决方法是锁定this的类型或者静态只读私有变量,即lock(typeof(this)) 或者 lock(lockHelper)
最好不要锁定string。
Monitor.Wait(obj) 和 Monitor.Pulse(obj)
这两个方法都是写在Monitor.Enter(obj)和Monitor.Pulse(obj)中间的。
Monitor.Wait(obj) ,用于在锁定对象之后,暂时释放锁,这样可以让其他线程也能访问。此时,它在等待其他Monitor.Pulse(obj)的通知,一旦收到,就继续往下执行。
Monitor.Pulse(obj), 用于告诉其他线程:我事忙完了,等我Monitor.Exit(obj)了,你们就接着弄吧
例子:
public class MyFood { private static readonly object lockHelper = new object(); public static int Last { get; set; } public MyFood() { Last = 500; } public void EatFood() { Monitor.Enter(lockHelper); //锁定lockHelper int foods = Last; Thread.Sleep(1000); Last = foods - 1; Console.WriteLine("第一个线程,剩余:" + Last); Monitor.Wait(lockHelper); //将该线程暂停,并释放锁允许其他线程访问
//等到了Monitor.Pulse(obj)和Monitor.Exit(obj)的信号,就继续往下执行 Monitor.Exit(lockHelper);
Console.WriteLine("第一个线程,等第二个执行完后剩余:" + Last); } public void EatMuchFood() { Monitor.Enter(lockHelper); //锁定对象 int foods = Last; Thread.Sleep(3000); Last = foods - 10; Monitor.Pulse(lockHelper); //通知其他线程,我忙完了,等我Monitor.Exit(obj)了,你们就继续吧 Monitor.Exit(lockHelper); //释放锁 Console.WriteLine("第二个线程,剩余:" + Last); } } class Program { static void Main(string[] args) { MyFood m = new MyFood(); Thread th1 = new Thread(new ThreadStart(m.EatFood)); Thread th2 = new Thread(new ThreadStart(m.EatMuchFood)); th1.Start(); th2.Start(); Console.ReadKey(); } }
这里输出结果为:
第一个线程,剩余:499
第二个线程,剩余:489
第一个线程,等第二个执行完后剩余:489
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构