多线程的概念-》历史,定义,线程安全的类

参考

陈硕 Linux多线程服务编程
java并发编程实战

多线程的历史

  1. 早期单核裸机CPU

    程序只能顺序执行,一次只能运行一个程序,造成的就是资源浪费和难编写代码

  2. 单核CPU + 操作系统(进程)

    1. 操作系统的出现让计算机可以运行多个程序,这也就是并发。操作系统引入进程的概念,让每个程序在自己进程中进行执行,并且为了通信,引入粗颗粒的进程通信方式(共享内存、管道、信号、消息队列等),操作系统使用时间片轮转的方式,给每个进程分配程序运行时间,看起来就好像程序都在同一时刻执行一样

    2. 操作系统的出现大大提高了资源利用率以及程序编写的便捷,比如中断概念(在主进程执行的时候可以被其他进程打断,打断处理后再回来做主进程的事情),还有那种I/O耗时操作,把当前资源释放,让其他进程使用

  3. 单核CPU + 操作系统 (进程 + 线程)

    1. 进程的出现极大便利了我们,但是注意的是进程间的通信以及进程的分割创建是很费时间,有的时候我们想直接一个进程内实现程序本身的职责 ,但是内部又想分来几个模块协同合作,比如一款windows的应用,一般都是一个进程实现的,但是可以实现点击UI,其他模块仍可以工作的目的。基于以上种种考虑,线程就出现了。

    2. 主打:轻量级,程序执行的最小单元,一个线程类似一个可执行模块一样

    3. 在一个进程内的线程之间共享进程的进程资源,比如内存句柄、文件句柄,堆空间等。但是每个线程有自己独自的栈、局部变量、程序计数器。由于有了线程,那么线程之间的通信也是不可避免

  4. 多核CPU + 操作系统 (进程 + 线程)

    1. 在多核CPU中,并行的概念就出现了

    2. 线程提供直观的模式来充分利用多处理器系统的硬件并行性,在同一个程序的多个线程也可以被调度到多个CPU运行

    3. 真正的并行出现了,可以一边烧水一边吃饭

  5. 题外:

    对于linux系统,进程和线程都是对资源的一次分配使用

并发和并行

  1. 从硬件角度来说,并发发生在单核CPU,并行发生在多核CPU

  2. 从程序流程角度讲:并发使用时间片形式给每个线程分配空间,就可以实现类似一个I/O阻塞,另一个线程可以干其他事情;

    并行就是两个以上线程可以同时做事情,既可以一个线程读I/O,又可以另一个线程干其他事情。

  3. 并发的时候 各个线程是对资源进行抢占的,并行不是–》资源很多大家一起用

线程的优势

​ 当今,最火的架构莫过于分布式架构了

​ 但是你有没有感觉,多核–》多个服务器,多线程–》多个服务运行

​ 多线程就是分布式架构基础啊

1. 发挥了多处理器的能力
2. 程序编写上:将不同类型的任务分配不同的线程,将程序执行逻辑、调度细节、交替执行的细节分离开,一个线程就是提供一种服务,每个线程**各自运行**,在关键点进行必要的**同步**
3. 异步事件的快速响应

线程的风险

  1. 安全性问题:

    1. 由于线程之间没有同步,当多个线程对共享资源进行读写的时候,顺序、结果是未知的
    2. 可见性:由于编译器的优化,变量的读取可能不会从内存中独缺,编译器把寄存器缓存,在需要的时候直接从寄存器读取,导致变量 在另一个线程修改了,但是当前线程读取的是旧值
    3. 顺序性:在0101的底层,处理器、编译器会疯狂的对代码 可执行文件进行优化,让两个语句之间交换顺序等–》最常见的就是单例模式的 new对象时候的操作,c++11之前需要内存屏障,c++11有了内存模型,对象会直接得到内存返回
    4. 原子性:对共享资源的更改只要不是原子性(一步汇编指令),都可能穿插执行操作,造成程序异常失败或者状态不正确
  2. 活跃性

    当我们加锁后,注意死锁、活锁

  3. 性能问题

    1. 线程调度器会让当前活跃线程挂起开启新的线程,多个线程之间发生切换、同步,让CPU上下文频繁切换,把时间花在调度上,保存和回复上下文

    2. 正因为这样,才有了无锁和有锁两种同步策略

线程的作用域

  1. 线程的普遍问题就是线程安全
  2. 我们都知道使用框架,但是框架中也可能有线程,框架中的回调函数需要我们保证函数内的线程安全
  3. 比如一个Time类,我们需要任务在稍后运行之类的,到时间后任务是在Time管理的线程运行的,那么!!这个线程就不是我们控制的,我们为了线程安全性,就需要在任务访问的对象是线程安全的

什么是线程安全的类

  1. 多个线程同时调用该类,仍表现正常行为

  2. 对象始终保持正确的状态–>要不没有值,要不每个成员都赋值,不会出现一半赋值的情况

    操作系统 会在某个时间点停止当前线程执行另一个线程,那么不论线程之间的执行顺序是什么样的,类仍是正确的

  3. 调用者不需要额外的同步或者其他协调工作,类内部封装了必要的同步机制,C++的Vector不行,虽然STL考虑的是效率

术语:竟态条件

  1. 就是函数内是先check条件再执行代码的,在多线程存在问题就是多个线程基于失效的条件来进行计算

    常见的单例模式,内部先 if == null, 再执行,这在多线程中可能对象已经不是null,条件失效了,多个线程并发顺次创建多个对象

  2. 还有大名鼎鼎的 i++

    这中间分了三个步骤 读取i、值增加、写入回i,每一个都是条件

常见的解决办法

​ 将需要同步的代码进行加锁/原子变量,让操作变成原子形式执行,并行的程序到这里变成了串行

注意点

​ 需要注意的是为了效率,锁一定要在尽量小的作用域内

​ 如果程序会执行较长时间的计算和运行,比如I/O操作,那一定不要持有锁

​ 代码同步简单带来性能,过大的同步(原子类 + 锁)也会带来复杂和安全性,需要平衡

多线程如何安全创建对象

​ 不要在构造函数中 把this 传给跨线程的对象 !!!

​ 因为对象是在构造函数后才构建好的,一旦把this传给外部很可能导致跨线程对象 调用 一个不完整的对象导致出错

​ 解决办法就是 :定义额外的register,在当前对象构造结束后,手动调用注册函数进行this传递

1. 构造函数进行 外界对象 保存
2. register函数里面 对跨线程的外界对象 this传递
3. **使用工厂方式 newinstance,在newinstance中进行   source.register**

多线程如何安全销毁对象

  1. 原始指针 无法根据地址是否为Null或者有效 判断对象是否被析构 –》java中只要地址有值就是有效的
  2. mutex 数据成员本身就是 对象的一部分,当析构发生 不确定mutex是否被析构
  3. 多线程调用的时候存在,线程析构一半的这种不确定性

安全的做法是:shared_ptr

shared_ptr使用指针包容器的方式提供安全的访问指针的方式,跨线程对象 可以安全的使用和销毁

保证线程安全的几个方式

  1. 线程封闭

    1. 栈封闭

      函数内使用栈上的对象,每个栈的空间都是私有的

    2. ThreadLocal

      将变量和线程关联,带来的效果就是 每个线程访问的都是 变量的副本,当修改的时候会不影响

  2. 不可变对象

    让对象内部的内部状态不会更改,多线程访问就存在只读权限,那么就不会有问题了

    需要满足下面的条件:

    1. 对象创建后就不能修改

    2. 对象的所有域都是const类型

    3. 对象是正确创建的,对象创建期间 this没有溢出

    如果想修改一下不可变对象呢:直接重新new整体

    注意不可变对象就是把对象看作一个整体了

  3. 事实不可变对象

    对象内部不是const的,但是对象创建后,不会被修改,这样的对象只需要保证 创建的时候对其他线程的可见性就好了

  4. 加锁

    将可变对象 使用 内部锁方式,函数进行必要的加锁,保证内部状态的同步

posted @ 2021-06-26 23:01  make_wheels  阅读(117)  评论(0编辑  收藏  举报