首页 > 编程语言 >并发编程 - 线程同步(一)

并发编程 - 线程同步(一)

时间:2025-01-23 21:53:15浏览次数:1  
标签:同步 编程 共享资源 并发 线程 内核 var 多线程

经过前面对线程的尝试使用,我们对线程的了解又进一步加深了。今天我们继续来深入学习线程的新知识 —— 线程同步。

01、什么是线程同步

线程同步是指在多线程环境下,确保多个线程在同时使用共享资源时不会发生冲突或数据不一致问题的技术,保证线程间的正确协作。它的目的是使得多个线程在执行过程中能够按照某种顺序、安全地使用共享资源。

02、为何需要线程同步

1、避免竞争条件

不知道大家还记得在《并发编程 - 初识线程》中出现的关键字volatile和特性ThreadStatic吗?它们都是为了解决多线程共享资源问题。

在多线程中当多个线程需要同时使用共享资源时,很容易产生互相竞争资源使用权的情况,这一问题也叫竞争条件。此时就可以通过线程同步技术实现多个线程按顺序使用共享资源,从而避免竞争条件。

2、保证共享资源安全

我们举个简单的例子,假如我的银行账户里有1000元,此时我正在用电子银行在线上操作准备向我老婆的账户里转账100元,而恰巧此时我老婆拿着我的银行卡准备取款500。

假如银行系统还是一个只有多线程,没有线程同步功能的老系统,在这一前置条件下。假如恰巧我们俩在同一瞬间点了确认操作,相信此时系统会发生什么?

有可能会是系统同时收到我们俩的请求,此时我的操作线程A,首先读取我账户余额1000,然后执行转账操作把余额减100得到900,再更新至余额中。而我老婆的操作线程B因为是和我同时的,所以在读取我账户余额的时候得到的也是1000,而不是900,此时线程B执行取款500操作把余额减500得到500,再更新至余额中。

可以发现我们俩最后更新余额,无论谁更新成功最后结果都是不正确的。这个例子就导致银行账户余额最终不正确,也就是我们说的共享资源不安全。如果使用线程同步,使得线程A、B可以按顺序执行,无论谁先执行最终结果都会是正确的。

下面我们再来结合代码举一个经典问题 —— torn read

先解释一下什么叫torn read,可以翻译成一次读取被撕成两半。或者说在机器级别上,要分两个MOV指令才能读完。

具体来说就是一个long类型变量_var,当一个线程把_var赋值为0x0123456789ABCDEF,而此时另一个线程来读取_var,结果读取的值是0x0123456700000000或0x0000000089ABCDEF。这同样是因为多线程导致的共享资源不安全问题。

下面看看模拟代码实现效果:

public class ThreadSync
{
    //共享的int64变量
    public static long _var;  
    public static void Run()
    {
        //启动写入线程
        var writerThread = new Thread(WriteToSharedValue);
        //启动读取线程
        var readerThread = new Thread(ReadFromSharedValue);
        //启动线程
        writerThread.Start();
        readerThread.Start();
        //等待线程执行完成
        writerThread.Join();
        readerThread.Join();
    }
    //写入线程
    static void WriteToSharedValue()
    {
        //模拟分两步写入
        long high = 0x01234567;
        long low = 0x89ABCDEF;
        unsafe
        {
            //将 _var 分成高低两部分写入
            //写高 32 位
            _var = high << 32;
            // 确保读取线程能在这里读取中间值
            Thread.Sleep(0);  
            //写低 32 位
            _var |= low;
        }
        Console.WriteLine($"写: 写入值 0x{_var:X16}");
    }
    //读取线程
    static void ReadFromSharedValue()
    {
        // 读取共享变量的值
        Console.WriteLine($"读: 读取值 0x{_var:X16}");
    }
}

我们看下执行效果:

当然上面的例子并不是每次都会出现的,可能需要多运行几次,另外关于写入线程为什么不是直接赋值而是把值拆成高低位分两次写入?

这是因为我的电脑是64位系统,在大多数现代的 x64 系统架构(例如 Intel 和 AMD 处理器)上,64 位的原子性操作通常是被保证的。即使对于像 long(64 位)这种数据类型,处理器通常会在硬件层面确保它的读写操作是原子性的,因此,不太容易发生撕裂的读(torn read)。

所以这里的代码把一次赋值行为认为拆解成两步,同时Thread.Sleep(0)也为了让当前线程主动让出 CPU 时间片,使读线程有机会读取,使其更贴近在x32环境下运行的情况。如果有条件可以用直接赋值再x32环境下看看效果。

03、如何实现线程同步

1、避免资源共享

当然严格意义上说可能这一条不算是线程同步,只能说解决了多线程碰到的问题,达到线程同步的效果。

如果没有共享资源,那么自然就无须进行线程同步。大多数时候可以通过重新设计程序来除移共享状态,从而去掉复杂的同步构造。尽可能避免在多个线程间使用单一对象。

除了通过重新设计来移除共享状态,还可以通过语言特性设计使其达到无共享状态。比如值类型在传递过程中总是被复制,每个线程都会有自己的数据副本,比如看下面这个方法:

public static int Max(int val1, int val2)
{ 
    return val1 > val2 ? val1 : val2;
}

即使这个方法没有使用任何线程同步方法,这个方法也是线程安全的。因为值类型特性原因,所以传给Max的两个int值会复制到方法内部,形成自己的数据副本。此时无论有多少个线程调用Max方法,每个线程处理的都是它自己的数据,线程之间并不会互相干扰。

2、用户模式同步机制

用户模式同步机制指在用户空间内完成线程的阻塞和唤醒操作,由程序自己管理同步对象的一种同步方式,因为不涉及与操作系统内核交换,因此开销较低,更轻量级。

实现方式有SpinLock、SpinWait、Monitor(lock)等。

3、内核模式同步机制

内核模式同步机制是指在操作系统内核空间就完成线程的挂起与恢复,由操作系统管理同步对象的一种同步方式,因为每次线程同步操作都需要操作系统参与,因此必然回涉及内核态的上下文切换,同时还是涉及到操作系统内部的数据结构和资源管理,因此内核模式同步机制往往会导致较高的开销。

实现方式有Semaphore、Mutex、AutoResetEvent等。

4、混合模式同步机制

混合模式同步机制在某些情况下会根据线程竞争的情况在用户模式和内核模式之间切换。通常,当资源访问冲突较小或线程阻塞较少时,采用用户模式同步;当资源争用较多或有较大的线程等待时,自动切换到内核模式同步。

实现方式有SemaphoreSlim、ManualResetEventSlim、CountDownEvent、Barrier、ReaderWriterLockSlim等。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

标签:同步,编程,共享资源,并发,线程,内核,var,多线程
From: https://www.cnblogs.com/hugogoos/p/18688670

相关文章

  • 第五章java面向对象编程(上)
    面向对象编程Java的核心就是OOP初始面向对象面向过程思想:步骤思维清晰,第一步做什么,第二步做什么…。适合解决一些简单的问题面向对象思想:物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行独立思考。最后,才对某个分类下的细节进行面向过程的......
  • 掌握Spring事务隔离级别,提升并发处理能力
    Spring框架支持的事务隔离级别与标准的JDBC隔离级别保持一致,共包括五大隔离级别,它们分别是:DEFAULT(默认隔离级别)、READ_UNCOMMITTED(读未提交)、READ_COMMITTED(读已提交)、REPEATABLE_READ(可重复读)和SERIALIZABLE(可串行化)。这些隔离级别在数据一致性和并发性能之间进行了权衡,以满足......
  • Vue编程式路由跳转多次执行报错
    文章目录问题描述路由跳转的两种形式问题原因解决方案1.传递回调函数2.重写`push`方法(推荐)代码实现细节解析1.为什么不能直接使用`this.originalPush()`?2.`this`的指向是什么?3.`call(this)`的作用4.异常捕获的意义其他注意事项总结问题描述在使用Vue......
  • 如何打造一个高并发系统?
    今天和大家聊聊作为一个后端开发,在实际工作中,我们如何打造一个高并发的系统?如下图所示,大概有六个层面,我们结合具体的场景直播间签到去一一细说。一、前端1、打散请求:即把用户的接口分散一点去请求后端,尽量不要集中在某一时刻。场景:比如直播间讲师发起了一个签到,用户去点击签......
  • 编程界“华山论剑”:PHP与Go,谁主沉浮?
    在编程的广阔天地里,选择一门合适的编程语言就如同为一场冒险挑选趁手的武器,至关重要却又常常令人纠结。当我们面对PHP与Go这两种备受瞩目的编程语言时,这种纠结愈发明显:PHP,作为Web开发领域的老牌劲旅,拥有着庞大的开发者社区和丰富的框架资源,能轻松搭建起功能强大的动态......
  • 详细剖析Java动态线程池的扩容以及缩容操作
    前言在项目中,我们经常会使用到线程来处理加快我们的任务。但为了节约资源,大多数程序员都会把线程进行池化,使用线程池来更好的支持我们的业务。Java线程池ThreadPoolExecutor有几个比较核心的参数,如corePoolSize、maximumPoolSize等等。无论是在工作中还是在面试中,都会被问到,如何......
  • 从入门到起飞:一线大厂前端工程师的AI编程进化论 - 提升10倍生产力的秘密!!!
    0.前言(求......
  • Golang笔记——静态强类型、编译型、并发型语言
    大家好,这里是GoodNote,关注公主号:Goodnote,专栏文章私信限时Free。本文详细介绍Go语言的基础知识,包括数据类型,深浅拷贝,编程范式,Go语言是一种静态(静态类型语言和静态语言)强类型、编译型、并发型,并具有垃圾回收功能的编程语言。文章目录1.Go语言基础知识数据类型......
  • 【Spring Boot编程】Spring Boot实现防盗链功能详解
    前言在当今的互联网环境中,资源的保护愈发重要。防盗链作为一种保护网站资源不被其他网站随意盗用的手段,显得尤为关键。本文将详细介绍如何在SpringBoot项目中实现防盗链功能。什么是防盗链防盗链是指防止未经授权的网站通过链接直接访问本网站的资源,比如图片、视频、文件等。......
  • AI编程助手文心快码插件结合IDEA辅助编程
    文心快码介绍文心快码(BaiduComate)是由百度基于文心大模型研发的编程辅助工具,可提供自动代码生成、单元测试生成、注释生成以及智能问答等功能。支持上百种编程语言,旨在帮助开发者大幅提升编码效率。使用Comate,让编程更加高效和便捷 文心快码的核心优势在于其广泛的代码理......