2024年最全线程安全(thread-safe)介绍,2024年最新2024年C C++面试心得

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

线程安全问题都是由全局变量静态变量引起的。如果每个线程中对全局变量或静态变量只有读操作,而无写操作,一般来说,这个全局变量或静态变量是线程安全的;如果有多个线程同时对全局变量或静态变量执行写操作,则一般都需要考虑线程同步,否则就可能影响线程安全。

1.2 类的线程安全

线程安全的类,首先必须在单线程环境中有**正确行为:**如果一个类的实现正确(即符合规格说明),那么对这个类的对象的任何操作序列(读或写公共字段以及调用公共方法),都不会让该对象处于无效状态、或者违反类的任何不可变量、前置条件或者后置条件的情况。

此外,一个类要成为线程安全的,在被多个线程同时访问时,不管这些线程是怎样的时序安排或者交错,该类必须仍然具备上述的正确行为,并且在调用代码中不需要进行任何额外的同步。其效果是,在所有线程看来,对于线程安全对象的操作是以固定的、全局一致的顺序发生的。

1.3 线程不安全的示例

1.3.1 示例1

比如一个 ArrayList 类,在添加一个元素的时候,包括两个步骤:

  1. 在 Items[Size] 的位置存放此元素;

  2. 增大 Size 的值。

在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而 Size=1。该类功能正常,数组变化与预期一致。

在多线程情况下,比如有两个线程(A 和 B),线程 A 先将元素存放在位置 0。但是此时经 CPU 调度,线程 A 暂停(挂起),线程 B 开始运行,线程 B 也向 ArrayList 类的 Items 中添加元素,由于此时线程 B 获取的 Size 仍然为 0(因为前面线程 A 仅仅完成了存放操作,尚未进行增大 Size 值操作),所以线程 B 也将元素存放在位置 0,之后线程 A 和线程 B 继续运行,(无论 CPU 如何调度)线程 A 和线程 B 都会增加 Size 的值,导致 Size 值变为 2,然后流程结束。

此时,Items 中的元素数量实际上只有一个,存放在位置 0,而 Size 却等于 2 了,这就出现了“线程不安全”现象了。

1.3.2 示例2

有变量 counter = 0,counter 在两线程 A 和 B 之间共享。假设线程 A、线程 B 同时对 counter 进行递增计算(counter++),那么正常情况下,计算后的结果应该是 2,但是在“线程不安全”的情况下,实际的结果却可能是 1。

具体流程与示例 1 类似,递增计算一定要分为多步(本例为三步)进行,步骤如下:

  1. 先获取 counter 的值:

  2. 对 counter 进行递增计算;

  3. 将计算后的结果重新赋值给 counter。

从 CPU 调度角度来看,线程 A 执行上述第 1、2 步之后,CPU 转而去执行线程 B,当线程 B 也执行了上述第1、2步之后,无论后续 CPU 如何调度,最终计算出来的 counter 都是 1(线程 A 的计算结果覆盖线程 B,或者相反),这样就出现了“线程不安全”情况。

1.4 How

通过前面的讲述,我们已经知道了线程安全的原理,那么应该如何解决多线程并发情况下的线程安全问题呢?

通常可以通过使用锁(也称互斥锁,mutex),来保证线程安全。

有些文章在多线程编程中引入了“原子性”、“可见性”、“顺序性”概念,然后以保证这三者正常实现的方式来保证线程安全,这些方案中包括了使用锁来保证这三者的正常实现,即,使用锁来保证线程安全性。

锁能使其保护的代码路径以串行形式被访问,因此可以通过锁可以实现:(对变量操作的)原子性、(完整的变量操作之后才释放锁,保证其他线程对于该变量操作后结果的)可见性、(锁之内的代码全部执行完成之前,其他线程不能进入该代码逻辑,进行变量修改操作)顺序性。

2 示例程序

在这里提供一个示例程序,该程序会创建两个线程,这两个线程执行同一段代码,该段代码会对全局变量 count 进行修改(加5000次)并打印 count 的值。正常情况下,最终的 count 值应该为 10000。

现在分别在线程不安全和线程安全场景下,编写上述程序。

2.1 线程不安全代码

线程不安全代码(thread_unsafe_test1.cpp)的内容如下:

#include <iostream>
#include <pthread.h>

using namespace std;

// 定义全局变量count
int count = 1;

void* run(void * arg1)
{
    int i = 0;

    while (1)
    {
        // 定义在此处时,是线程不安全的
        int val = count;
        // 在取值和赋值之间,增加打印操作
        cout << "count is: " << count << endl;
        // 定义在此处时,是线程安全的
        //int val = count;
        count = val + 1;

        i++;

        if (5000 == i)
        {
            break;
        }
    }
}

int main()
{
    pthread_t tid1, tid2;
    // 创建线程tid1和tid2,线程的运行函数为run
    pthread_create(&tid1, NULL, run, NULL);
    pthread_create(&tid2, NULL, run, NULL);

    // 等待线程结束,回收线程资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    return 0;
}

编译并执行上述代码,(部分)运行结果可能如下:

关于上述示例程序,说明如下:

  1. 由于 CPU 调度的不确定性,上述示例程序每次的执行结果都可能会不同,即最终的 count 值为不确定值;
  2. 在上述代码中,在读取 count 值(int val = count;)和把新值赋给 count(count = val + 1;)之间,插入一个打印操作(cout << "count is: " << count << endl;),该操作会调用 write 系统调用,此时(程序)会从用户态进入内核态,为内核调度别的线程(执行)提供了一个很好的时机,所以就可能会导致线程的并发运行(CPU 在线程间来回切换),进而产生线程不安全的问题;
  3. 通过程序的打印结果,正面证实了上述第 1 点说明结论。另外,为了从另一角度证实第 1 点说明结论,我们将打印操作从“读取与赋值语句之间”移动到“赋值语句之后”,再次编译执行程序,就没有出现线程不安全的问题,即最终的 count 值为 10000;

通过上述几点说明,可以得出一种好的编程方法(或者说编程习惯):代码需要尽量内聚,这里不仅仅是针对代码模块,更多的是针对代码行与行之间逻辑。就本例来说,需要把“打印操作”放到“取值与赋值操作”之前或之后,这样就保证了“取值与赋值操作”的内聚性(虽然仅仅是两行代码),而这样的操作同时也(误打误撞地)实现了线程安全线,尽管我们的初衷并不是为了解决线程不安全的问题。可以说,提高代码内聚性,确实好处多多!

2.2 线程安全代码

针对 2.1 节 中遇到的线程不安全问题,通过锁机制来实现线程安全。

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值