Linux:多线程

目录

1.线程的概念

1.1线程的理解

1.2进程的理解

1.3线程如何看待进程内部的资源?

1.4进程 VS 线程

2.线程的控制

2.1线程的创建

2.2线程的等待

2.3线程的终止

2.4线程ID

2.5线程的分离

3.线程的互斥与同步

3.1相关概念

3.2互斥锁

3.2.1概念理解

3.2.2操作理解

3.2.3互斥锁原理

3.3死锁

3.3.1概念

3.3.2死锁的四个必要条件

3.3.3避免死锁

3.4线程同步

3.4.1同步概念与竞态条件

3.4.2条件变量

3.4.3条件变量的使用

4.生产者消费模型

4.1生产消费者模型的概念

4.2生产消费者模型的意义

5.信号量

5.1信号量概念

5.2信号量的操作

6.代码编写

6.1多线程并行问题

6.2线程的同步

6.3生产消费者模型(阻塞队列)

6.4生产消费者模型(循环队列)

6.5线程池

补充


1.线程的概念

线程在进程内部执行,是操作系统调度的基本单位

1.1线程的理解

Linux:多线程_第1张图片

线程:有一种执行流, 比进程的执行力度更细, 更轻量化, 创建和终止都更轻, 资源占用更少, 调度成本相对较低

1.2进程的理解

Linux:多线程_第2张图片

a.资源角度

用户视角: 内核数据结构 + 代码和数据

内核视角: 承担分配系统资源的基本实体(以进程为单位向系统申请资源)

进程 = 一批内核数据结构 + 一个地址空间 + 页表 + 对应的代码数据块

b.CPU角度

--CPU其实不怎么关心,当前是进程还是线程的概念, 只认PCB~~>CPU调用的基本单位"线程"

--LInux下: PCB <= 其它OS内的PCB ,Linux下的进程, 统一称之为轻量级进程 (CPU拿到的PCB可能是一个独立的进程,也可能是有多个执行流进程的某个PCB)

c.Linux没有真正意义上的线程结构, Linux是用进程的PCB模拟线程的

~~>Linux并不能直接提供给我们线程的接口, 只能提供轻量级进程的接口

~~>在用户层实现了一套多线程方案, 以库的方式提供给用户进行使用(pthread线程库--原生线程库)

1.3线程如何看待进程内部的资源?

a.大部分资源都是共享的, 但寄存器和栈是私有的(重要)

b.进程内的资源一旦释放了, 整个线程也就跟着退出了(线程向进程申请资源)

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程特有数据:
    • 线程ID
    • 一组寄存器(线程的上下文, 线程是调度的基本单位,一个线程被调度一定会形成自己的上下文 ~~>线程需要有自己私有的上下文)
    • 栈 (每个线程在运行的时候要调用不同的函数来完成它的功能, 需要入栈出栈, 形成的临时变量需要保存到栈里面~~>每一个线程需要有自己的私有栈) ~~>线程的动态属性,其运行不会和其它线程互相干扰,它的临时数据是会压在自己的栈上的
    • errno
    • 信号屏蔽字
    • 调度优先级
  • 共享的数据:
    • 文件描述符表
    • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
    • 当前工作目录
    • 用户id和组id
    • (共享同一地址空间:如果定义一个函数,在各线程中都可以调用)

补充: a.堆区可以被共享,但是我们认为是私有的 b.栈区被认为是私有的

1.4进程 VS 线程

a.调度层面:上下文

b.为什么线程切换的成本更低?

--地址空间 && 页表不需要切换

--CPU内部是有L1~L3cache 对内存的代码和数据, 根据局部原理, 预读CPU内部

--如果进程切换cache就立即失效: 新进程过来, 只能重新缓存

2.线程的控制

2.0编写的时候加上:lpthread(使用pthread_create,必须引入线程库)

makefile:

mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

2.1线程的创建

功能:创建一个新的线程,并在该线程上运行指定的函数
头文件:#include 
函数:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
返回值:0:成功创建线程,非0:表示错误

thread:一个指向pthread_t类型的指针,用于存储新创建线程的ID;
attr:一个指向pthread_attr_t类型的指针,用于设置新线程的属性(如堆栈大小、调度策略等),
可以传入NULL使用默认属性
start_routine:一个函数指针,指定当新线程被创建时所要执行的函数
arg:传递给start_routine函数的参数



一旦调用成功,新线程将开始执行start_routine函数,并可通过arg参数访问传递给它的数据。线程执行完毕后,可以通过调用pthread_join函数等待线程的终止来进行清理操作

代码编写:

创建多个线程来打印name与pid,若线程是在进程内运行,其pid应该相同

#include 
#include 
#include 
#include 
#include 

using namespace std;

void *ThreadFun(void *args)
{
    const string name = (char *)args;
    while (1)
    {
        cout << "name: " << name << " pid: " << getpid() << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid[3];
    char name[64];
    for (int i = 1; i <= 3; i++)
    {
        snprintf(name,sizeof(name),"%s-%d","thread",i);
        pthread_create(tid + i, nullptr, ThreadFun, (void *)name);
        sleep(1);
    }

    while (1)
    {
        cout << "main thread: " << getpid() << endl;
        sleep(3);
    }

    return 0;
}

结果:

Linux:多线程_第3张图片

  • LWP:轻量级进程
  • 杀掉主进程,所有进程都退出了(所有线程所使用的资源是进程给的,进程被杀死后资源被回收)

2.2线程的等待

功能:用于等待指定的线程终止
头文件:#include 
函数:int pthread_join(pthread_t thread, void **retval);
返回值:成功返回为0,错误返回非零的错误码

thread:要等待终止的线程的标识符(类型为pthread_t)
retval:用于接收线程的返回值的指针

为什么要等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

2.3线程的终止

1.exit退出~~>终止进程(一般在线程中不用)

2.pthread_exit函数

头文件:#include
功能:线程终止
函数:void pthread_exit(void *value_ptr);
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

参数value_ptr:value_ptr不要指向一个局部变量

3.pthread_canel函数

功能:取消一个执行中的线程
原型:int pthread_cancel(pthread_t thread);
返回值:成功返回0;失败返回错误码
参数:thread:线程ID

2.4线程ID

1.pthread_t 本质就是一个地址

是把线程相关属性集合的起始地址作为线程ID

线程ID是在原生线程库当中的, 该线程匹配的属性集合的起始地址

Linux:多线程_第4张图片

每个线程要拥有独立的栈(由库去提供), 主线程使用的是内核提供的栈(达到每个线程拥有独立的栈)

可以使用pthread_self() 来获取线程id

2.--thread : 修饰全局变量 ~~> 让每一个线程各自拥有一个全局的变量 -- 线程的局部存储

3.若在线程执行execl函数, 会导致整个线程的代码和数据被替换 等价于进程被替换~~>exit

2.5线程的分离

线程分离后出现异常也会导致整个进程退出

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
int pthread_detach(pthread_t thread);
  • a.线程组内其他线程对目标线程进行分离 b.自己分离:pthread_detach(pthread_self());
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的

3.线程的互斥与同步

3.1相关概念

临界资源概念补充: 在一个资源被多个执行流共享的情况下, 通过一定的方式,让任何时刻只允许一个执行流访问的资源

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界自娱的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:可以被调度影响,但是并不影响本身的特性,该操作只有两态,要么完成,要么未完成

--多线程引发的问题

1.因为线程间的切换~~>执行流出现不可预期的结果(调度时序问题) ~~>需要加上多执行流访问资源的保护

--线程不断切换,而对一个不加保护的全局变量做修改的时候可能会引发的问题(如计算)

--当前CPU正在执行哪个执行流,寄存器里面放的就是哪个执行流的上下文数据

~~>当执行流被切换,其上下文是要被保存的

~~>把数据读取到CPU的寄存器, 本质是把我们的数据读取到当前的执行流的上下文

3.2互斥锁

3.2.1概念理解

互斥锁(mutex)是一种用于控制多线程对共享资源进行访问的同步机制,在多线程编程中起到了重要的作用。互斥锁的主要功能确保在任意时刻只有一个线程能够访问共享资源,从而避免竞争条件(race condition)和数据不一致的问题。

1.如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗?

2.加锁保护:加锁的时候.一定要保证加锁的粒度,越小越好

3.pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALZER; pthread_mutex_t 就是原生线程库提供的数据类型

4.加锁就是串行执行了吗? 加锁了之后, 线程在临界区中, 是否会切换, 会有问题吗? 原子性的体现

是(执行临界区代码一定是串行的), 会切换, 不会有问题.

第一次理解:虽然被切换了,但是你是持有锁被切换的, 所以其它抢票线程要执行临界区代码, 也必须先申请锁,但锁无法申请成功, 也不会让其它线程进入临界区, 就保证了临界区中数据一致性

//不申请锁,直接访问临界资源 ~~>错误的编码方式

//原子性,在没有持有锁的线程看来,对我有意义的情况只有两种:

1.线程1没有持有锁(什么都没做) 2.线程1释放锁(做完) 此时我可以申请锁

a.可重入VS 线程安全

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

--常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

--常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

--常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

--常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

b.可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

c.可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

3.2.2操作理解

在C和C++的多线程编程中,可以使用互斥锁来保护共享资源,确保在同一时刻只有一个线程可以访问这些资源。使用互斥锁的一般流程如下:

  1. 定义并初始化互斥锁。
  2. 在访问共享资源之前,使用互斥锁进行加锁(lock)操作。
  3. 访问完共享资源后,使用互斥锁进行解锁(unlock)操作。

头文件:

int pthread_mutex_init(pthread_mutex_t *mutex)函数进行初始化

int pthread_mutex_lock(pthread_mutex_t *mutex)进行加锁(阻塞式)

int pthread_mutex_trylock(pthread_mutex_t *mutex)(非阻塞式)

int pthread_mutex_unlock(pthread_mutex_t *mutex)进行解锁

int pthread_mutex_destroy(pthread_mutex_t *mutex)进行销毁

//适用于全局变量或者静态变量
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 静态初始化

//适用于在函数内部创建互斥锁对象
pthread_mutex_t mutex; // 变量定义
pthread_mutex_init(&mutex, NULL); // 动态初始化


// 在临界区内加锁和解锁
pthread_mutex_lock(&mutex);

// 访问共享资源

pthread_mutex_unlock(&mutex);

//销毁锁
pthread_mutex_destroy(&mutex);

3.2.3互斥锁原理

要访问临界资源,每一个线程必须先申请锁, 每一个线程都必须先看到同一把锁&&访问它~~>锁本身是不是一种临界资源? 谁来保证锁的安全呢?所以为了保证锁的安全, 申请和释放锁必须是原子的自己保证(一行汇编指令)

~swap或exchange指令:以一条汇编的方式,将内存和CPU内寄存区数据进行交换

--在汇编的角度:只有一条汇编语句, 就认为该汇编语句的执行时原子的

--在执行流视角,如何看待CPU上面的寄存器?CPU内部的寄存器本质叫做当前执行流的上下文,寄存器的空间是被所有执行流共享的, 但寄存器的内容,是每一个执行流私有的(上下文)

Linux:多线程_第5张图片

  • lock:1.将0放入寄存器%al中  2. 把寄存器%al的值与内存中mutex的值做交换 3.如果al寄存器的内容>0 , 说明申请锁成功, 就返回0, 否则申请失败, 就挂起等待
  • unlock: 把1给内存中的mutex

如果线程A已经申请锁成功, 此时%al: 1, mtx:0, 若现在切换为线程B来申请锁(线程A要带走自己寄存器中的内存:1), 线程B再执行lock的代码, 发现执行失败(%al为0)

3.3死锁

3.3.1概念

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

3.3.2死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

3.3.3避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

3.4线程同步

互斥锁的缺陷: 1.频繁的申请到资源 2.太过于浪费自己和对方的资源

引入同步: 主要是为了解决访问临界资源合理性的问题的

~~>按照一定的顺序,进行临界资源的访问, 线程同步

当我们申请临界资源的时候~~>先要做临界资源是否存在的检测~~>检测的本质:也是访问临界资源

结论: 对临界资源的访问, 也一定是需要在加锁和解锁之间的!

常规方式要检测条件就绪, 注定了我们必须频繁申请和释放锁, 有没有办法让我们的线程检测到资源不就绪的时候(条件变量)

1.不要让线程在频繁的自己检测,等待

2.当条件就绪的时候, 通知对应的进程, 让他来进行资源申请和访问

3.4.1同步概念与竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

3.4.2条件变量

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

条件变量是多线程编程中用于线程间通信和同步的一种机制

3.4.3条件变量的使用

--条件变量的基本功能包括:等待某个条件的发生(wait)、发送信号通知(signal)和广播通知(broadcast)

头文件:

#include  

初始化:

pthread_cond_init

函数原型:int pthread_cond_init(pthread_cond_t *restrict cond, 
                      const pthread_condattr_t *restrict attr);
功能:初始化条件变量
参数:cond 是指向条件变量对象的指针,attr 是一个指向线程属性对象的指针,可以为 NULL。
返回值:调用成功返回0,失败返回错误码

销毁:

pthread_cond_destroy

函数原型:int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁条件变量
参数:cond 是指向已初始化的条件变量对象的指针。
返回值:调用成功返回0,失败返回错误码。

等待:

pthread_cond_wait

函数原型:int pthread_cond_wait(pthread_cond_t *restrict cond,  
                            pthread_mutex_t *restrict mutex);
功能:等待条件变量,并在收到信号或广播时解除阻塞
参数:cond 是指向条件变量对象的指针,mutex 是与条件变量相关联的互斥锁。
返回值:调用成功返回0,失败返回错误码。

规范: 在while()中等待-->保证条件就绪的时候再被唤醒

唤醒:

pthread_cond_signal

功能:当条件满足时用来唤醒等待在条件变量上的一个线程。
函数原型:int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒一个等待在条件变量上的线程
参数:cond 是指向条件变量对象的指针。
返回值:调用成功返回0,失败返回错误码
pthread_cond_broadcast
函数原型:int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒所有等待在条件变量上的线程
参数:cond 是指向条件变量对象的指针。
返回值:调用成功返回0,失败返回错误码。

--条件变量通常与互斥锁一起使用:

//pthread_cond_t cond; // 变量定义
//pthread_cond_init(&cond, NULL); // 动态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

等待线程:
pthread_mutex_lock(&mutex);
while (condition_is_not_met) 
{
    pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);

发送通知的线程:
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);// 改变条件
pthread_mutex_unlock(&mutex);

通过合理使用条件变量,可以实现多线程程序间的同步和通信
避免忙等待的问题,提高系统性能和可维护性

--条件变量通常用于一种生产者-消费者的模式,即一个或多个线程等待某个条件的发生,而另外的线程在某个时刻满足条件后通知等待线程

4.生产者消费模型

4.1生产消费者模型的概念

基本工程师思维

  • 生产者和消费者: 线程承担~~>给线程进行角色化
  • 交易场所: 某种数据结构表示的缓冲区
  • 商品: 数据

~~>有一部分线程生产对应的数据, 放入缓冲区

~~>有一部分线程消费对应的数据, 对数据做处理

生产者-消费者模型通常需要解决以下几个问题:

  1. 同步:确保生产者不会向已满的缓冲区中放入数据,消费者也不会从空的缓冲区中取出数据。
  2. 互斥:当生产者向缓冲区放入数据或消费者从缓冲区取出数据时,需要确保这个操作是原子的,不会被其他线程中断。
  3. 缓冲区管理:需要合理管理缓冲区,确保数据能够安全地存储和取出。

通常会使用同步工具,如互斥锁、条件变量或信号量来实现生产者-消费者模型

4.2生产消费者模型的意义

--生产消费者模型为什么能提高效率 + 解耦?~~>并发

a. 消费者拿走数据花费时间去处理

~~>此时消费者并没有访问仓库和申请锁~~>此时生产者可以生产数据 + 把数据放入到仓库

b.生产者生产数据需要时间去生产

~~>此时生产者并没有访问仓库和申请锁~~>此时消费者可以去拿数据 + 处理数据

生产者生产数据的时候, 消费者可以不去等待生产者生产数据, 仓库里可能有历史的数据, 消费者直接拿去处理

总结: 当生产者在生产数据的时候, 消费者同时也在处理数据~~>两个线程实现的一定程度的并发

通过缓冲区的特点来提高生产和消费的并发度

c.多生产多消费的意义?

当任务很多(生产和消费这个任务所需是的时间较长)

生产之前和消费之后, 它们可以并发的有多个执行流, 同时进行生产和消费

5.信号量

5.1信号量概念

a.信号量是一种软件资源, 信号量本质上是一个计数器:

信号量计数器 ~~> 对临界资源的预定机制

申请信号量 ~~> 计数器 -- ~~> P操作 ~~> 必须是原子的

释放信号量 ~~> 计数器 ++ ~~> V操作 ~~> 必须是原子的

b.计数器的意义:可以不用进入临界区就能知到资源情况(减少临界区内部的判断)

条件变量:申请锁 --> 判断与访问 -->解锁 (本质:我们并不清楚临界资源的情况)

信号量: 提前预设资源的情况, 而且再pv变化过程中, 我们在外部就能知晓临界资源的情况

5.2信号量的操作

--头文件:

#include   // 包含信号量相关的函数和数据类型的声明
#include     // 包含了线程相关的声明,因为通常信号量会和线程一起使用

--初始化sem_init:

函数原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化一个未命名的信号量
参数:
    sem 是指向信号量对象的指针,
    pshared 用于指示信号量是在进程间共享还是线程间共享,
    value 是信号量的初始值。
返回值:调用成功返回0,失败返回-1

--销毁sem_destroy:

函数原型:int sem_destroy(sem_t *sem);
功能:销毁一个未命名的信号量
参数:sem 是指向已初始化的信号量对象的指针
返回值:调用成功返回0,失败返回-1

--等待sem_wait:

函数原型:int sem_wait(sem_t *sem);
功能:等待信号量,如果信号量的值大于0,将其减1;否则将线程阻塞,直到信号量的值大于0
参数:sem 是指向信号量对象的指针
返回值:调用成功返回0,失败返回-1

--发布sem_post:

函数原型:int sem_post(sem_t *sem);
功能:释放信号量,将信号量的值加1,唤醒等待该信号量的线程
参数:sem 是指向信号量对象的指针
返回值:调用成功返回0,失败返回-1

--示例:

#include 
#include 
#include 

#define NUM_THREADS 3

sem_t semaphore; // 定义一个信号量

void* thread_function(void* arg) {
    int thread_id = *((int*)arg);
    printf("Thread %d is waiting...\n", thread_id);
    sem_wait(&semaphore); // 等待信号量
    printf("Thread %d has acquired the semaphore and is now working\n", thread_id);
    // 模拟线程工作
    for (int i = 0; i < 5; i++) {
        printf("Thread %d is working\n", thread_id);
    }
    sem_post(&semaphore); // 释放信号量
    printf("Thread %d has released the semaphore and finished\n", thread_id);
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    sem_init(&semaphore, 0, 1); // 初始化信号量,初始值为1

    int thread_ids[NUM_THREADS];

    for (int i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
    }

    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    sem_destroy(&semaphore); // 销毁信号量
    return 0;
}

6.代码编写

6.1多线程并行问题

// 多个线程执行抢票
#include 
#include 
#include 
#include 

using namespace std;

int ticket = 50;
void *getTicket(void *args)
{
    char *id = (char *)args;

    while (1)
    {
        if (ticket > 0)
        {
            printf("%s get ticket, rest: %d\n",id,ticket);
            ticket--;
        }
        else
            break;
    }
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, getTicket, (void *)"t1");
    pthread_create(&t2, nullptr, getTicket, (void *)"t2");
    pthread_create(&t3, nullptr, getTicket, (void *)"t3");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);

    return 0;
}

结果:

Linux:多线程_第6张图片

原因:调度时序问题(进程被切换引起)

ticket>0编译出来可能是3条指令:数据在内存中,而需要通过CPU计算

  • 将内存中的数据漏斗到CPU
  • CPU进行计算
  • 将结果漏斗到内存

在执行上面步骤的时候,线程A可能随时被切换,而公共资源ticket又可能线程B拿走(更新ticket),运行一段时间后,线程A切换回来,继续之前的步骤向下计算.~~>引起时序问题

解决:加锁保护临界资源,使其在任意时刻,只能有1个线程来访问

代码:Linux-test: Linux下,提交代码 - Gitee.com

Linux:多线程_第7张图片

加锁保护临界资源, 线程创建的时候,传递的参数也可以传递自定义对象

6.2线程的同步

创建多个线程打印信息来模拟对临界资源的访问

对临界资源的访问,是需要加锁的,并且我们希望线程按照一定顺序访问临界资源

若临界资源不就绪, 线程就阻塞式等待, 直到被唤醒(不用再频繁的判断临界资源是否就绪)

代码:Linux-test: ---Linux练习代码--- - Gitee.com

Linux:多线程_第8张图片

6.3生产消费者模型(阻塞队列)

角色化:生产者生产数据, 消费者消费数据, 缓冲区:blockqueue

通信: 让生产者和消费者看到同一个阻塞队列

blockqueue:

  • 场景: a.队满->通知消费者来消费  b.队空->通知生产者来生产
  • 加锁: consumer在pop的时候, prod在push(加锁保证安全)
  • 条件变量: 判断临界资源是否就绪(就绪则唤醒)
  • 存储自定义类型

代码:Linux-test: ---Linux练习代码---

Linux:多线程_第9张图片

6.4生产消费者模型(循环队列)

1.使用信号量方案实现生产消费模型--> 可以局部使用临界资源(环形队列)

   使用加锁+条件变量实现生产消费模型-->将临界资源看为一个整体

2.当生产者和消费者指向不同位置的时候, 让他们并发执行

当生产者和消费者指向同一个位置的时候, 具有互斥与同步关系即可

  • 为空: 让生产者先执行
  • 为满:让消费者先执行

3.生产者:关注空间资源, semSpace - > N   消费者:关注数据资源, semData - > 0     

  •    生产: P(semSpace) --> semSpace--   特定位置生产:V(semData) --> semData++
  •    消费: P(semData) --> semData--        特定位置消费:V(semSpace) --> semSpace++

4.多生产者多消费者

  • 在生产者与生产者之间加锁, 在消费者与消费者之间加锁(互斥关系)
  • 生产者们和消费者们的临界资源: 下标
  • 先申请信号量在加锁 (信号量本身时原子的)(多线程并发执行, 先分配完信号量) (后面就只剩进入到锁空间即可) -->效率提高

代码:Linux-test: ---Linux练习代码--- - Gitee.com

6.5线程池

1.线程池: 维护一组预先创建的线程来处理任务, 线程池中的线程可以被多个任务重复使用,以减少创合销毁的开销-->提高性能(空间换时间)

2.任务队列: push或pop任务的时候需要加锁(保证资源安全) + 条件变量(避免频繁询问临界资源)

代码:Linux-test: ---Linux练习代码--- - Gitee.com

Linux:多线程_第10张图片

补充

对一块空间的细粒度划分

struct vm_area_struct Linux 内核中用于描述虚拟内存区域的结构体

主要包含以下字段:
unsigned long vm_start:虚拟内存区域的起始地址
unsigned long vm_end:虚拟内存区域的结束地址
struct vm_area_struct *vm_next:指向下一个虚拟内存区域的指针
struct vm_area_struct *vm_prev:指向上一个虚拟内存区域的指针
...

可执行程序:exe

1 .exe就是一个文件
2 可执行程序是按照地址空间方式进行编译的
3 可执行程序,按照区域被划分为了以4KB为单位(页帧)
//物理内存也以4KB为单位进行划分(页框)

//IO的基本单位是4KB,IO的时候:将页帧装进页框里
//OS使用struct Page来管理这些4KB

缺页中断:当程序试图访问虚拟内存中的一个页,但是该页不在物理内存中时,就会发生缺页中断。操作系统会响应这个中断,将需要的页从辅助存储(如硬盘)中加载到主内存中,然后重新执行产生中断的指令。

Linux:多线程_第11张图片

页表的映射

Linux:多线程_第12张图片

你可能感兴趣的:(Linux,linux)