C#多线程之线程同步
作者:.NET开发菜鸟 发布时间:2022-06-14 19:58:03
一、前言
我们先来看下面一个例子:
using System;
using System.Threading;
namespace ThreadSynchDemo
{
class Program
{
private static int Counter = 0;
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
Counter++;
Thread.Sleep(1);
}
});
t1.Start();
Thread t2 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
Counter++;
Thread.Sleep(1);
}
});
t2.Start();
Thread.Sleep(3000);
Console.WriteLine(Counter);
Console.ReadKey();
}
}
}
我们猜想一下程序的输出结果是多少?2000?我们运行程序看一下输出结果:
我们看到,程序最后输出的结果跟我们预测的完全不一样,这是什么原因呢?这就是由线程同步引起的问题。
线程同步问题:是解决多个线程同时操作一个资源的问题
。
在上面的例子中,t1和t2两个线程里面都是让变量Counter的值自增1,假设这时t1线程读取到Counter的值为200,可能t2线程执行非常快,t1线程读取Counter值的时候,t2线程已经把Counter的值改为了205,等t1线程执行完毕以后,Counter的值又被变为了201,这样就会出现线程同步的问题了。那么该如何解决这个问题呢?
二、解决线程同步问题
1、lock
解决线程同步问题最简单的是使用lock。lock可以解决多个线程同时操作一个资源引起的问题。lock是C#中的关键字,它要锁定一个资源,lock的特点是:同一时刻只能有一个线程进入lock的对象的范围,其它lock的线程都要等待。我们看下面优化后的代码:
using System;
using System.Threading;
namespace ThreadSynchDemo
{
class Program
{
private static int Counter = 0;
// 定义一个locker对象
private static Object locker = new Object();
static void Main(string[] args)
{
#region 存在线程同步问题
//Thread t1 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// Counter++;
// Thread.Sleep(1);
// }
//});
//t1.Start();
//Thread t2 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// Counter++;
// Thread.Sleep(1);
// }
//});
//t2.Start();
#endregion
#region 使用Lock解决线程同步问题
Thread t1 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
lock(locker)
{
Counter++;
}
Thread.Sleep(1);
}
});
t1.Start();
Thread t2 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
lock (locker)
{
Counter++;
}
Thread.Sleep(1);
}
});
t2.Start();
#endregion
Thread.Sleep(3000);
Console.WriteLine(Counter);
Console.ReadKey();
}
}
}
这时我们在运行程序,查看输出结果:
这时输出结果是正确的。
注意:lock只能锁住同一个对象,如果是不同的对象,还是会有线程同步的问题。lock锁定的对象必须是引用类型的对象。
我们在定义一个Object类型的对象,lock分别锁住两个对象,看看是什么结果:
using System;
using System.Threading;
namespace ThreadSynchDemo
{
class Program
{
private static int Counter = 0;
// 定义一个locker对象
private static Object locker = new Object();
// 定义locker2
private static Object locker2 = new Object();
static void Main(string[] args)
{
#region 存在线程同步问题
//Thread t1 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// Counter++;
// Thread.Sleep(1);
// }
//});
//t1.Start();
//Thread t2 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// Counter++;
// Thread.Sleep(1);
// }
//});
//t2.Start();
#endregion
#region 使用Lock解决线程同步问题
//Thread t1 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// lock(locker)
// {
// Counter++;
// }
// Thread.Sleep(1);
// }
//});
//t1.Start();
//Thread t2 = new Thread(() => {
// for (int i = 0; i < 1000; i++)
// {
// lock (locker)
// {
// Counter++;
// }
// Thread.Sleep(1);
// }
//});
//t2.Start();
#endregion
#region 使用lock锁住不同的对象也会有线程同步问题
Thread t1 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
lock (locker)
{
Counter++;
}
Thread.Sleep(1);
}
});
t1.Start();
Thread t2 = new Thread(() => {
for (int i = 0; i < 1000; i++)
{
lock (locker2)
{
Counter++;
}
Thread.Sleep(1);
}
});
t2.Start();
#endregion
Thread.Sleep(3000);
Console.WriteLine(Counter);
Console.ReadKey();
}
}
}
程序运行结果:
可以看到,这时还是会有线程同步的问题。虽然使用了lock,但是我们锁住的是不同的对象,这样也会有线程同步问题。lock必须锁住同一个对象才可以。
我们下面在来看一个多线程同步问题的例子:
using System;
using System.Threading;
namespace ThreadSynchDemo2
{
class Program
{
static int Money = 100;
/// <summary>
/// 定义一个取钱的方法
/// </summary>
/// <param name="name"></param>
static void QuQian(string name)
{
Console.WriteLine(name + "查看一下余额" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取钱");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余额" + Money);
Console.ReadKey();
}
}
}
我们看一下输出结果:
可以看到,最终的余额并不是80,这也是线程同步带来的问题,如何解决。解决思路就是使用同步的技术避免两个线程同时修改一个余额。
2、最大粒度——同步方法
在方法上面使用[MethodImpl(MethodImplOptions.Synchronized)],标记该方法是同步方法,这样一个方法只能同时被一个线程访问。我们在QuQian的方法上面标记,修改后的代码如下:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace ThreadSynchDemo2
{
class Program
{
static int Money = 100;
/// <summary>
/// 定义一个取钱的方法,在上面标记为同步方法
/// </summary>
/// <param name="name"></param>
[MethodImpl(MethodImplOptions.Synchronized)]
static void QuQian(string name)
{
Console.WriteLine(name + "查看一下余额" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取钱");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余额" + Money);
Console.ReadKey();
}
}
}
程序输出结果:
现在的方法就是“线程安全”的了。什么是“线程安全”呢?“线程安全”是指方法可以被多个线程随意调用,而不会出现混乱。如果出现了混乱,那么就是“线程不安全”的。“线程安全”的方法可以在多线程里面随意的使用。
3、对象互斥锁
对象互斥锁就是我们上面讲的lock。我们在用lock来修改上面QuQian的例子:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace ThreadSynchDemo2
{
class Program
{
static int Money = 100;
/// <summary>
/// 定义一个取钱的方法,在上面标记为同步方法
/// </summary>
/// <param name="name"></param>
//[MethodImpl(MethodImplOptions.Synchronized)]
//static void QuQian(string name)
//{
// Console.WriteLine(name + "查看一下余额" + Money);
// int yue = Money - 1;
// Console.WriteLine(name + "取钱");
// Money = yue;
// Console.WriteLine(name + "取完了,剩" + Money);
//}
private static object locker = new object();
static void QuQian(string name)
{
Console.WriteLine(name + "查看一下余额" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取钱");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
// 使用对象互斥锁
lock(locker)
{
QuQian("t1");
}
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
lock (locker)
{
QuQian("t2");
}
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余额" + Money);
Console.ReadKey();
}
}
}
程序输出结果:
可以看到,最终的输出结果还是80。
同一时刻只能有一个线程进入同一个对象的lock代码块。必须是同一个对象才能起到互斥的作用。lock后必须是引用类型,不一定是object,只要是对象就行。
锁对象选择很重要,选不对就起不到同步的作用;选不对还有可能会造成其他地方被锁,比如用字符串做锁(因为字符串缓冲池导致导致可能用的是其他地方正在使用的锁),所以不建议使用字符串做锁。下面的代码就是不允许的:
lock("locker")
两个方法如果都用一个对象做锁,那么访问A的时候就不能访问B,因此锁选择很重要。
4、Monitor
其实lock关键字就是对Monitor的简化调用,lock最终会被编译成Monitor,因此一般不直接使用Monitor类,看下面代码:
using System;
using System.Threading;
namespace MonitorDemo
{
class Program
{
static int Money = 100;
private static object locker = new object();
static void QuQian(string name)
{
// 等待没有人锁定locker对象,就锁定它,然后继续执行
Monitor.Enter(locker);
try
{
Console.WriteLine(name + "查看一下余额" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取钱");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
finally
{
// 释放locker对象的锁
Monitor.Exit(locker);
}
}
static void Main(string[] args)
{
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t1");
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
QuQian("t2");
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余额" + Money);
Console.ReadKey();
}
}
}
程序输出结果:
Monitor类里面还有TryEnter方法,如果Enter的时候有人在占用锁,它不会等待,而是会返回false。看下面的示例代码:
using System;
using System.Threading;
namespace MonitorDemo
{
class Program
{
static int Money = 100;
private static object locker = new object();
static void QuQian(string name)
{
// 等待没有人锁定locker对象,就锁定它,然后继续执行
Monitor.Enter(locker);
try
{
Console.WriteLine(name + "查看一下余额" + Money);
int yue = Money - 1;
Console.WriteLine(name + "取钱");
Money = yue;
Console.WriteLine(name + "取完了,剩" + Money);
}
finally
{
// 释放locker对象的锁
Monitor.Exit(locker);
}
}
static void F1(int i)
{
if (!Monitor.TryEnter(locker))
{
Console.WriteLine("有人在锁着呢");
return;
}
Console.WriteLine(i);
Monitor.Exit(locker);
}
static void Main(string[] args)
{
//Thread t1 = new Thread(() => {
// for (int i = 0; i < 10; i++)
// {
// QuQian("t1");
// }
//});
//Thread t2 = new Thread(() => {
// for (int i = 0; i < 10; i++)
// {
// QuQian("t2");
// }
//});
Thread t1 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
F1(i);
}
});
Thread t2 = new Thread(() => {
for (int i = 0; i < 10; i++)
{
F1(i);
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("余额" + Money);
Console.ReadKey();
}
}
}
程序输出结果:
来源:https://www.cnblogs.com/dotnet261010/p/12335538.html


猜你喜欢
- 先准备好一个新闻实体类package com.zb.fragmentbestpractice/** * title:表示新闻的实体类 * c
- 环境:SpringBoot 2.0.4.RELEASE需求:很多Controller方法,刚进来要先获取当前登录用户的信息,以便做后续的用户
- 前提概要:上一篇文章已经介绍过了RecyclerView的基本使用方法,原文如下:android RecyclerView布局真的只是那么简
- 1. Handler使用引出现在作为客户,有这样一个需求,当打开Activity界面时,开始倒计时,倒计时结束后跳转新的界面(思维活跃的朋友
- HashTable和HashMap区别第一,继承的父类不同。Hashtable继承自Dictionary类,而HashMap继承自Abstr
- using System; using System.IO; public class FileApp { &nbs
- 在android 中可以广泛看到的template<typename T> class Sp 句柄类实际上是android 为实
- idea统计代码行数可以用到插件:Statistic。步骤:File→Settings进入Plugins点击Marketplace搜索Sta
- Java处理JSON数据有三个比较流行的类库FastJSON、Gson和Jackson。JacksonJackson是由其社区进行维护,简单
- 使用方法 首先在Github或者Gitee上面新建一个仓库复制仓库的链接用idea在本地新建一个demo项目点击菜单栏的VCS,按
- 本文实例为大家分享了java实现2048小游戏的具体代码,供大家参考,具体内容如下效果图:游戏介绍:1.2048是一款益智类小游戏,刚开始随
- 背景知识Fluent Interface是一种通过连续的方法调用以完成特定逻辑处理的API实现方式,在代码中引入Fluent Interfa
- 看代码吧~package com.mtpc.admin.controller.exportSql;import ch.qos.logback
- 目录一、简介二、环境介绍三、主题1. ThemeData2. main.dart or MaterialApp四、全局配置1. Global
- 需求在配置类中,从application.properties中读取一个复杂list。如List<Person>或者初始化一个m
- 一、读取系统配置文件application.yaml1、application.yaml配置文件中增加一下测试配置testdata: &nb
- screenshot截图展示import step1. Add it in your root build.gradle at the en
- Java执行cmd命令//当前绝对路径System.out.println(IoUtil.read(Runtime.getRuntime()
- 水波纹效果已经不是什么稀罕的东西了,用过5.0新控件的小伙伴都知道这个效果,可是如果使用一个TextView或者Button或者其它普通控件
- C#中的DateTime.Compare()方法用于比较两个DateTime实例。它返回一个整数值,<0-如果date1早于date2