10. pthread_t 类型
注意:
- 每一个线程的库级别的tcb的起始地址,就是线程的 tid
- 每一个线程都有自己独立的栈结构
- 线程和线程之间,也是可以被其他线程看到并访问的(比如全局函数)
代码
如果想要进程拥有私人的全局变量(即线程库里面的线程局部存储):
__stread 修饰 变量(只能是内置类型的,不能是自定义类型的) ,如:
__stread int val
注意:
__stread 是编译选项
代码
11. linux线程互斥
(一)进程线程间的互斥相关背景概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
- 多线程并发访问共享资源的时候,容易发生数据不一致问题
抢票举例
if ( tickets > 0) // 判断
{
tickets--; //抢到票了
}
注意:
由于是 tickets-- 这一步是三条汇编指令去执行
- 把 tickets 拷贝到CPU寄存器中
- CPU寄存器的值--
- CPU寄存器的值拷贝到内存
发生错误可能原因:
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- tickets-- 操作本身就不是一个原子操作
注意:
要解决数据不一致问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
(二)互斥量
互斥量相关函数
初始化互斥量
初始化互斥量有两种方法:
1. 静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
注意:
此时不需要对 pthread_mutex_t 类型变量做初始化和销毁处理(即不需要调用 pthread_mutex_init 和 pthread_mutex_destroy 函数)
2. 动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:互斥量的属性,一般设置成NULL
注意:
这个函数一定要搭配 pthread_mutex_destroy一起使用
销毁互斥量
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,失败返回错误号
注意:
1. 调用 pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
互斥量处于加锁状态,该线程会被阻塞,执行流处于被挂起,等待操作系统唤醒(即互斥量解锁了)
2. 哪个线程都可以对互斥量解锁,和加锁的线程是哪个无关
3. 即使只有一个线程,也可能由于操作不当,被阻塞挂起(比如:调用了两次加锁)
4. 如果申请了锁的资源,一定要把它释放掉
5. 加了锁的临界区的代码发生线程切换,也不会影响结果的正确
(三)加锁
- 加锁的本质:用时间来换安全
- 加锁的表现:线程对于临界区代码串行执行
- 加锁的原则:尽量要保证临界区的代码少
注意:
- 纯互斥环境下,如果锁分配的不合理,容易导致其他线程的饥饿问题(线程对锁的竞争能力可能不同,比如一个线程释放了锁,又能立刻拿到锁资源,导致其他线程一直阻塞)
- 不一定是有互斥,必有饥饿,纯互 m斥的场景,就用互斥
- 解决饥饿:让所有的线程获取锁资源,是按一定的顺序获取 --------- 叫同步
(三)互斥性原理
锁也是共享资源,为了维护其他临界资源,应该先保护自己的资源
锁的实现原理:
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令(在硬件芯片上都有对应的指令集),该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期
把lock和unlock的伪代码改一下:
注意:
- 寄存器不等于寄存器的内容(寄存器实际上很多时候,得到的数据是上下文数据)
- 线程在执行的时候,将共享资源,加载到CPU寄存器的本质:把数据的内容,变成自己的上下文(发生线程切换,上下文数据需要被保存回线程,从而变成线程独有的资源 ---- 线程局部储存)
12. 可重入和 线程安全
(一)概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
(二)常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
(三)常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
(四)常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
(五)常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
(六)可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
(七)可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的