线程简介

线程 Thread

线程基础

  • 线程是程序执行流的最小单元
  • 一个线程由线程 ID、当前指令指针 PC、寄存器集合和堆栈组成
  • 一个进程(Process)通常由一个或多个线程组成,各线程之间共享该进程的内存空间(代码段、数据段、堆等)和进程级的资源(打开文件、信号等)
  • 为什么使用线程:
    • 某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行;多线程执行可以有效利用等待时间
    • 多线程可以让一个线程负责交互,另一个线程负责计算
    • 程序逻辑本身要求并发操作
    • 多核 CPU 具备同时执行多个线程的能力
    • 多线程比多进程在数据共享方面效率要高得多

线程的访问权限

  • 线程也拥有私有的存储空间:
    • 线程局部存储 TLS
    • 寄存器
  • 从 C 语言的角度看
    • 以下数据和资源由进程所有,在线程间共享
      • 全局变量
      • 堆上的变量
      • 函数里的静态变量
      • 程序代码,任何线程都有权利读取并执行任何代码
      • 打开的文件,A 线程打开的文件可以由 B 线程读写
    • 以下数据由各个线程私有
      • 局部变量
      • 函数的参数
      • TLS 数据

线程调度 thread schedule

  • 线程调度就是不断在一个处理器上(或处理器的一个核上)切换不同的线程
  • 在线程调度中,线程通常拥有至少三种状态:
    • 运行(running):线程正在执行
    • 就绪(ready):线程可以立即执行,但 CPU 已被占用
    • 等待(waiting):线程正在等待某一事件(I/O 或同步)发生,无法执行
  • 线程状态的转移:
    • 运行中的线程拥有一段可以执行的时间,称为时间片(time slice);当时间片用尽时该进程将进入就绪状态,即运行 --> 就绪
    • 如果在时间片用尽之前线程就开始等待某事件,那么它将进入等待状态,即运行 --> 等待
    • 在一个处于等待状态的线程所等待的事件发生之后,该线程将进入就绪状态,即等待 --> 就绪
    • 每当一个线程离开运行状态时,调度系统就会选择一个其他的就绪线程继续执行,即就绪 --> 运行
    • 上述状态转移的图解如下:

线程状态转移

  • 线程的优先级 priority:
    • 可由用户指定优先级
    • 根据进入等待状态的频繁程度提升或降低优先级
    • 长时间得不到执行而被提升优先级

线程安全

竞争与原子操作

  • 有些操作被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,转去执行别的代码
  • 把单指令的操作称为原子的(atomic),显然原子操作不会被打断

同步和锁 synchronization and lock

  • 为了避免多个线程同时读写同一个数据而产生不可预料的后果,需要将各个线程对同一个数据的访问进行同步
  • 同步就是在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问
  • 同步使得对数据的访问原子化
  • 同步的最常用方式是使用(lock):每个线程在访问数据或使用资源之前先请求获取(acquire)锁,在访问数据或使用资源结束后则释放(release)锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用
信号量 semaphore
  • 数据或资源只能由一个线程独占时,使用二元信号量(binary semaphore);二元信号量只有占用和非占用两种状态,处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,此后其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放
  • 数据或资源可被多个线程共享时,使用多元信号量,简称信号量;一个初始值为 N 的信号量允许 N 个线程并发访问。线程访问资源时会发生如下操作:
    • 首先获取信号量 s
    • 如果 s == 0,说明该数据或资源已经达到最大共享量,于是线程进入等待状态
    • 如果 s > 0,则将信号量的值减 1,即 --s,然后执行线程
    • 线程访问资源结束后,将信号量的值加 1,即 ++s,然后释放信号量
    • 此时 s 必然大于 0,于是唤醒一个等待中的就绪线程
  • 同一个信号量可以被系统中的一个线程获取之后由另一个线程释放
互斥量 mutex
  • 与信号量不同,互斥量要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁
临界区 critical section
  • 临界区是比互斥量更严格的锁
    • 互斥员和信号量在系统的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试阳去获取该锁是合法的
    • 临界区的作用范围仅限于本进程,其他的进程无法获取该锁
读写锁 read-write lock
  • 多个线程可以同时读取一段数据,但假设操作都不是原子型,只要有任何一个线程试图对这个数据进行修改,就必须使用同步手段来避免出错
  • 如果大多数操作都是读操作,只有少量写操作,那么每次都同步会非常低效
  • 读写锁通过两种获取方式解决上述问题:共享获取(shared)和独占获取(exclusive)
    • 当锁处于自由的状态时,可以以任何一种方式获取锁,并将锁置于对应的状态
    • 如果锁处于共享状态,其他线程以共享的方式获取锁仍然会成功,此时这个锁分配给了多个线程
    • 如果其他线程试图以独占的方式获取已经处于共享状态的锁,那么它将必须等待锁被所有的线程释放
    • 如果锁处于独占状态,便会阻止任何其他线程获取该锁
条件变量 conditional variable
  • 对千条件变量,线程可以有两种操作
    • 线程可以等待条件变量,一个条件变量可以被多个线程等待
    • 线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续支持
  • 使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行

函数重入和线程安全

  • 一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行
    • 多个线程同时执行这个函数
    • 函数自身(可能是经过多层调用之后)调用自身
  • 一个函数可重入,表明该函数被重入之后不会产生任何不良后果
  • 一个函数要成为可重入的,必须具有如下几个特点:
    • 不使用任何(局部)静态或全局的非 const 变量
    • 不返回任何指向(局部)静态或全局的非 const 变量的指针
    • 仅依赖于调用方提供的参数
    • 不依赖任何单个资源的锁
    • 不调用任何不可重入的函数
  • 可重入是并发安全的强力保证,一个可重入的函数可以在多线程环境下放心使用

多线程的内部情况

  • 内核线程的并发执行由多处理器或操作系统调度来实现
  • 用户态线程不一定在操作系统内核里对应同等数量的内核线程

一对一模型

  • 一个用户态线程唯一对应一个内核使用的线程
    • 这个模型下用户线程具有和内核线程一致的优点,线程之间的并发是真正的并发
    • 一个线程因为某原因阻塞时,其他线程执行不会受到影响
    • 一对一模型也可以让多线程程序在多处理器的系统上有更好的表现
  • 一般直接使用 API 或系统调用创建的线程均为一对一的线程
  • 一对一线程缺点有两个:
    • 许多操作系统限制了内核线程的数量,因此一对一线程会让用户的线程数量受到限制
    • 许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降

多对一模型

  • 多个用户线程对应一个内核线程,线程之间的切换由用户态的代码来进行
  • 多对一模型的优点在于:
    • 相对于一对一模型,多对一模型的线程切换要快得多
    • 相对于一对一模型,多对一模型可以支持几乎无限制的用户态线程数量
  • 多对一模型的缺点在于:
    • 如果其中一个用户线程阻塞,那么所有的线程都将无法执行,因为此时内核里的线程也随之阻塞了
    • 处理器的增多对多对一模型的线程性能没有明显的帮助

多对多模型

  • 多个用户线程对应少数但多于一个的内核线程
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值