iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >深入了解C#多线程安全
  • 403
分享到

深入了解C#多线程安全

2024-04-02 19:04:59 403人浏览 薄情痞子
摘要

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

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

什么是多线程安全?

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

多线程安全示例

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**************");
}

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

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

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

2.经过分析发现,因为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**************");
}

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

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

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

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

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


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**************");
}

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

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

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

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

由此可以得出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**************");
}

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

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

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 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**************");
}

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

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

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++)
        {
            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);
    }
}

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

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

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 sender, 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 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#多线程安全的详细内容,更多关于C#多线程安全的资料请关注编程网其它相关文章!

--结束END--

本文标题: 深入了解C#多线程安全

本文链接: https://www.lsjlt.com/news/160520.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 深入了解C#多线程安全
    目录什么是多线程安全?多线程安全示例1. 多线程不安全示例12. 多线程不安全示例2加锁lock加锁原理为何锁对象要用私有类型?为什么锁对象要用static类型?加锁锁定的是什么?泛...
    99+
    2022-11-12
  • 深入了解Python的多线程基础
    目录线程多线程Python多线程创建线程GIL锁线程池总结线程 线程(Thread),有时也被称为轻量级进程(Lightweight Process,LWP),是操作系统独⽴调度和分...
    99+
    2022-11-12
  • 深入学习C#多线程
    目录一、基本概念1、进程2、线程二、多线程2.1System.Threading.Thread类2.2 线程的常用属性2.2.1线程的标识符2.2.2线程的优先级别2.2....
    99+
    2022-11-13
  • 深入理解线程安全与Singleton
    线程安全是个非常棘手的问题。即使你合理的使用了锁(lock),依然可能不会产生预期的效果。让我们来看看貌似合理的代码复制代码 代码如下:X=0;Thread 1  ...
    99+
    2022-11-15
    线程安全 Singleton
  • Java多线程通信问题深入了解
    目录概述引入加入线程安全实现生产者与消费者问题总结概述 多线程通信问题,也就是生产者与消费者问题 生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消...
    99+
    2022-11-12
  • C#多线程安全怎么理解
    这篇文章主要介绍“C#多线程安全怎么理解”,在日常操作中,相信很多人在C#多线程安全怎么理解问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C#多线程安全怎么理解”的疑惑有所帮助!接下来,请跟着小编一起来学习吧...
    99+
    2023-06-22
  • Java多线程深入理解
    目录线程Thread类Runnable接口创建线程Thread和Runnable的区别匿名内部类方式实现线程的创建线程安全线程安全线程同步同步方法Lock锁线程状态等待唤醒机制线程间...
    99+
    2022-11-12
  • C++线程安全的队列你了解嘛
    目录无界队列有界队列总结 无界队列 #include<queue> #include<mutex> #include<condition_variabl...
    99+
    2022-11-13
  • 一文读懂flutter线程: 深入了解Flutter中的多线程编程
    深入了解Flutter中的多线程编程 前言一、为什么需要多线程?二、在Flutter中创建线程三、多线程的最佳实践四、Flutter中的多线程示例五、Flutter中的多线程错误处理六、Flu...
    99+
    2023-10-28
    flutter
  • 深入理解python多线程编程
    进程 进程的概念: 进程是资源分配的最小单位,他是操作系统进行资源分配和调度运行的基本单位。通俗理解:一个正在运行的一个程序就是一个进程。例如:正在运行的qq、wechat等,它们都...
    99+
    2022-11-12
  • 深入理解QT多线程编程
    目录一、线程基础1、GUI线程与工作线程2、数据的同步访问二、QT多线程简介三、QThread线程1、QThread线程基础2、线程的优先级3、线程的创建4、线程的执行5、线程的退出...
    99+
    2022-11-12
  • 深入了解C++的多态与虚函数
    目录1.多态的机制与虚函数的机制1.1 多态的机制1.2 虚函数的机制1.3虚函数表的结构图1.4 动态多态实现的三个前提件(很重要)2.多态实例应用3.多态的巨大问题与虚析构3.1...
    99+
    2022-11-13
  • 深入理解java各种集合的线程安全
    线程安全首先要明白线程的工作原理,jvm有一个main memory,而每个线程有自己的workingmemory,一个线程对一个variable进行操作时,都要在自己的workingmemory里面建立一个copy,操作完之后再写入mai...
    99+
    2017-12-26
    java入门 java 线程安全
  • C# 线程安全详解
    目录介绍经典生产消费问题介绍QueueConcurrentQueueBlockingCollectionBlockingCollection 枚举BlockingCollection...
    99+
    2022-11-12
  • Java多线程之深入理解ReentrantLock
    目录前言一、可重入锁二、ReentrantLock2.1 ReentrantLock的简单使用2.2 ReentrantLock UML图2.3 lock()方法调用链三、AQS3....
    99+
    2022-11-12
  • redis 6.0之多线程,深入解读
    文章目录 前言一、架构演进1. 单线程2. 单线程+后台线程3. 多线程+后台线程 二、原理1. 初始化2. 多线程读3. 多线程写: 三、配置总结 前言 本文参考源码版本为...
    99+
    2023-08-31
    redis 数据库 缓存
  • 深入了解Python 中线程和进程区别
    目录一、什么是进程/线程1、引论2、线程3、进程4、区别5、使用二、多线程使用1、常用方法2、常用参数3、多线程的应用3.1重写线程法3.2直接调用法4、线程间数据的共享三、多进程使...
    99+
    2022-11-13
  • 【Java系列】深入解析Java多线程
    序言 你只管努力,其他交给时间,时间会证明一切。 文章标记颜色说明: 黄色:重要标题红色:用来标记结论绿色:用来标记一级重要蓝色:用来标记二级重要 希望这篇文章能让你不仅有一定的收获,而且可以愉快的学习,如果有什么建议...
    99+
    2023-08-31
    java 开发语言 jvm
  • 深入探究Java线程不安全的原因与解决
    目录一、什么是线程安全二、线程不安全的原因1、修改共享数据2、原子性3、内存可见性4、指令重排序三、解决线程安全方案一、什么是线程安全 想给出一个线程安全的确切定义是复杂的,但我们可...
    99+
    2022-11-13
  • Java多线程 - 线程安全和线程同步解决线程安全问题
    文章目录 线程安全问题线程同步方式一: 同步代码块方式二: 同步方法方式三: Lock锁 线程安全问题 线程安全问题指的是: 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。 举例:...
    99+
    2023-08-20
    java 安全 jvm
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作