1.1 线程互斥相关概念
- 临界资源:多线程执行流共享的资源就叫做临界资源。
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
1.2 多线程的互斥问题
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
#include <cstdio>
#include <cstdint>
#include <pthread.h>
#include <unistd.h>
// 共享资源
int tickets = 10;
// 线程函数
void* sell_ticket(void* arg) {
int sold = 0;
while (true) {
// 模拟售票过程
if (tickets > 0) {
printf("Thread %ld sold ticket %d, remaining tickets: %d\n", (long)arg, sold, tickets);
tickets--;
sold++;
usleep(1000);
}
}
pthread_exit(NULL);
}
int main() {
pthread_t thread[5];
// 创建售票线程
for(uint64_t i = 0; i < 5; i++){
pthread_create(&thread[i], NULL, sell_ticket, (void *)i);
}
// 等待线程结束
for(uint64_t i = 0; i < 5; i++){
pthread_join(thread[i], NULL);
}
printf("All tickets sold out.\n");
return 0;
}
为什么可能无法获得正确结果?
- –ticket操作本身就不是一个原子操作
- if语句判断条件为真以后,代码可以并发的切换到其他线程
要解决该问题, 需要做到以下三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
本质上就是需要一把锁, 而Linux上提供的是互斥量!
1.3 互斥量
1.3.1 初始化互斥量
静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数说明:
-
pthread_mutex_t *restrict mutex:指向要初始化的互斥锁的指针。
-
const pthread_mutexattr_t *restrict attr:指向互斥锁属性对象的指针。这个属性对象可以用来设置互斥锁的一些特殊属性,如类型、协议和优先级继承等。如果不需要特殊的属性设置,可以传入 NULL,使用默认属性。
返回值:
- 0:表示成功。
- 非零值:表示失败。常见的错误码包括:
EINVAL:attr 指定的属性无效。
ENOMEM:内存不足,无法初始化互斥锁。
1.3.2 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 参数说明:
- pthread_mutex_t *mutex:指向要销毁的互斥锁的指针。
- 返回值:
- 0:表示成功。
- 非零值:表示失败。常见的错误码包括:
EINVAL:指定的互斥锁无效。
EBUSY:互斥锁当前被锁定,无法销毁。
1.3.3 互斥量加锁和解锁
加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 参数说明:
- pthread_mutex_t *mutex:指向要锁定的互斥锁的指针。
- 返回值:
- 0:表示成功。
- 非零值:表示失败。常见的错误码包括:
EINVAL:指定的互斥锁无效。
EDEADLK:当前线程已经锁定了该互斥锁,导致死锁。
解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数说明:
- pthread_mutex_t *mutex:指向要解锁的互斥锁的指针。
- 返回值:
- 0:表示成功。
- 非零值:表示失败。常见的错误码包括:
EINVAL:指定的互斥锁无效。
EPERM:当前线程没有锁定该互斥锁。
1.3.4 线程安全的售票系统
#include <cstdio>
#include <cstdint>
#include <pthread.h>
#include <unistd.h>
// 共享资源
int tickets = 10;
pthread_mutex_t mutex;
// 线程函数
void* sell_ticket(void* arg) {
int sold = 0;
while (true) {
pthread_mutex_lock(&mutex); // 锁定互斥锁
if (tickets > 0) {
tickets--;
sold++;
printf("Thread %ld sold ticket %d, remaining tickets: %d\n", (long)arg, sold, tickets);
} else {
pthread_mutex_unlock(&mutex); // 解锁互斥锁
break;
}
pthread_mutex_unlock(&mutex); // 解锁互斥锁
usleep(1000); // 模拟耗时操作
}
pthread_exit(NULL);
}
int main() {
pthread_t thread[5];
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建售票线程
for(uint64_t i = 0; i < 5; i++) {
pthread_create(&thread[i], NULL, sell_ticket, (void *)i);
}
// 等待线程结束
for(uint64_t i = 0; i < 5; i++) {
pthread_join(thread[i], NULL);
}
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
printf("All tickets sold out.\n");
return 0;
}
1.3.4 互斥量的实现原理
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令, 该指令的作用是把寄存器和内存单元的数据相交换, 由于只有一条指令, 保证了原子性, 即使是多处理器平台, 访问内存的总线周期也有先后, 一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
1.4 线程安全和可重入函数
-
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
-
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
1.4.1 可重入与线程安全的联系
- 函数是可重入的,那就是线程安全的。
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
1.4.2 可重入与线程安全的区别
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。