首页 > 作文

深入了解C#多线程安全

更新时间:2023-04-04 04:36:59 阅读: 评论:0

目录
什么是多线程安全?多线程安全示例1. 多线程不安全示例12. 多线程不安全示例2加锁lock加锁原理为何锁对象要用私有类型?为什么锁对象要用static类型?加锁锁定的是什么?泛型锁对象递归加锁

前面两篇文章,分别简述了多线程的使用和发展历程,但是使用多线程无法避免的一个问题就是多线程安全。那什么是多线程安全?如何解决多线程安全?本文主要通过一些简单的小例子,简述多线程相关的问题,仅供学习分享使用,如有不足之处,还请指正。

什么是多线程安全?

一段程序,单线程和多线程执行结果不一致,就表示存在多线程安全问题,即多线程不安全。

多线程安全示例

1. 多线程不安全示例1

假如我们有一个需求,需要输出5个线程,且线程序号按0-4命名,我们编写代码如下:

private void btntask1_click(object nder, 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**************");}

然后运行示例,如下所示:

通过对以上示例进行分析,得出结论如下:

1.在for循环中,启动的5个线程,线程序号都是5,并没有按照我们预期的结果【0,1,2,3,4】进行输出。

2.经过分析发现,因为for循环中,i是同一个变量,线程启动是异步进行的,存在延迟,当线程启动时,for循环已经结束,i的值为5,所以才导致线程序号和预期不一致。

为了解决上述问题,可以通过引入局部变量来解决,即每次循环声明一个变量,循环5次,存在5个变量,则相互之间不会覆盖。如下所示:

private void btntask1_click(object nder, 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 nder, 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**************");}

通过运行示例,如下所示:

通过对以上示例进行分析,得出结论如下:

1.列表的记录条数不对,会少。

2.列表的元素内容与预期的内容不一致。

针对上述问题,采用中间局部变量的方式,可以解决吗?不妨一试,修改后的 代码如下:

private void btntask2_click(object nder, 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**************");}

运行优化示例,如下所示:

通过运行上述示例,得出结论如下:

1.列表长度依然不对,会小于实际单一线程的长度。注意:多线程列表长度不是一定会小于单一线程运行时列表长度,只是存在概率,即多个线程存在同时写入一个位置的概率。

2.列表内容,采用局部变量,可以解决部分问题。

由此可以得出list不是线程安全的数据类型。

加锁lock

针对多线程的不安全问题,可以通过加锁进行解决,加锁的目的:在任意时刻,加锁块都之允许一个线程访问。

加锁原理

lock实际是一个语法糖,实际效果等同于monitor。锁定的是引用对象的一个内存地址引用。所以锁定对象不可以是值类型,也不可以是null,只能是引用类型。

lock对象的标准写法:默认情况下,锁对象是私有,静态,只读,引用对象。如下所示:

/// <summary>/// 定义一个锁对象/// </summary>private static readonly object obj = new object();

然后优化程序,如下所示:

private void btntask2_click(object nder, 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**************");}

运行优化后的示例,如下所示:

通过对上述示例有关读书的成语进行分析,得出结论如下:

1.加锁后,列表在多线程下也变成安全,符合预期的要求。

2.但是由于加锁的原因,同一时刻,只能由一个线程进入,其他线程就会等待,所以多线程也变成了单线程。

为何锁对象要用私有类型?

标准写法,锁对象是私有类型,目的是为了避免锁对象被其他线程使用,如果被使用,则会相互阻塞,如下所示:

假如,现在有一个锁对象,在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 nder, 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**************");}

运行上述示例,如下所示:

通过上述示例,得出结论如下:

1.t和m是成对相邻,且各代码块交互出现。

2.多个代码块,共用一把锁,是会相互阻塞的。这也是为啥不建议使用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++)        {            in高二下t 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 bt茶叶公司简介ntask4_click(object nder, eventargs e){    console.writeline("【开始】**************线程示例btntask3_click**************");    testlock testlock1 = new testlock();    testlock1.show("first");    testlock testlock2 = new testlock();    testlock2.show("cond");    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);    }}

运行上述示例,结果如下:

通过上述示例,得出结论如下:

1.字符串是一种特殊的锁类型,如果字符串的值一致,则认为是同一个锁对象,不同对象之间会进行阻塞。因为string类型是享元的,在内存堆里面只有一个花无缺。

2.如果是其他类型,则是不同的锁对象,是可以相互并发的。

3.说明锁定的是内存引用地址,而非锁定对象的值。

泛型锁对象

如果testlock为泛型类,如下所示:

1 public class testlock<t> 2 { 3     private static readonly object obj = new object(); 4  5     public static  void show(string name) 6     { 7  8         console.writeline("【开始】**************线程示例show--{0}**************",name); 9 10         for (int i = 0; i < 5; i++)11         {12             int k = i;13             task.run(() =>14             {15                 lock (obj)16                 {17                     console.writeline($"【begin】*********t*****这是第 {k}--{name} 个线程,线程id={thread.currentthread.managedthreadid}**************");18                     thread.sleep(2000);19                     console.writeline($"【 end 】*********t*****这是第 {k}--{name} 个线程,线程id={thread.currentthread.managedthreadid}**************");20                 }21             });22         }23 24         console.writeline("【结束】**************线程示例show--{0}**************",name);25     }26 }

那么在调用时,会相互阻塞吗?调用代码如下:

private void btntask5_click(object nder, eventargs e){    console.writeline("【开始】**************线程示例btntask5_click**************");    testlock<int>.show("aa");    testlock<string>.show("bb");    console.writeline("【结束】**************线程示例btntask5_click**************");}

运行上述示例,如下所示:

通过分析上述示例,得出结论如下所示:

1.对于泛型类,不同类型参数之间是可以相互并发的,因为泛型类针对不同类型参数会编译成不同的类,那对应的锁对象,会变成不同的引用类型。

2.如果锁对象为字符串类型,则也是会相互阻塞的,只是因为字符串是享元模式。

3.泛型t的不同,会编译成不同的副本。

递归加锁

如果在递归函数中进行加锁,会造成死锁吗?示例代码如下:

private void btntask6_click(object nder, 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#多线程安全的详细内容,更多关于c#多线程安全的资料请关注www.887551.com其它相关文章!

本文发布于:2023-04-04 04:36:57,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/zuowen/3383c44199f785afa45d2187efc8559b.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

本文word下载地址:深入了解C#多线程安全.doc

本文 PDF 下载地址:深入了解C#多线程安全.pdf

标签:线程   示例   对象   这是
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图