再说多线程(一)

news2025/1/16 8:07:26

世界是并行!

做过复杂项目的朋友一定遇到过并发的问题,无论是大项目如订票系统,还是小项目中的文件管理都会有并行需求。所以不同于上学时接触的大部分代码,实际的业务往往是为多人提供服务,必然天然的带有并发的需求。这里我先不解释并行和并发的区别,也不去讨论cpu和操作系统的低层是如何做到并发的,让我们从最简单的串行说起。


1.串行世界

看下面一个简单的例子:

        public static void Print(string name)
        {
            Console.Write("[Today is raining day! ");
            Thread.Sleep(10);
            Console.WriteLine($"--- What do you do? : {(name as string)}]");
        }

一个简单的打印函数,只不过,在两句话之间,添加了一个小停顿。假设有三个人访问你家,你依次跟三个人进行上面的对话,那么你应该这样写:

        public static void Talk()
        {
            Print("王总");
            Print("张总");
            Print("刘总");
        }

那运行结果自然是:

[Today is raining day! --- What do you do? : 王总]
[Today is raining day! --- What do you do? : 张总]
[Today is raining day! --- What do you do? : 刘总]

假设是公司开会,来了很多客人,如果只安排你一个接待员,那肯定要接待很久,造成不良影响,所以你打算再找2个人帮你,这样一来接待效率变为之前的三倍,那程序要怎么处理呢?

2. 并发的问题

假设一个程序是一个线程,那三个接待员就是三个线程,C#中有一个线程类Thread就是来构造多线程的,基本用法可以参考MSDN。所以我们用Thread来模拟三个接待员:

首先把打印函数调整一下,以便作为参数传入线程中:

        public static void Print(object? name)
        {

                Console.Write("[Today is raining day! ");
                Thread.Sleep(1);
                Console.WriteLine($"--- What do you do? : {(name as string)}]");
            
        }

然后构造三个线程执行:

        public static void Run()
        {
            Thread t1 = new(Print) { Name = "t1" };
            Thread t2 = new(Print) { Name = "t2" };
            Thread t3 = new(Print) { Name = "t3" };
            t1.Start("张总");
            t2.Start("王总");
            t3.Start("刘总"); 
            Console.ReadLine();
        }

运行结果:

[Today is raining day! [Today is raining day! [Today is raining day! --- What do you do? : 王总]
--- What do you do? : 张总]
--- What do you do? : 刘总]

对话发生了混乱,显然可能是因为Sleep函数导致每个线程的第二段打印都滞后了,我们去掉Thread.Sleep().如果再次运行:

[Today is raining day! --- What do you do? : 张总]
[Today is raining day! [Today is raining day! --- What do you do? : 刘总]
--- What do you do? : 王总]

重复运行:

[Today is raining day! [Today is raining day! --- What do you do? : 张总]
--- What do you do? : 王总]
[Today is raining day! --- What do you do? : 刘总]

发现输出结果是无法预料的,这就像100个人同时抢10张火车票,假设同时开抢,后台同时收到100个订单,结果也可能是无法预知的。在我们刚才的例子,虽然打印混乱影响不大,但是在某些场景,这是很致命的,比如有名次的抽奖,比如抢演唱会门票。

所以,并行世界会衍生出很多串行程序中没有的问题,熟悉数据库的朋友都知道,数据库有各种锁来保证数据一致性,所以并发程序应该也是如此。纵观并发程序的各种设计,无非是要在下面两点下功夫:

  • 原子性:线程同步要支持原子性,也就是保证多个线程运行时不能让他们同时都能访问公共数据,以免造成数据的不一致性,程序的关键代码被原子性的执行(也就是有且只有一个线程在运行)。比如刚才讲得订票案例就是如此(不能让一张票分给了2个人)。

  • 顺序性:我们通常希望两个或更多线程以特定顺序执行任务,或者我们希望将对共享资源的访问限制为仅特定数量的线程。通常,我们对这一切没有太多控制,这是竞争条件的原因之一。线程同步提供对排序的支持,以便您可以控制线程以根据您的要求执行任务。

当进程或线程想要访问对象时,它会请求锁定该对象。有两种类型的锁决定了对共享资源的访问——独占锁和非独占锁。

  • 独占锁:独占锁确保在任何给定时间点只有一个线程可以访问或进入临界区。在C#中,我们可以使用lock关键字、Monitor类、Mutex类、SpinLock类来实现Exclusive Lock。

  • 非排他锁: 非排他锁提供对共享资源的只读访问并限制并发,即限制对共享资源的并发访问数。在 C# 中,我们可以使用 Semaphore、SemaphoreSlim 和 ReaderWriterLockSlim 类来实现非排他锁。

3.加锁

3.1 C#中的lock语句是什么?

按照微软的说法,lock语句获取给定对象的互斥锁,执行一个语句块,然后释放锁。持有锁时,持有锁的线程可以再次获取和释放锁。任何其他线程都被阻止获取锁并等待直到锁被释放。

注意:当你想同步线程访问一个共享资源时,你应该将共享资源锁定在一个专用的对象实例上(例如,private readonly object _lockObject = new object();或private static readonly object _lockObject = new object() ; ). 避免对不同的共享资源使用相同的锁对象实例,因为这可能会导致死锁。

3.2 C# 中的 lock 语句在内部是如何工作的?

当我们编译代码时,C# 中的 lock 语句在内部转换为 try-finally 块。锁定语句的编译代码如下所示。可以看到,它在内部使用了Monitor类的Enter和Exit方法。在后面文章中,我们将详细讨论Monitor 类的 Enter 和 Exit 方法,现在为了理解,我们可以说的是,它通过调用 Monitor 类的 Enter 方法在 try 块中获取独占锁并在 finally 块中释放获得的独占锁。

3.3 自增实例

我们看一个例子,假设三个线程给一个int变量增加:

    internal class Increment
    {
        static int Count = 0;

        private static readonly object Lock = new object();
        public static void Add()
        {
            Thread t1 = new Thread(IncrementCount);
            Thread t2 = new Thread(IncrementCount);
            Thread t3 = new Thread(IncrementCount);
            t1.Start();
            t2.Start();
            t3.Start();

            Console.WriteLine($"Count: {Count}");
            Console.Read();
        }

        static void IncrementCount()
        {
            for(int i = 0; i < 100000; i++)
            {
                Count++;
            }
        }
    }

理想值应该是300000,但是每次运行都到不了300000,说明打印结果时,三个线程还没运行结束,所以我们需要保证三个线程先运行结束,Thread.Join函数就是强制线程结束后再运行后面的代码。所以Add函数改为:

        public static void Add()
        {
            Thread t1 = new Thread(IncrementCount);
            Thread t2 = new Thread(IncrementCount);
            Thread t3 = new Thread(IncrementCount);
            t1.Start();
            t2.Start();
            t3.Start();
            //Wait for all three threads to complete their execution
            t1.Join();
            t2.Join();
            t3.Join();
            Console.WriteLine($"Count: {Count}");
            Console.Read();
        }

再次运行,你会发现还是无法变成稳定的值,其实经过前面的介绍,应该很容易分析,由于没有加锁,那么当Count=99时,可能同时被2个线程获取,理论上2个线程各加了1,应该为101,但实际上,在做加法后,给Count复值写入时,均写的是100,这样无疑就漏了一次。可以想象,假设我开辟100个线程,那这种情况就会出现相当多次,最终结果必然是小于串行运行的结果。

所以我们给InCrementLock函数加锁:

这里我们给关键代码加锁Lock,让大家看看效果如何:

        public static void Print(object? name)
        {
            lock(locker)  //如果不加锁,则显示不会一致
            {
                Console.Write("[Today is raining day! ");
                Thread.Sleep(10);
                Console.WriteLine($"--- What do you do? : {(name as string)}]");
            }
        }

其它部分的代码不变,运行结果为:

[Today is raining day! --- What do you do? : 张总]
[Today is raining day! --- What do you do? : 王总]
[Today is raining day! --- What do you do? : 刘总]

现在看起来整洁多了,但是一个lock真能解决我们上述所有需求吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/155989.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mybatis-plus 使用 typeHandler 将 String 拼接字符串转换为 List 列表

一、需求描述 首先说明需求&#xff0c;有三张表&#xff1a; 学生表、角色表、以及一张关联的中间表。 学生可以有多个角色&#xff0c;但是这多个角色我是作为多条记录存储在另外一张表中的&#xff0c;现在想将这多条记录查询出来&#xff0c;注入到Student对象中的一个L…

微服务之JVM调优

一、Xms Xmx Xss等定义及功能 1.Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。 2.Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异…

易基因|14种全基因组DNA甲基化测序(WGBS)标准分析比对软件的比较| 生信专区

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。全基因组重亚硫酸盐测序&#xff08;WGBS&#xff09;是甲基化研究的重要技术。尽管已经开发了一系列工具来解决由亚硫酸盐处理引起的比对问题&#xff0c;但尚未对最新可用工具的reads比对…

HashMap,Hashtable,ConcurrentHashMap

目录 一、多线程使用HashMap的一些线程安全问题 ①造成数据新增丢失 ②扩容时候&#xff0c;造成链表成环 二、Hashtable和HashMap的区别 ①核心方法加锁 ②其他语法上面的略微差异 三、引入ConcurrentHashMap【重要】 ①ConcurrentHashMap相比于Hashtable的优势 Hashtab…

著名相声艺术家侯耀华,77岁寿宴现场曝光,郭德纲师哥前去祝贺

在中国的相声界&#xff0c;有一条不成文的规定&#xff0c;关于著名相声表演艺术家的判定&#xff0c;从来不是以相声水平高低为标准。只要你有足够长的寿命&#xff0c;只要你能把其他人都熬走熬败&#xff0c;就算你是一个相声小白&#xff0c;也能摇身一变成为艺术家。 不过…

Git介绍与使用

1.集中式版本控制 svn 中央服务器 所有的版本数据都存在服务器上&#xff0c;用户本地只有自己所同步的版本&#xff0c;如果不联网的话&#xff0c;用户就看不到 SVN是集中式版本控制系统&#xff0c;版本库是集中放在中央服务器的 而工作的时候,用的都是自己的电脑,所以首先…

跨境电商物流系统功能框架

随着国内互联网巨头们逐渐将更多注意力投向了跨境电商市场&#xff0c;电商巨头出海也在掀起新的发展高潮。下面是跨境电商物流系统功能框架&#xff0c;供大家参考1、OMS叫做订单管理系统&#xff08;Order Management System&#xff09;&#xff0c;在不同公司&#xff0c;不…

云原生时代的运维体系进化

云原生已经成为数字经济技术的创新基石&#xff0c;并且正在深刻地改变企业上云和用云的方式。云原生的用云方式可以帮助企业最大化获得云价值&#xff0c;也给企业的计算基础设施、应用架构、组织文化和研发流程带来新一轮变革。而业务和技术挑战也催生了新一代云原生运维技术…

设计模式(一)----设计模式概述及UML图解析

1、设计模式概述 1.1 软件设计模式的产生背景 "设计模式"最初并不是出现在软件设计中&#xff0c;而是被用于建筑领域的设计中。 1977年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫亚历山大&#xff08;Christopher Alexander&#xf…

Golang开发 02

文章目录一、Golang开发工具二、visual studio code安装(VS code)1、安装window2、安装mac、linux一、Golang开发工具 # 1、Visual studio code &#xff08;常用&#xff09; # 2、Sublime Text(免费) # 3、Vim # 4、Emacs # 5、Eclipes IDE工具&#xff0c;开源免费&#xf…

数据分析-深度学习PytorchDay1

深度学习框架pytorch学习(一)准备环境准备环境一、深度学习框架简介二、Tensorflow与Pytorch的比较三、安装开发环境一、深度学习框架简介1、Google阵营最早的是由加拿大团队开发的theano一个机器学习库&#xff0c;现在已经停止更新。接着Google开发了Tensorflow&#xff0c;并…

【机器学习知识点】3. 目标检测任务中如何在图片上的目标位置绘制边界框

目录前言导入图片定义边界框绘制函数在图片中绘制边界框总结前言 在图像分类任务中&#xff0c;很多时候我们不仅要知道图像中目标的类别&#xff0c;而且还想知道它们在图像中的具体位置。在计算机视觉里&#xff0c;这类任务被称为目标检测&#xff08;object detection&…

uniapp开发技术

目录 1、js 判断iPhone|iPad|iPod|iOS|Android客户端 2、js实现防抖 3、 js实现节流 4、 页面在弹窗时禁止底部页面滚动&#xff08;h5端&#xff09; touchmove.stop.prevent 5、scrollIntoView 1、js 判断iPhone|iPad|iPod|iOS|Android客户端 // fullScreen代表整个页面…

【C++】STL---list的模拟实现

目录前言一、list和vector的区别二、节点的定义三、list类定义四、push_back函数五、push_front函数六、迭代器七、begin和end函数八、迭代器区间初始化九、迭代器的操作符重载操作符重载操作符- -重载操作符&#xff01;重载操作符重载操作符*重载十、insert函数十一、erase函…

如何应用卫星图像插入到Auto CAD

如何应用卫星图像插入到Auto CAD发布时间&#xff1a;2018-01-17 版权&#xff1a;工具准备BIGEMAP GIS Office&#xff1a;http://www.bigemap.com/reader/download/案例&#xff1a;等高线完美套合卫星影像教程本实例使用AutoCAD2008软件进行影像与矢量数据叠加配准。影像获取…

变压器和特斯拉线圈

目录 变压器用途 变压器的原理 变压器特点 特斯拉线圈用途 特斯拉线圈原理 特斯拉线圈特点 参考&#xff1a; 变压器用途 电压变换、电流变换、阻抗变换、隔离、稳压等 1&#xff09;开关电源&#xff0c;充电器&#xff0c;220v转换为指定电压&#xff0c;以给各类电子…

Revit建模幕墙问题:幕墙添加门/窗和生成幕墙

一、Revit中如何在幕墙当中添加门、窗构件 今天跟大家分享一下幕墙当中添加门窗的方法&#xff0c;这种方法大家可以联想到很多应用上&#xff0c;因为这个既是个方法也是个技巧&#xff0c;好了&#xff0c;我们直接进入主题吧。 首先&#xff0c;我们新建幕墙&#xff0c;给它…

范数的意义与计算方法

1. 范数的意义 范数可以简单的理解为“距离”。由于向量是既有大小又有方向的量&#xff0c;所以向量是不能直接比较大小的&#xff0c;但是范数提供了一种方法&#xff0c;可以将所有的向量转化为一个实数&#xff0c;然后就可以比较向量的大小了。&#xff08;注&#xff1a…

【计算机视觉】Pooling层的作用以及如何进行反向传播

问题 CNN网络在反向传播中需要逐层向前求梯度,然而pooling层没有可学习的参数,那它是如何进行反向传播的呢? 此外,CNN中为什么要加pooling层,它的作用是什么? Pooling层 CNN一般采用average pooling或max pooling来进行池化操作,而池化操作会改变feature map的大小,…

swagger的使用与步骤

1、导入maven工程首先我们创建一个 Spring Boot 项目&#xff0c;并引入 Swagger3 的核心依赖包&#xff0c;如下&#xff1a;<dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.…