共享栈 第一个主线程会在栈区 而当其他线程创建时实在共享区动态申请的栈区
线程局部存储 __thread 关键字 与编译有关
全局变量是被线程共享的 每个线程都能看到 修改 但是如果对该全局变量加上__thread关键字后 该全局变量就不会被共享 将变量在库中的每一个线程的属性集合中都会添加一份 访问时用的是自己属性中的变量地址 这样每个线程访问的地址不同也就不会起冲突了
类本身就是一个全局变量
在共享区动态创建的共享栈是不会动态向下生长的 是固定大小8M 通过mmap调用穿件stack
线程互斥
补充概念
共享资源:多个线程可以访问的资源。
临界资源:多线程执⾏流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起 保护作⽤
原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成
互斥量mutex
互斥量 是一种同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问或修改该资源。互斥量是多线程编程中最常用的同步工具之一,广泛应用于各种编程语言和环境中。
线程访问的操作本身就不是原子性的
计算可以分为逻辑计算和算术计算
执行中 1.--操作 虽然代码只有一句话 但是CPU在处理时会将其分为多个指令 先将变量读入寄存器中 读到算术运算符之后 在对寄存器中内容进行进行算术运算 在将计算后的内容放回 所以它并不具有原子性
2.if判断 也是不具有原子性的 这里if判断是一种逻辑计算 CPU在处理时会将其分为多个指令 先将变量读入寄存器 之后执行逻辑判断 之后根据逻辑判断执行逻辑分支 所以也不具有原子性
在线程工作中有些现场会被减到负数 或者在一个时间中突然增大 这是因为在线程操作时不具有原子性 导致可能同一时间中多个线程同时访问造成的结果
线程或进程什么时候会进行切换
a.时间片耗尽
b.有更高优先级的时间片要调度
c.通过sleep 当从内核返回用户时 会检测时间片是否到达 从而导致切换 也就是在sleep的过程中 也会有其它线程来使用你的时间片 (这是对a的补充)
一条--指令的执行大概有三步
1. 数据->寄存器(本质 将数据从共享变成线程私有)(寄存器的内容 是执行流(线程)硬件上下文)
2.CPU内部运算
3.写回数据
可以看到在一个--的运算操作中时不能保证原子性的 所以应该怎么去解决这个问题呢?
代码必须要有互斥⾏为:
1.当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
2.如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程 进⼊该临界区。
3.如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量。
解决方案1. 加锁解锁
锁是全局的 所以锁也是共享资源
初始化互斥量的接口
静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量 销毁互斥量需要注意:
• 使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
• 不要销毁⼀个已经加锁的互斥量
• 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调⽤ pthread_ lock 时,可能会遇到以下情况:
• 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
• 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到 互斥量,那么pthread_lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。
1.锁本身就是共享的 那么锁要交给谁来保护? 将lock 和 unlock 设置为原子性
2.如何看待锁 信号量(二元信号量 == 锁) 在信号量中我们说本质就是去预定资源 这里我们的锁就是将资源看做一个大整体 去加锁预定它 使用整个资源
3.申请锁 如果互斥量已经被拿走使用 那么当前线程就要进行阻塞等待 直到使用加锁的线程释放之后 所有当前正在等待的线程在一起竞争这个锁
4如果加锁后的线程被切换 这个锁仍旧使用的状态 其他线程依旧无法使用 也就是串行 也就是加锁执行效率低的原因
5.可不可以不遵守锁的规则 由于锁的出现本身就是为了程序的安全 不遵守会导致更容易出现bug
加锁就是对临界区加锁
还有一种接口是非阻塞加锁 pthread_lock_trylock()
如果是全局锁 就没必要进行初始化 和人销毁 如果是局部锁就有必要
互斥量(锁)的原理实现
互斥量的原理主要在于swap和exchange指令 当我们的代码在汇编链接之后 最后会在加锁的部分就会出现swap和exchange 所以我们在平时时看不到的 该指令的作用是将内存单元和寄存器的数据进行交换 swap或是exchange都是一条指令所以可以保证原子性 在刚开始我们的mutex是存在在内存单元的 而当需要加锁 swap或是exchange就会将其交换到寄存器 我们的内存单元就变为了0(在刚开始寄存器是0 ) 之后得到锁的线程开始执行临界区任务 结束之后将锁释放 也就是将其从自己的寄存器在交换会内存单元 而在这一过程中 其他线程是无法得到锁的 (因为这时内存单元为0 其他线程中的寄存器中也是0)
这样就实现了锁的原子性(这个工作是软件实现)
但是其实我们的硬件也是可以实现的 我们线程调度是要受到时钟中断的影响 是时钟中断在推动着系统工作 如果我们通过硬件进行关中断 (关闭时钟和外部中断) 这样就可以保证一个正在访问临界区线程的原子性
纯互斥可解决大部分安全问题 但是并不一定合理
比如在一个自习室只有一个座位一把钥匙 也就是只能有一个人用这个自习室 而其他人就只能在门外等待钥匙释放后进行争夺 可是如果这个拿着钥匙的在释放钥匙又立马争夺钥匙 那么在门外等地啊的人就永远不可能 拿到那是使用这个自习室资源 这也就是饥饿 问题
那么我们该如何解决饥饿问题呢 ?
同步 条件变量
也就是在上面的例子中在外面等待的人不在通过争夺获取钥匙 而是在外面排队 当释放资源的人出来后 就必须排队队列的最后等待再次获取钥匙 这样每个人都有访问临界资源的机会也就不会导致饥饿
条件变量
• 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
• 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列 中。这种情况就需要⽤到条件变量。
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从⽽有效避免 饥饿问题,叫做同步
条件变量的初始化 销毁 等待 释放 接口
1. 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
-
功能:初始化一个条件变量。
-
参数:
-
cond
:指向要初始化的条件变量的指针。 -
attr
:指向条件变量属性的指针。如果为NULL
,则使用默认属性。
-
-
返回值:
-
成功时返回
0
。 -
失败时返回错误码,例如:
-
EINVAL
:cond
或attr
无效。 -
EBUSY
:cond
已经被初始化。
-
-
2. 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
-
功能:销毁一个条件变量,释放与之相关的资源。
-
参数:
-
cond
:指向要销毁的条件变量的指针。
-
-
返回值:
-
成功时返回
0
。 -
失败时返回错误码,例如:
-
EBUSY
:有线程正在等待该条件变量。
-
-
3. 等待条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
-
功能:使线程等待某个条件变量满足。调用此函数时,线程会释放互斥锁(
mutex
),进入等待状态。当条件变量被唤醒时,线程会重新获取互斥锁并继续执行。 -
参数:
-
cond
:要等待的条件变量。 -
mutex
:与条件变量关联的互斥锁。调用此函数之前,必须先锁定该互斥锁。
-
-
返回值:
-
成功时返回
0
。 -
失败时返回错误码,例如:
-
EINVAL
:cond
或mutex
无效。 -
EDEADLK
:尝试获取互斥锁时发生死锁。
-
-
4. 唤醒所有等待条件变量的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
-
功能:唤醒所有正在等待该条件变量的线程。
-
参数:
-
cond
:指向要唤醒的条件变量的指针。
-
-
返回值:
-
成功时返回
0
。 -
失败时返回错误码,例如:
-
EINVAL
:cond
无效。
-
-
int pthread_cond_signal(pthread_cond_t *cond);
-
cond
:指向要操作的条件变量的指针。该条件变量必须已经通过pthread_cond_init
初始化(或者使用静态初始化器PTHREAD_COND_INITIALIZER
)。
-
唤醒单个等待线程:
pthread_cond_signal
会唤醒一个正在等待该条件变量的线程。如果当前没有线程正在等待该条件变量,则调用此函数不会有任何效果。 -
线程选择:具体唤醒哪个线程由线程库的调度策略决定,通常与线程的优先级和等待时间有关。
返回值
-
成功时返回
0
。 -
失败时返回错误码,例如:
-
EINVAL
:cond
参数无效。
-
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
-
功能:初始化一个信号量。
-
参数:
-
sem
:指向信号量对象的指针。 -
pshared
:控制信号量是线程间共享还是进程间共享。0
表示线程间共享,非零值表示进程间共享。 -
value
:信号量的初始值,表示可用资源的数量。
-
-
返回值:
-
成功时返回
0
。 -
失败时返回
-1
并设置errno
以指示错误原因。
-
销毁信号量
int sem_destroy(sem_t *sem);
-
功能:销毁一个已经初始化的信号量,释放相关资源。
-
参数:
-
sem
:指向要销毁的信号量对象的指针。
-
-
返回值:
-
成功时返回
0
。 -
失败时返回
-1
并设置errno
。
-
等待信号量(P操作)
int sem_wait(sem_t *sem); // P()
-
功能:等待信号量,如果信号量的值大于
0
,则将其减1
并立即返回;如果信号量的值为0
,则线程挂起(阻塞),直到信号量的值大于0
。 -
参数:
-
sem
:指向信号量对象的指针。
-
-
返回值:
-
成功时返回
0
。 -
失败时返回
-1
并设置errno
-
发布信号量(V操作)
int sem_post(sem_t *sem); // V()
-
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量的值加
1
。如果有线程因等待该信号量而阻塞,则唤醒其中一个线程。 -
参数:
-
sem
:指向信号量对象的指针。
-
-
返回值:
-
成功时返回
0
。 -
失败时返回
-1
并设置errno
。
-
基于环形队列的⽣产消费模型
环形队列(也称为循环缓冲区)是一种高效的数据结构,用于在生产者和消费者之间传递数据。在这种模型中,生产者负责生成数据并将其放入队列,而消费者则从队列中取出数据进行处理。环形队列通过使用固定大小的缓冲区来实现高效的数据交换,避免了动态内存分配的开销。
基本元素:
-
缓冲区数组:一个固定大小的数组,用于存储数据。
-
头指针(head):指向队列中第一个有效数据的位置。
-
尾指针(tail):指向队列中下一个可写位置。
-
容量(N):缓冲区的大小,即最多可以存储的数据项数。
操作
-
生产者操作(生产数据):
-
生产者首先检查缓冲区是否有空间(即
tail
指针是否小于head
指针加上缓冲区大小)。 -
如果有空间,生产者将数据写入
tail
指针指向的位置,然后更新tail
指针。 -
如果没有空间,生产者可能需要等待或采取其他措施。
-
-
消费者操作(消费数据):
-
消费者首先检查缓冲区是否有数据(即
head
指针是否小于tail
指针)。 -
如果有数据,消费者从
head
指针指向的位置读取数据,然后更新head
指针。 -
如果没有数据,消费者可能需要等待或采取其他措施。
-
同步和互斥
由于生产者和消费者可能同时访问环形队列,因此需要使用同步机制来避免数据竞争和不一致的问题:
-
互斥锁(Mutex):
-
确保在任何时刻只有一个生产者或消费者可以访问环形队列。
-
通过锁定和解锁操作来控制对队列的访问。
-
-
信号量(Semaphore):
-
data
信号量用于控制数据项的数量,确保消费者不会在没有数据时读取。 -
space
信号量用于控制缓冲区空间的数量,确保生产者不会在没有空间时写入。
-
只有在为满为空时指向同一个位置 而当其他位置时 两者之间是解耦的