C# 多线程入门系列(三)
前面两篇文章,分别简述了多线程的使用和发展历程,但是使用多线程无法避免的一个问题就是多线程安全。那什么是多线程安全?如何解决多线程安全?本文主要通过一些简单的小例子,简述多线程相关的问题,仅供学习分享使用,如有不足之处,还请指正。
什么是多线程安全?
一段程序,单线程和多线程执行结果不一致,就表示存在多线程安全问题,即多线程不安全。
多线程安全示例
1. 多线程安全示例1
假如我们有一个需求,需要输出5个线程,且线程序号按0-4命名,我们编写代码如下:
private void btnTask1_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");for (int i = 0; i < 5; i++){Task.Run(() =>{Console.WriteLine($"【BEGIN】**************这是第 {i} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");Thread.Sleep(2000);Console.WriteLine($"【 END 】**************这是第 {i} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");});}Console.WriteLine("【结束】**************线程不安全示例btnTask1_Click**************");
}
然后运行示例,如下所示:
通过对以上示例进行分析,得出结论如下:
- 在for循环中,启动的5个线程,线程序号都是5,并没有按照我们预期的结果【0,1,2,3,4】进行输出。
- 经过分析发现,因为for循环中,i是同一个变量,线程启动是异步进行的,存在延迟,当线程启动时,for循环已经结束,i的值为5,所以才导致线程序号和预期不一致。
为了解决上述问题,可以通过引入局部变量来解决,即每次循环声明一个变量,循环5次,存在5个变量,则相互之间不会覆盖。如下所示:
private void btnTask1_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");for (int i = 0; i < 5; i++){int k = i;Task.Run(() =>{Console.WriteLine($"【BEGIN】**************这是第 {k} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");Thread.Sleep(2000);Console.WriteLine($"【 END 】**************这是第 {k} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");});}Console.WriteLine("【结束】**************线程不安全示例btnTask1_Click**************");
}
运行优化后的示例,如下所示:
通过运行示例发现,局部变量可以解决相应的问题。
2. 多线程安全示例2
假如我们有一个需求:将0到200增加到一个列表中,采用多线程来实现,如下所示:
private void btnTask2_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");List<int> list = new List<int>();List<Task> tasks = new List<Task>();for (int i = 0; i < 200; i++){tasks.Add( Task.Run(() =>{list.Add(i);}));}Task.WaitAll(tasks.ToArray());string res = string.Join(",", list);Console.WriteLine($"列表长度: {list.Count} ,列表内容:{res}");Console.WriteLine("【结束】**************线程不安全示例btnTask1_Click**************");
}
通过运行示例,如下所示:
通过对以上示例进行分析,得出结论如下:
- 列表的记录条数不对,会少。
- 列表的元素内容与预期的内容不一致。
针对上述问题,采用中间局部变量的方式,可以解决吗?不妨一试,修改后的 代码如下:
private void btnTask2_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");List<int> list = new List<int>();List<Task> tasks = new List<Task>();for (int i = 0; i < 200; i++){int k = i;tasks.Add( Task.Run(() =>{list.Add(k);}));}Task.WaitAll(tasks.ToArray());string res = string.Join(",", list);Console.WriteLine($"列表长度: {list.Count} ,列表内容:{res}");Console.WriteLine("【结束】**************线程不安全示例btnTask1_Click**************");
}
运行优化示例,如下所示:
通过运行上述示例,得出结论如下:
- 列表长度依然不对,会小于实际单一线程的长度。注意:多线程列表长度不是一定会小于单一线程运行时列表长度,只是存在概率,即多个线程存在同时写入一个位置的概率。
- 列表内容,采用局部变量,可以解决部分问题。
由此可以得出List不是线程安全的数据类型。
加锁lock
针对多线程的不安全问题,可以通过加锁进行解决,加锁的目的:在任意时刻,加锁块都之允许一个线程访问。
加锁原理
lock实际是一个语法糖,实际效果等同于Monitor。锁定的是引用对象的一个内存地址引用。所以锁定对象不可以是值类型,也不可以是null,只能是引用类型。
lock对象的标准写法:默认情况下,锁对象是私有,静态,只读,引用对象。如下所示:
/// <summary>
/// 定义一个锁对象
/// </summary>
private static readonly object obj = new object();
然后优化程序,如下所示:
private void btnTask2_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");List<int> list = new List<int>();List<Task> tasks = new List<Task>();for (int i = 0; i < 200; i++){int k = i;tasks.Add( Task.Run(() =>{lock (obj){list.Add(k);}}));}Task.WaitAll(tasks.ToArray());string res = string.Join(",", list);Console.WriteLine($"列表长度: {list.Count} ,列表内容:{res}");Console.WriteLine("【结束】**************线程不安全示例btnTask1_Click**************");
}
运行优化后的示例,如下所示:
通过对上述示例进行分析,得出结论如下:
- 加锁后,列表在多线程下也变成安全,符合预期的要求。
- 但是由于加锁的原因,同一时刻,只能由一个线程进入,其他线程就会等待,所以多线程也变成了单线程。
为何锁对象要用私有类型?
标准写法,锁对象是私有类型,目的是为了避免锁对象被其他线程使用,如果被使用,则会相互阻塞,如下所示:
假如,现在有一个锁对象,在TestLock中使用,如下所示:
public class TestLock
{public static readonly object Obj = new object();public void Show(){Console.WriteLine("【开始】**************线程示例Show**************");for (int i = 0; i < 5; i++){int k = i;Task.Run(() =>{lock (Obj){Console.WriteLine($"【BEGIN】*********T*****这是第 {k} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");Thread.Sleep(2000);Console.WriteLine($"【 END 】*********T*****这是第 {k} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");}});}Console.WriteLine("【结束】**************线程示例Show**************");}
}
同时在FrmMain中使用,如下所示:
private void btnTask3_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程示例btnTask3_Click**************");//类对象中多线程TestLock.Show();//主方法中多线程for (int i = 0; i < 5; i++){int k = i;Task.Run(() =>{lock (TestLock.Obj){Console.WriteLine($"【BEGIN】*********M*****这是第 {k} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");Thread.Sleep(2000);Console.WriteLine($"【 END 】*********M*****这是第 {k} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");}});}Console.WriteLine("【结束】**************线程示例btnTask3_Click**************");
}
运行上述示例,如下所示:
通过上述示例,得出结论如下:
- T和M是成对相邻,且各代码块交互出现。
- 多个代码块,共用一把锁,是会相互阻塞的。这也是为啥不建议使用public修饰符的原因,避免被不恰当的加锁。
如果使用不同的锁对象,多个代码块之间是可以并发的【T和M是不成对,且不相邻出现,但是有同一代码块的内部顺序】,效果如下:
为什么锁对象要用static类型?
假如对象不是static类型,那么锁对象就是对象属性,不同的对象之间是相互独立的,所以不同通对象调用相同的方法,就会存在并发的问题,如下所示:
修改TestLock代码【去掉static】,如下所示:
public class TestLock
{public readonly object Obj = new object();public void Show(string name){Console.WriteLine("【开始】**************线程示例Show--{0}**************",name);for (int i = 0; i < 5; i++){int k = i;Task.Run(() =>{lock (Obj){Console.WriteLine($"【BEGIN】*********T*****这是第 {k}--{name} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");Thread.Sleep(2000);Console.WriteLine($"【 END 】*********T*****这是第 {k}--{name} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");}});}Console.WriteLine("【结束】**************线程示例Show--{0}**************",name);}
}
声明两个对象,分别调用Show方法,如下所示:
private void btnTask4_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程示例btnTask3_Click**************");TestLock testLock1 = new TestLock();testLock1.Show("first");TestLock testLock2 = new TestLock();testLock2.Show("second");Console.WriteLine("【结束】**************线程示例btnTask3_Click**************");
}
测试示例,如下所示:
通过以上示例,得出结论如下:
- 非静态锁对象,只在当前对象内部进行允许同一时刻只有一个线程进入,但是多个对象之间,是相互并发,相互独立的。所以建议锁对象为static对象。
加锁锁定的是什么?
在lock模式下,锁定的是内存引用地址,而不是锁定的对象的值。假如将Form的锁对象的类型改为字符串,如下所示:
/// <summary>
/// 定义一个锁对象
/// </summary>
private static readonly string obj = "花无缺";
同时TestLock类的锁对象也改为字符串,如下所示:
public class TestLock
{private static readonly string obj = "花无缺";public static void Show(string name){Console.WriteLine("【开始】**************线程示例Show--{0}**************",name);for (int i = 0; i < 5; i++){int k = i;Task.Run(() =>{lock (obj){Console.WriteLine($"【BEGIN】*********T*****这是第 {k}--{name} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");Thread.Sleep(2000);Console.WriteLine($"【 END 】*********T*****这是第 {k}--{name} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");}});}Console.WriteLine("【结束】**************线程示例Show--{0}**************",name);}
}
运行上述示例,结果如下:
通过上述示例,得出结论如下:
- 字符串是一种特殊的锁类型,如果字符串的值一致,则认为是同一个锁对象,不同对象之间会进行阻塞。因为string类型是享元的,在内存堆里面只有一个花无缺。
- 如果是其他类型,则是不同的锁对象,是可以相互并发的。
- 说明锁定的是内存引用地址,而非锁定对象的值。
泛型锁对象
如果TestLock为泛型类,如下所示:
public class TestLock<T>
{private static readonly object obj = new object(); 4 public static void Show(string name){Console.WriteLine("【开始】**************线程示例Show--{0}**************",name);for (int i = 0; i < 5; i++){int k = i;Task.Run(() =>{lock (obj){Console.WriteLine($"【BEGIN】*********T*****这是第 {k}--{name} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");Thread.Sleep(2000);Console.WriteLine($"【 END 】*********T*****这是第 {k}--{name} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");}});}Console.WriteLine("【结束】**************线程示例Show--{0}**************",name);}
}
那么在调用时,会相互阻塞吗?调用代码如下:
private void btnTask5_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程示例btnTask5_Click**************");TestLock<int>.Show("AA");TestLock<string>.Show("BB");Console.WriteLine("【结束】**************线程示例btnTask5_Click**************");
}
运行上述示例,如下所示:
通过分析上述示例,得出结论如下所示:
- 对于泛型类,不同类型参数之间是可以相互并发的,因为泛型类针对不同类型参数会编译成不同的类,那对应的锁对象,会变成不同的引用类型。
- 如果锁对象为字符串类型,则也是会相互阻塞的,只是因为字符串是享元模式。
- 泛型T的不同,会编译成不同的副本。
递归加锁
如果在递归函数中进行加锁,会造成死锁吗?示例代码如下:
private void btnTask6_Click(object sender, EventArgs e)
{Console.WriteLine("【开始】**************线程示例btnTask6_Click**************");this.add(1);Console.WriteLine("【结束】**************线程示例btnTask6_Click**************");
}private int num = 0;private void add(int index) {this.num++;Task.Run(()=> {lock (obj){Console.WriteLine($"【BEGIN】**************这是第 {num} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");Thread.Sleep(2000);Console.WriteLine($"【 END 】**************这是第 {num} 个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");if (num < 5){this.add(index);}}});
}
运行上述示例,如下所示:
通过运行上述示例,得出结论如下:
- 在递归函数中进行加锁,会进行阻塞等待,但是不会造成死锁。
备注
以上就是多线程安全的简单介绍,旨在抛砖引玉,大家一起学习,共同进步。
酬乐天扬州初逢席上见赠【作者】刘禹锡 【朝代】唐
巴山楚水凄凉地,二十三年弃置身。
怀旧空吟闻笛赋,到乡翻似烂柯人。
沉舟侧畔千帆过,病树前头万木春。
今日听君歌一曲,暂凭杯酒长精神。
C# 多线程入门系列(三)相关推荐
- 机器学习入门系列三(关键词:逻辑回归,正则化)
机器学习入门系列三(关键词:逻辑回归,正则化) 目录(?)[+] 一逻辑回归 逻辑回归 假设表示 决策边界 代价函数 其他优化方法 多元分类 二正则化 一.逻辑回归 1.逻辑回归 什么是逻辑回归问题, ...
- Reflex WMS入门系列三十二:导出到Excel
Reflex WMS入门系列三十二:导出到Excel 如同SAP系统的风格 --- 凡是有list的界面,都能导出到Excel ---, Reflex WMS系统也提供了类似的功能.几乎在任何的Lis ...
- 小猪的C语言快速入门系列(三)
小猪的C语言快速入门系列(三) 标签: C语言 本节引言: 在上一节中,对C语言的基本语法进行了学习,类比成学英语的话,我们现在 只是会单词而已,组成一个个句子还需要学习一些语法,本节学习的就是两对 ...
- 零基础数据挖掘入门系列(三) - 数据清洗和转换技巧
思维导图:零基础入门数据挖掘的学习路径 1. 写在前面 零基础入门数据挖掘是记录自己在Datawhale举办的数据挖掘专题学习中的所学和所想, 该系列笔记使用理论结合实践的方式,整理数据挖掘相关知识, ...
- sumo添加车辆_SUMO仿真快速入门系列三:产生车辆移动模型
在<SUMO快速入门系列二>中,我们已经产生了一个较为简单的街道地图模型. 本节中我们产生车辆移动模型并与道路模型结合,使得车辆在真实道路中跑起来.在SUMO中,车辆移动模型称为Deman ...
- SUMO仿真快速入门系列三:产生车辆移动模型
在<SUMO快速入门系列二>中,我们已经产生了一个较为简单的街道地图模型. 本节中我们产生车辆移动模型并与道路模型结合,使得车辆在真实道路中跑起来.在SUMO中,车辆移动模型称为Deman ...
- etcd入门系列三:身份验证访问控制
etcd入门系列 一. etcd在docker中的安装与使用 二. etcd 开启 https 1. 简介 etcd 默认是没有开启访问控制的,如果我们开启外网访问的话就需要考虑访问控制的问题,etc ...
- 保存点云数据_PCL入门系列三——PCL进行数据读写
本节课我们将了解到以下内容: 基本的PCL中的数据类型: 使用PCL进行简单编程:写文件与读文件. 一.PCL库基本数据类型 上一节课,我们使用PCL库在本地写入了一个名为test_pcd.pcd的文 ...
- C# 多线程入门系列(二)
线程(英语:thread)是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的 ...
最新文章
- xshell启动报nssock2_nssock2.dll,下载,简介,描述,修复,等相关问题一站搞定_DLL之家
- DOM修改元素的方法总结
- nyc检测Javascript代码覆盖率
- BlockJUnit4ClassRunner
- 钱荒下银行理财收益率角逐:邮储银行垫底
- luogu 4768
- MySQL UNION 与 UNION ALL 语法与用法
- 42. 确保lessT与operator小于具有相同的语义
- 运用集合把文字写入读出文件
- 百度itextpdf工具类,快速生成PDF打印模板,itextpdf5加公章
- 看看故障诊断文献中的故障设置方法-中文论文篇
- Web框架-SSM框架
- python编写鸡兔同笼程序_鸡兔同笼问题的python实现
- 数字电路加法器 基本原理(一)
- 从“心”开始,带领团队度过变革期
- 记一次授权的APK渗透测试
- 关于“‘c‘ argument has 1 elements, which is not acceptable for use with ‘x‘ with size 300“的解决办法
- cannot be cast to javax.servlet.Servlet 解决
- 用R语言进行筛选数据
- 【BI学习作业18-评分卡模型】