深入分析C# 线程同步
作者:JoeSnail 发布时间:2023-05-09 00:49:44
目录
线程同步的几种方法:
阻塞
加锁(lock)
Monitors
互斥锁(Mutex)
信号和句柄
Interlocked
ReaderWriterLock
上一篇介绍了如何开启线程,线程间相互传递参数,及线程中本地变量和全局共享变量区别。
本篇主要说明线程同步。
如果有多个线程同时访问共享数据的时候,就必须要用线程同步,防止共享数据被破坏。如果多个线程不会同时访问共享数据,可以不用线程同步。
线程同步也会有一些问题存在:
性能损耗。获取,释放锁,线程上下文建切换都是耗性能的。
同步会使线程排队等待执行。
线程同步的几种方法:
阻塞
当线程调用Sleep,Join,EndInvoke,线程就处于阻塞状态(Sleep使调用线程阻塞,Join、EndInvoke使另外一个线程阻塞),会立即从cpu退出。(阻塞状态的线程不消耗cpu)
当线程在阻塞和非阻塞状态间切换时会消耗几毫秒时间。
//Join
static void Main()
{
Thread t = new Thread (Go);
Console.WriteLine ("Main方法已经运行....");
t.Start();
t.Join();//阻塞Main方法
Console.WriteLine ("Main方法解除阻塞,继续运行...");
}
static void Go()
{
Console.WriteLine ("在t线程上运行Go方法...");
}
//Sleep
static void Main()
{
Console.WriteLine ("Main方法已经运行....");
Thread.CurrentThread.Sleep(3000);//阻塞当前线程
Console.WriteLine ("Main方法解除阻塞,继续运行...");
}
//Task
static void Main()
{
Task Task1=Task.Run(() => {
Console.WriteLine("task方法执行...");
Thread.Sleep(1000);
});
Console.WriteLine(Task1.IsCompleted);
Task1.Wait();//阻塞主线程 ,等该Task1完成
Console.WriteLine(Task1.IsCompleted);
}
加锁(lock)
加锁使多个线程同一时间只有一个线程可以调用该方法,其他线程被阻塞。
同步对象的选择:
使用引用类型,值类型加锁时会装箱,产生一个新的对象。
使用private修饰,使用public时易产生死锁。(使用lock(this),lock(typeof(实例))时,该类也应该是private)。
string不能作为锁对象。
不能在lock中使用
await
关键字
锁是否必须是静态类型?
如果被锁定的方法是静态的,那么这个锁必须是静态类型。这样就是在全局锁定了该方法,不管该类有多少个实例,都要排队执行。
如果被锁定的方法不是静态的,那么不能使用静态类型的锁,因为被锁定的方法是属于实例的,只要该实例调用锁定方法不产生损坏就可以,不同实例间是不需要锁的。这个锁只锁该实例的方法,而不是锁所有实例的方法.*
class ThreadSafe
{
private static object _locker = new object();
void Go()
{
lock (_locker)
{
......//共享数据的操作 (Static Method),使用静态锁确保所有实例排队执行
}
}
private object _locker2=new object();
void GoTo()
{
lock(_locker2)
//共享数据的操作,非静态方法,是用非静态锁,确保同一个实例的方法调用者排队执行
}
}
同步对象可以兼作它lock的对象
如:
class ThreadSafe
{
private List <string> _list = new List <string>();
void Test()
{
lock (_list)
{
_list.Add ("Item 1");
}
}
}
Monitors
lock
其实是Monitors
的简洁写法。
lock (x)
{
DoSomething();
}
两者其实是一样的。
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
互斥锁(Mutex)
互斥锁是一个互斥的同步对象,同一时间有且仅有一个线程可以获取它。可以实现进程级别上线程的同步。
class Program
{
//实例化一个互斥锁
public static Mutex mutex = new Mutex();
static void Main(string[] args)
{
for (int i = 0; i < 3; i++)
{
//在不同的线程中调用受互斥锁保护的方法
Thread test = new Thread(MutexMethod);
test.Start();
}
Console.Read();
}
public static void MutexMethod()
{
Console.WriteLine("{0} 请求获取互斥锁", Thread.CurrentThread.Name);
mut.WaitOne();
Console.WriteLine("{0} 已获取到互斥锁", Thread.CurrentThread.Name);
Thread.Sleep(1000);
Console.WriteLine("{0} 准备释放互斥锁", Thread.CurrentThread.Name);
// 释放互斥锁
mut.ReleaseMutex();
Console.WriteLine("{0} 已经释放互斥锁", Thread.CurrentThread.Name);
}
}
互斥锁可以在不同的进程间实现线程同步
使用互斥锁实现一个一次只能启动一个应用程序的功能。
public static class SingleInstance
{
private static Mutex m;
public static bool IsSingleInstance()
{
//是否需要创建一个应用
Boolean isCreateNew = false;
try
{
m = new Mutex(initiallyOwned: true, name: "SingleInstanceMutex", createdNew: out isCreateNew);
}
catch (Exception ex)
{
}
return isCreateNew;
}
}
互斥锁的带有三个参数的构造函数
initiallyOwned: 如果initiallyOwned为true,互斥锁的初始状态就是被所实例化的线程所获取,否则实例化的线程处于未获取状态。
name:该互斥锁的名字,在操作系统中只有一个命名为name的互斥锁mutex,如果一个线程得到这个name的互斥锁,其他线程就无法得到这个互斥锁了,必须等待那个线程对这个线程释放。
createNew:如果指定名称的互斥体已经存在就返回false,否则返回true。
信号和句柄
lock
和mutex
可以实现线程同步,确保一次只有一个线程执行。但是线程间的通信就不能实现。如果线程需要相互通信的话就要使用AutoResetEvent
,ManualResetEvent
,通过信号来相互通信。它们都有两个状态,终止状态和非终止状态。只有处于非终止状态时,线程才可以阻塞。
AutoResetEvent:
AutoResetEvent
构造函数可以传入一个bool类型的参数,false
表示将AutoResetEvent
对象的初始状态设置为非终止。如果为true
标识终止状态,那么WaitOne
方法就不会再阻塞线程了。但是因为该类会自动的将终止状态修改为非终止,所以,之后再调用WaitOne
方法就会被阻塞。
WaitOne
方法如果AutoResetEvent
对象状态非终止,则阻塞调用该方法的线程。可以指定时间,若没有获取到信号,返回false
set
方法释放被阻塞的线程。但是一次只可以释放一个被阻塞的线程。
class ThreadSafe
{
static AutoResetEvent autoEvent;
static void Main()
{
//使AutoResetEvent处于非终止状态
autoEvent = new AutoResetEvent(false);
Console.WriteLine("主线程运行...");
Thread t = new Thread(DoWork);
t.Start();
Console.WriteLine("主线程sleep 1秒...");
Thread.Sleep(1000);
Console.WriteLine("主线程释放信号...");
autoEvent.Set();
}
static void DoWork()
{
Console.WriteLine(" t线程运行DoWork方法,阻塞自己等待main线程信号...");
autoEvent.WaitOne();
Console.WriteLine(" t线程DoWork方法获取到main线程信号,继续执行...");
}
}
输出
主线程运行...
主线程sleep 1秒...
t线程运行DoWork方法,阻塞自己等待main线程信号...
主线程释放信号...
t线程DoWork方法获取到main线程信号,继续执行...
ManualResetEvent
ManualResetEvent
和AutoResetEvent
用法类似。
AutoResetEvent
在调用了Set
方法后,会自动的将信号由释放(终止)改为阻塞(非终止),一次只有一个线程会得到释放信号。而ManualResetEvent
在调用Set
方法后不会自动的将信号由释放(终止)改为阻塞(非终止),而是一直保持释放信号,使得一次有多个被阻塞线程运行,只能手动的调用Reset
方法,将信号由释放(终止)改为阻塞(非终止),之后的再调用Wait.One方法的线程才会被再次阻塞。
public class ThreadSafe
{
//创建一个处于非终止状态的ManualResetEvent
private static ManualResetEvent mre = new ManualResetEvent(false);
static void Main()
{
for(int i = 0; i <= 2; i++)
{
Thread t = new Thread(ThreadProc);
t.Name = "Thread_" + i;
t.Start();
}
Thread.Sleep(500);
Console.WriteLine("\n新线程的方法已经启动,且被阻塞,调用Set释放阻塞线程");
mre.Set();
Thread.Sleep(500);
Console.WriteLine("\n当ManualResetEvent处于终止状态时,调用由Wait.One方法的多线程,不会被阻塞。");
for(int i = 3; i <= 4; i++)
{
Thread t = new Thread(ThreadProc);
t.Name = "Thread_" + i;
t.Start();
}
Thread.Sleep(500);
Console.WriteLine("\n调用Reset方法,ManualResetEvent处于非阻塞状态,此时调用Wait.One方法的线程再次被阻塞");
mre.Reset();
Thread t5 = new Thread(ThreadProc);
t5.Name = "Thread_5";
t5.Start();
Thread.Sleep(500);
Console.WriteLine("\n调用Set方法,释放阻塞线程");
mre.Set();
}
private static void ThreadProc()
{
string name = Thread.CurrentThread.Name;
Console.WriteLine(name + " 运行并调用WaitOne()");
mre.WaitOne();
Console.WriteLine(name + " 结束");
}
}
//Thread_2 运行并调用WaitOne()
//Thread_1 运行并调用WaitOne()
//Thread_0 运行并调用WaitOne()
//新线程的方法已经启动,且被阻塞,调用Set释放阻塞线程
//Thread_2 结束
//Thread_1 结束
//Thread_0 结束
//当ManualResetEvent处于终止状态时,调用由Wait.One方法的多线程,不会被阻塞。
//Thread_3 运行并调用WaitOne()
//Thread_4 运行并调用WaitOne()
//Thread_4 结束
//Thread_3 结束
///调用Reset方法,ManualResetEvent处于非阻塞状态,此时调用Wait.One方法的线程再次被阻塞
//Thread_5 运行并调用WaitOne()
//调用Set方法,释放阻塞线程
//Thread_5 结束
Interlocked
如果一个变量被多个线程修改,读取。可以用Interlocked
。
计算机上不能保证对一个数据的增删是原子性的,因为对数据的操作也是分步骤的:
将实例变量中的值加载到寄存器中。
增加或减少该值。
在实例变量中存储该值。
Interlocked
为多线程共享的变量提供原子操作。Interlocked
提供了需要原子操作的方法:
public static int Add (ref int location1, int value); 两个参数相加,且把结果和赋值该第一个参数。
public static int Increment (ref int location); 自增。
public static int CompareExchange (ref int location1, int value, int comparand);
location1 和comparand比较,被value替换.
value 如果第一个参数和第三个参数相等,那么就把value赋值给第一个参数。
comparand 和第一个参数对比。
ReaderWriterLock
如果要确保一个资源或数据在被访问之前是最新的。那么就可以使用ReaderWriterLock
.该锁确保在对资源获取赋值或更新时,只有它自己可以访问这些资源,其他线程都不可以访问。即排它锁。但用改锁读取这些数据时,不能实现排它锁。
lock
允许同一时间只有一个线程执行。而ReaderWriterLock
允许同一时间有多个线程可以执行读操作,或者只有一个有排它锁的线程执行写操作。
class Program
{
// 创建一个对象
public static ReaderWriterLock readerwritelock = new ReaderWriterLock();
static void Main(string[] args)
{
//创建一个线程读取数据
Thread t1 = new Thread(Write);
// t1.Start(1);
Thread t2 = new Thread(Write);
//t2.Start(2);
// 创建10个线程读取数据
for (int i = 3; i < 6; i++)
{
Thread t = new Thread(Read);
// t.Start(i);
}
Console.Read();
}
// 写入方法
public static void Write(object i)
{
// 获取写入锁,20毫秒超时。
Console.WriteLine("线程:" + i + "准备写...");
readerwritelock.AcquireWriterLock(Timeout.Infinite);
Console.WriteLine("线程:" + i + " 写操作" + DateTime.Now);
// 释放写入锁
Console.WriteLine("线程:" + i + "写结束...");
Thread.Sleep(1000);
readerwritelock.ReleaseWriterLock();
}
// 读取方法
public static void Read(object i)
{
Console.WriteLine("线程:" + i + "准备读...");
// 获取读取锁,20毫秒超时
readerwritelock.AcquireReaderLock(Timeout.Infinite);
Console.WriteLine("线程:" + i + " 读操作" + DateTime.Now);
// 释放读取锁
Console.WriteLine("线程:" + i + "读结束...");
Thread.Sleep(1000);
readerwritelock.ReleaseReaderLock();
}
}
//分别屏蔽writer和reader方法。可以更清晰的看到 writer被阻塞了。而reader没有被阻塞。
//屏蔽reader方法
//线程:1准备写...
//线程:1 写操作2017/7/5 17:50:01
//线程:1写结束...
//线程:2准备写...
//线程:2 写操作2017/7/5 17:50:02
//线程:2写结束...
//屏蔽writer方法
//线程:3准备读...
//线程:5准备读...
//线程:4准备读...
//线程:5 读操作2017/7/5 17:50:54
//线程:5读结束...
//线程:3 读操作2017/7/5 17:50:54
//线程:3读结束...
//线程:4 读操作2017/7/5 17:50:54
//线程:4读结束...
参考:
MSDN
《CLR via C#》
来源:https://www.cnblogs.com/JoeSnail/p/7155815.html


猜你喜欢
- C# httpwebrequest访问HTTPS链接时遇到这个错误,但是如果我开抓包工具,比如filddler2,则POST返回正常错误提示
- 简单说一下(定义)什么是原型模式:原型模式是用于创建重复的对象,同时又能保证性能。用一个已经创建的实例作为原型,通过复制该原型对象来创建一个
- Maven依赖:<dependency><groupId>de.rototor.jeuclid</groupI
- 一、简介Bottom Sheet是Design Support Library23.2 版本引入的一个类似于对话框的控件。 Bottom S
- 废话不多说了,直接给大家贴代码,具体代码如下所示://菜单树形结构 public JSONArray treeMenuList(JSONAr
- 本文实例分析了Android编程之json解析的方法。分享给大家供大家参考,具体如下:JSON的定义:一种轻量级的数据交换格式,具有良好的可
- Maven是个很好用的管理工具,不经能够管理jar,还能实现打包。这里讲解Maven 本地打包,服务器打包,可以全部交给jenkins去完成
- 本文实例讲述了Android实现跑马灯效果的方法。分享给大家供大家参考。具体如下:运行效果截图如下:直接在布局里写代码就好了:<Tex
- 目录字节输入流字节输入流结构图FileInputStream类构造方法:常用读取方法:字节输出流字节输出流结构图:FileOutputStr
- 本文实例为大家分享了Java实现银行ATM系统的具体代码,供大家参考,具体内容如下一、前言银行ATM系列简单操作二、使用步骤1.创建用户信息
- 需求:接口是否可继承接口?抽象类是否可实现(implements)接口?抽象类是否可继承实体类(concrete class)?抽象类中是否
- 一、AOP概述AOP,即面向切面编程,简单来说就是将代码中重复的部分抽取出来,在需要执行的时候使用 * 的技术,在不修改源码的基础上对方法
- 背景传统 SpringMVC 项目中,我们可以定义容器初始化 Servlet ,然后在 web.xml 配置该 Servlet ,指定 lo
- 本文实例为大家分享了C#实现钟表程序设计的具体代码,供大家参考,具体内容如下工作空间:代码如下:using System;using Sys
- 今天,我们接着讲微信支付的系列教程,前面,我们讲了这个微信红包和扫码支付。现在,我们讲讲这个公众号支付。公众号支付的应用环境常见的用户通过公
- public static IPAddress GetHostIP(){ &nbs
- 本文实例为大家分享了unity实现虚拟摇杆控的具体代码,供大家参考,具体内容如下using UnityEngine;using UnityE
- Android RollPagerView实现轮播图android图片轮播效果,RollViewPager的简单使用 <c
- try就像一个网,把try{}里面的代码所抛出的异常都网住,然后把异常交给catch{}里面的代码去处理。最后执行finally之中的代码。
- @Bean修饰的方法参数的注入方法参数默认注入方式为Autowired,即先根据类型匹配,若有多个在根据名称进行匹配。1:复杂类型可以通过@