多线程编程时一种比多线程编程更灵活、高效的并发编程关系
go并发编程模型在底层使用的是内核提供的posix线程库

线程
pthread_create

线程的标识

  • 每个线程都有自己的ID,这个ID也叫做线程ID或者PID
  • 线程ID只需要保证在所属进程的范围内唯一即可
  • Linux系统的线程实现了确保每个线程ID在系统范围内是唯一的,当线程不存在了,其线程ID可以被其他线程复用
  • 线程ID是由操作系统内核分配和维护的,应用程序一般无需关注

线程间的控制

同一个进程中任意两个线程之间的关系都是平等的,任何线程都可以对同一进程中的其他线程进入有限的管理, 比如:

pthread_join流程控制权pthread_joinpthread_detach(别的线程的id)pthread_detachpthread_join

当然,线程可以自己控制自己

  • 终止自己
  • 分离自己
    在这里插入图片描述

线程的调度

  • 调度器会把时间划分成非常小的时间片并且把这些时间片分配给不同的线程,然后快速的切片,从而实现并发效果

  • 线程的执行总是处于CPU受限或者IO受限

    • CPU受限: 一般值计算。。。
    • IO受限:从磁盘中读取资料、等待键盘输入。。。
  • 线程有两个优先级:静态优先级和动态优先级

    • 静态优先级:由应用程序决定,决定了线程单次在CPU上运行的最长时间。如果应用程序没有指定的话,默认是0
    • 动态优先级:调度器在静态优先级的基础上实时调整,决定了线程的顺序顺序

所有等待使用CPU的线程会按照动态优先级从高到低的顺序排列,并且依序放到与该CPU对应的运行队列中。因此,下一个运行的线程总是动态优先级最高的那一个。实际上,每一个CPU的运行队列中都包含两个优先级阵列:其中一个用于存放正在等待运行的线程,我们称为”激活的优先级队列“,另一个用于存放已经运行过但是还没有运行完的线程。更确切的讲,优先级阵列式一个由若干个链表组成数组。一个链表只会包含具有相同优先级的线程,而一个线程也只会放到于其优先级相对应的那个链表中。当一个线程放入某个优先级阵列时,它实际上就是放到了与其优先级相对应的那个链表的末尾处。
在这里插入图片描述
下一个运行的线程总是会从激活的优先级阵列中选出。如果调度器发现某个线程已经占用了CPU很长时间(这个时间只会小于或者等于给予该线程的时间片),并且激活的优先级阵列中还有优先级和它相同的线程正在等待运行,那么调度器就会让那个等待的线程在CPU上运行,而被换下的线程会排入过期的优先级阵列。当激活的优先级阵列中没有待运行的线程时,调度器会把这两个优先级阵列的身份互换,也就是之前激活的优先级阵列称为新的过期的优先级阵列,而之前过期的优先级阵列则会成为新的激活的优先级阵列。从而,之前被放入过期的优先级阵列的线程就又有机会运行了。

线程如果因为要等待某个事件发生(键盘输入,硬盘资料读取),就会被放到相应事件的等待队列中,并随即进入睡眠状态。当事件发生或者条件满足时,内核会通知对应的等待队列中的所有线程,这些线程就会被唤醒并从等待队列转移到适当的运行队列中。调度器一般会稍稍调高这些被唤醒的线程的动态优先级。

调度器会尽量使一个线程在一个特定的cpu上运行【维持高速缓存的高命中率、高效使用最近的内存】,这可能导致一些CPU过于忙碌,一个CPU被闲置。在这种情况下,调度器会把一些比较忙碌的CPU上运行的线程迁移到比较空闲的CPU上运行。由于内核会为每个CPU建立一个运行队列,所以这种迁移是很容易的。

线程实现模型

  • . 用户级线程模型:这种程序由应用程序调用用户级别的线程库全权管理的,它位于用户空间中,内核无法调度这些线程,而由程序自发决定创建、终止、切换等操作。
    1. 移植性强,但是不能真正实现并发运行:如果某个线程在IO操作中被阻塞,那么所属进程也会被阻塞。
    2. 在调度器看来,进程是一个无法被分割的调度单元,无论进程里面有多少个线程
    3. 即使计算机上有多个CPU,进程中的多个线程也无法被分配给不同的单元
    4. 这种模型限制基本上已经不用了
    5. 由于包含了多个用户级的线程的进程只与一个KSE对应,因此它也叫做M:1的线程实现
  • 内核级线程模型:应用程序【本质上]是通过调用内核提供的API来创建线程。这些线程完全由内核来管理,包括创建、终止、切换等。
    1. 移植性弱,但是能真正实现并发运行:如果某个线程在IO操作中被阻塞,那么进程和其他线程[与被阻塞的线程不存在同步关系]不会被影响
    2. 资源消耗大,调度速度慢。
    3. 很多操作系统都是给予此种模型实现的,包括Linux系统
    4. 因为进程中的每一个线程斗鱼一个KSE相对应,因此它也叫做1:1的线程实现
  • 两级线程操作
    1. 综合了上面两种模型的优点,也叫做M:N的线程实现
    2. 实现很复杂,一般由编程语言层面提供支持
    3. Go的并发模型和它很像,但是更加优雅。在Go中,不受操作系统内核管理的独立控制流并不叫做线程,加载goroutine
      在这里插入图片描述

线程同步

在这里插入图片描述

互斥量

在同一时刻,只允许一个线程处于临界区之内的约束叫做”互斥(mutex)“。

互斥量有两种可能的状态: 已锁定状态未锁定状态

  • 每个线程进入临界区之前,都必须先锁定某个对象【互斥对象,也叫做互斥量】,只有成功锁定对象的线程才会允许临时进入临界区,否则就会被阻塞。

  • 互斥量每次只能锁定一次,也就是说,处于已锁定状态的互斥量不能再次锁定。任何线程都不能对互斥量进行二次加锁。

  • 线程在离开临界区的时候,必须对相应互斥量解锁,这样,其他因想进入该临界区而被阻塞的线程才会被唤醒并有机会再次尝试锁定该互斥量。

  • 对同一个互斥量的锁定和解锁必须成对出现,否则可能造成严重的后果
    在这里插入图片描述

  • 互斥量属于共享资源,必须能够被所有相关线程访问到,因此互斥量一般不是局部的。

  • 初始化互斥量的操作总是在任何线程使用它之前进行。必须保证该互斥量只被初始化一次,否则可能造成严重的后果

条件变量

  • 条件变量的目的并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据发生变化时,通知其他因此被阻塞的线程。
  • 条件变量在使用前,必须线程创建、初始化[初始化必须保证唯一]、与某个互斥量绑定
  • 条件变量提供的操作有以下三种:
    • 等待通知(wait): 先解锁与该条件变量绑定在一起的那个互斥量,然后阻塞当前线程[也就是讲当前线程加入等待队列],直到被唤醒[当收到该条件变量发来的通知]
      在这里插入图片描述

    • 单发通知(signal):让条件变量向至少一个正在等待它的线程发送通知(至少唤醒一个阻塞线程),以表示某个共享数据已经被改变(这里的signal与操作系统中的signal信号是不一样的)

    • 广播通知(broadcast):唤醒所有正在等待这个互斥锁有关的进程

线程安全再理解
  • 线程安全:就是无论线程以什么样的顺序执行,返回结果但是一样的,这是我们必须要达到的目的
  • 什么是可重入函数
    • 可重入函数:就是线程安全的函数,假如某个进程中所有线程都可以并发的对一个函数进行调用,无论这些线程调度顺序是什么,该函数的执行效果都是一样的,这就叫做可重入函数
      • 不要返回一个共享数据
      • 任何操作了共享数据的代码的函数都是不可重入函数
      • 如果要操作共享数据,我们应该要用临界区等保护起来—》这样它们的最终执行效果是可以预料的。

总结: 多进程之间只能通过一些额外的手段(管道、消息队列、信号量、共享内存区等)传递数据,而多线程之间交换数据很容易,但是多线程之间交换数据可能产生竞态,需要使用一些同步工具(互斥量、条件变量)来保护