C#多线程安全怎么理解
C#多线程安全怎么理解
这篇文章主要介绍“C#多线程安全怎么理解”,在日常操作中,相信很多人在C#多线程安全怎么理解问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C#多线程安全怎么理解”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
什么是多线程安全?
一段程序,单线程和多线程执行结果不一致,就表示存在多线程安全问题,即多线程不安全。
多线程安全示例
1. 多线程不安全示例1
假如我们有一个需求,需要输出5个线程,且线程序号按0-4命名,我们编写代码如下:
privatevoidbtnTask1_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");for(inti=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**************");}
然后运行示例,如下所示:
通过对以上示例进行分析,得出结论如下:
1.在for循环中,启动的5个线程,线程序号都是5,并没有按照我们预期的结果【0,1,2,3,4】进行输出。
2.经过分析发现,因为for循环中,i是同一个变量,线程启动是异步进行的,存在延迟,当线程启动时,for循环已经结束,i的值为5,所以才导致线程序号和预期不一致。
为了解决上述问题,可以通过引入局部变量来解决,即每次循环声明一个变量,循环5次,存在5个变量,则相互之间不会覆盖。如下所示:
privatevoidbtnTask1_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");for(inti=0;i<5;i++){intk=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增加到一个列表中,采用多线程来实现,如下所示:
privatevoidbtnTask2_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");List<int>list=newList<int>();List<Task>tasks=newList<Task>();for(inti=0;i<200;i++){tasks.Add(Task.Run(()=>{list.Add(i);}));}Task.WaitAll(tasks.ToArray());stringres=string.Join(",",list);Console.WriteLine($"列表长度:{list.Count},列表内容:{res}");Console.WriteLine("【结束】**************线程不安全示例btnTask1_Click**************");}
通过运行示例,如下所示:
通过对以上示例进行分析,得出结论如下:
1.列表的记录条数不对,会少。
2.列表的元素内容与预期的内容不一致。
针对上述问题,采用中间局部变量的方式,可以解决吗?不妨一试,修改后的 代码如下:
privatevoidbtnTask2_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");List<int>list=newList<int>();List<Task>tasks=newList<Task>();for(inti=0;i<200;i++){intk=i;tasks.Add(Task.Run(()=>{list.Add(k);}));}Task.WaitAll(tasks.ToArray());stringres=string.Join(",",list);Console.WriteLine($"列表长度:{list.Count},列表内容:{res}");Console.WriteLine("【结束】**************线程不安全示例btnTask1_Click**************");}
运行优化示例,如下所示:
通过运行上述示例,得出结论如下:
1.列表长度依然不对,会小于实际单一线程的长度。注意:多线程列表长度不是一定会小于单一线程运行时列表长度,只是存在概率,即多个线程存在同时写入一个位置的概率。
2.列表内容,采用局部变量,可以解决部分问题。
由此可以得出List不是线程安全的数据类型。
加锁lock
针对多线程的不安全问题,可以通过加锁进行解决,加锁的目的:在任意时刻,加锁块都之允许一个线程访问。
加锁原理
lock实际是一个语法糖,实际效果等同于Monitor。锁定的是引用对象的一个内存地址引用。所以锁定对象不可以是值类型,也不可以是null,只能是引用类型。
lock对象的标准写法:默认情况下,锁对象是私有,静态,只读,引用对象。如下所示:
///<summary>///定义一个锁对象///</summary>privatestaticreadonlyobjectobj=newobject();
然后优化程序,如下所示:
privatevoidbtnTask2_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程不安全示例btnTask1_Click**************");List<int>list=newList<int>();List<Task>tasks=newList<Task>();for(inti=0;i<200;i++){intk=i;tasks.Add(Task.Run(()=>{lock(obj){list.Add(k);}}));}Task.WaitAll(tasks.ToArray());stringres=string.Join(",",list);Console.WriteLine($"列表长度:{list.Count},列表内容:{res}");Console.WriteLine("【结束】**************线程不安全示例btnTask1_Click**************");}
运行优化后的示例,如下所示:
通过对上述示例进行分析,得出结论如下:
1.加锁后,列表在多线程下也变成安全,符合预期的要求。
2.但是由于加锁的原因,同一时刻,只能由一个线程进入,其他线程就会等待,所以多线程也变成了单线程。
为何锁对象要用私有类型?
标准写法,锁对象是私有类型,目的是为了避免锁对象被其他线程使用,如果被使用,则会相互阻塞,如下所示:
假如,现在有一个锁对象,在TestLock中使用,如下所示:
publicclassTestLock{publicstaticreadonlyobjectObj=newobject();publicvoidShow(){Console.WriteLine("【开始】**************线程示例Show**************");for(inti=0;i<5;i++){intk=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中使用,如下所示:
privatevoidbtnTask3_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程示例btnTask3_Click**************");//类对象中多线程TestLock.Show();//主方法中多线程for(inti=0;i<5;i++){intk=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**************");}
运行上述示例,如下所示:
通过上述示例,得出结论如下:
1.T和M是成对相邻,且各代码块交互出现。
2.多个代码块,共用一把锁,是会相互阻塞的。这也是为啥不建议使用public修饰符的原因,避免被不恰当的加锁。
如果使用不同的锁对象,多个代码块之间是可以并发的【T和M是不成对,且不相邻出现,但是有同一代码块的内部顺序】,效果如下:
为什么锁对象要用static类型?
假如对象不是static类型,那么锁对象就是对象属性,不同的对象之间是相互独立的,所以不同通对象调用相同的方法,就会存在并发的问题,如下所示:
修改TestLock代码【去掉static】,如下所示:
publicclassTestLock{publicreadonlyobjectObj=newobject();publicvoidShow(stringname){Console.WriteLine("【开始】**************线程示例Show--{0}**************",name);for(inti=0;i<5;i++){intk=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方法,如下所示:
privatevoidbtnTask4_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程示例btnTask3_Click**************");TestLocktestLock1=newTestLock();testLock1.Show("first");TestLocktestLock2=newTestLock();testLock2.Show("second");Console.WriteLine("【结束】**************线程示例btnTask3_Click**************");}
测试示例,如下所示:
通过以上示例,得出结论如下:
非静态锁对象,只在当前对象内部进行允许同一时刻只有一个线程进入,但是多个对象之间,是相互并发,相互独立的。所以建议锁对象为static对象。
加锁锁定的是什么?
在lock模式下,锁定的是内存引用地址,而不是锁定的对象的值。假如将Form的锁对象的类型改为字符串,如下所示:
///<summary>///定义一个锁对象///</summary>privatestaticreadonlystringobj="花无缺";
同时TestLock类的锁对象也改为字符串,如下所示:
publicclassTestLock{privatestaticreadonlystringobj="花无缺";publicstaticvoidShow(stringname){Console.WriteLine("【开始】**************线程示例Show--{0}**************",name);for(inti=0;i<5;i++){intk=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);}}
运行上述示例,结果如下:
通过上述示例,得出结论如下:
1.字符串是一种特殊的锁类型,如果字符串的值一致,则认为是同一个锁对象,不同对象之间会进行阻塞。因为string类型是享元的,在内存堆里面只有一个花无缺。
2.如果是其他类型,则是不同的锁对象,是可以相互并发的。
3.说明锁定的是内存引用地址,而非锁定对象的值。
泛型锁对象
如果TestLock为泛型类,如下所示:
1publicclassTestLock<T>2{3privatestaticreadonlyobjectobj=newobject();45publicstaticvoidShow(stringname)6{78Console.WriteLine("【开始】**************线程示例Show--{0}**************",name);910for(inti=0;i<5;i++)11{12intk=i;13Task.Run(()=>14{15lock(obj)16{17Console.WriteLine($"【BEGIN】*********T*****这是第{k}--{name}个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");18Thread.Sleep(2000);19Console.WriteLine($"【END】*********T*****这是第{k}--{name}个线程,线程ID={Thread.CurrentThread.ManagedThreadId}**************");20}21});22}2324Console.WriteLine("【结束】**************线程示例Show--{0}**************",name);25}26}
那么在调用时,会相互阻塞吗?调用代码如下:
privatevoidbtnTask5_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程示例btnTask5_Click**************");TestLock<int>.Show("AA");TestLock<string>.Show("BB");Console.WriteLine("【结束】**************线程示例btnTask5_Click**************");}
运行上述示例,如下所示:
通过分析上述示例,得出结论如下所示:
1.对于泛型类,不同类型参数之间是可以相互并发的,因为泛型类针对不同类型参数会编译成不同的类,那对应的锁对象,会变成不同的引用类型。
2.如果锁对象为字符串类型,则也是会相互阻塞的,只是因为字符串是享元模式。
3.泛型T的不同,会编译成不同的副本。
递归加锁
如果在递归函数中进行加锁,会造成死锁吗?示例代码如下:
privatevoidbtnTask6_Click(objectsender,EventArgse){Console.WriteLine("【开始】**************线程示例btnTask6_Click**************");this.add(1);Console.WriteLine("【结束】**************线程示例btnTask6_Click**************");}privateintnum=0;privatevoidadd(intindex){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#多线程安全怎么理解”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注恰卡编程网网站,小编会继续努力为大家带来更多实用的文章!
推荐阅读
-
polyfills怎么按需加载
polyfills怎么按需加载本篇内容主要讲解“polyfills...
-
C#数据类型怎么实现背包、队列和栈
-
C#怎么实现冒泡排序和插入排序算法
C#怎么实现冒泡排序和插入排序算法这篇文章主要讲解了“C#怎么实现...
-
C#如何实现希尔排序
-
C#如何实现归并排序
-
C#怎么使用符号表实现查找算法
-
C#类的静态成员怎么用
C#类的静态成员怎么用这篇“C#类的静态成员怎么用”文章的知识点大...
-
C#的静态函数怎么用
C#的静态函数怎么用这篇文章主要讲解了“C#的静态函数怎么用”,文...
-
C#中的析构函数怎么用
C#中的析构函数怎么用这篇文章主要讲解了“C#中的析构函数怎么用”...
-
怎么用CZGL.ProcessMetrics监控.NET应用