Linux线程安全
Linux线程安全详解
Linux线程互斥
进程线程间的互斥相关背景概念
- 临界资源:多线程共享的资源,如全局变量。
- 临界区:访问临界资源的代码段。
- 互斥:确保同一时刻只有一个线程进入临界区,保护临界资源。
- 原子性:操作不可中断,要么完成要么未完成。
进程与线程对比:
- 进程间通信需创建第三方资源(如管道、共享内存),这些资源即临界资源,访问代码为临界区。
- 线程共享进程的大部分资源(如全局变量),无需额外创建资源即可通信。
示例:线程间通信
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int count = 0;
void* Routine(void* arg) {
while (1) {
count++;
sleep(1);
}
pthread_exit((void*)0);
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, Routine, NULL);
while (1) {
printf("count: %d\n", count);
sleep(1);
}
pthread_join(tid, NULL);
return 0;
}
count
是临界资源,被主线程和新线程共享。count++
和printf
是临界区。
互斥与原子性问题 :
多线程并发操作临界资源可能导致数据不一致。例如抢票系统:
c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int tickets = 1000;
void* TicketGrabbing(void* arg) {
const char* name = (char*)arg;
while (1) {
if (tickets > 0) {
usleep(10000); // 模拟业务耗时
printf("[%s] get a ticket, left: %d\n", name, --tickets);
} else {
break;
}
}
printf("%s quit!\n", name);
pthread_exit((void*)0);
}
int main() {
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, TicketGrabbing, "thread 1");
pthread_create(&t2, NULL, TicketGrabbing, "thread 2");
pthread_create(&t3, NULL, TicketGrabbing, "thread 3");
pthread_create(&t4, NULL, TicketGrabbing, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
运行结果可能出现负票数,原因:
if (tickets > 0)
判断后线程可能切换。usleep
期间其他线程可进入临界区。--tickets
非原子操作,分为加载(load)、更新(update)、存储(store)三步,可能被打断。
互斥量mutex
互斥量(mutex)是Linux提供的锁机制,解决线程间的互斥问题,要求:
- 同一时刻只有一个线程进入临界区。
- 无线程在临界区时,允许多线程竞争进入。
- 线程不在临界区时不得阻止其他线程进入。
互斥量的接口
-
初始化 :
cint pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 动态分配:
pthread_mutex_init(&mutex, NULL);
- 静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 动态分配:
-
销毁 :
cint pthread_mutex_destroy(pthread_mutex_t *mutex);
- 注意:已加锁的互斥量不可销毁。
-
加锁 :
cint pthread_mutex_lock(pthread_mutex_t *mutex);
- 未锁:加锁成功。
- 已锁:阻塞等待。
-
解锁 :
cint pthread_mutex_unlock(pthread_mutex_t *mutex);
抢票系统改进:
c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int tickets = 1000;
pthread_mutex_t mutex;
void* TicketGrabbing(void* arg) {
const char* name = (char*)arg;
while (1) {
pthread_mutex_lock(&mutex);
if (tickets > 0) {
usleep(100);
printf("[%s] get a ticket, left: %d\n", name, --tickets);
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
printf("%s quit!\n", name);
pthread_exit((void*)0);
}
int main() {
pthread_mutex_init(&mutex, NULL);
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, TicketGrabbing, "thread 1");
pthread_create(&t2, NULL, TicketGrabbing, "thread 2");
pthread_create(&t3, NULL, TicketGrabbing, "thread 3");
pthread_create(&t4, NULL, TicketGrabbing, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
- 加锁后票数不再出现负值。
- 注意:加锁降低并行性,需合理选择加锁范围。
互斥量实现原理探究
- 原子性:加锁后,临界区操作对其他线程表现为原子性(要么未开始,要么已完成)。
- 线程切换:临界区内可能切换,但锁未释放,其他线程无法进入。
- 锁的保护:锁本身是临界资源,需保证申请过程原子性。
- 实现机制 :使用
swap/exchange
指令(单指令原子性)。- 伪代码 :
- 加锁:
xchgb
交换寄存器(清0)与mutex
(初始1),成功得1,失败得0。 - 解锁:
mutex
置1,唤醒等待线程。
- 加锁:
- 原理:总线周期独占,确保多核环境下原子性。
- 伪代码 :
可重入VS线程安全
概念
- 线程安全:多线程并发执行同一代码,结果一致。
- 可重入:函数被多执行流调用,前一流未结束,后一流进入,结果无误。
常见的线程不安全的情况
- 无锁保护的共享变量操作。
- 函数状态随调用改变(如静态变量)。
- 返回静态变量指针。
- 调用线程不安全函数。
常见的线程安全的情况
- 只读全局/静态变量。
- 接口操作原子性。
- 线程切换不影响结果。
常见的不可重入的情况
- 调用
malloc/free
(全局堆链表)。 - 使用标准I/O(全局数据结构)。
- 函数体内依赖静态数据。
常见的可重入的情况
- 不使用全局/静态变量。
- 不调用
malloc/new
。 - 不调用不可重入函数。
- 数据由调用者提供或局部拷贝。
可重入与线程安全联系
- 可重入函数一定是线程安全的。
- 不可重入函数多线程使用可能不安全。
- 有全局变量的函数通常既不可重入也不安全。
可重入与线程安全区别
- 可重入是线程安全的一种。
- 加锁可使函数线程安全,但若锁未释放则不可重入。
常见锁概念
死锁
死锁是多线程互相等待对方释放资源,导致永久阻塞。
-
单线程死锁:同一线程多次申请同一锁。
c#include <stdio.h> #include <pthread.h> pthread_mutex_t mutex; void* Routine(void* arg) { pthread_mutex_lock(&mutex); pthread_mutex_lock(&mutex); // 死锁 pthread_exit((void*)0); } int main() { pthread_mutex_init(&mutex, NULL); pthread_t tid; pthread_create(&tid, NULL, Routine, NULL); pthread_join(tid, NULL); pthread_mutex_destroy(&mutex); return 0; }
- 状态:
Sl+
(锁阻塞)。
- 状态:
-
阻塞原理:
- 进程等待资源时,从运行队列移至资源等待队列。
- 资源就绪后,唤醒并移回运行队列。
死锁的四个必要条件
- 互斥:资源独占。
- 请求与保持:持有资源并请求新资源。
- 不剥夺:资源不可强夺。
- 循环等待:线程间形成等待环。
避免死锁
- 破坏任一必要条件。
- 统一加锁顺序。
- 避免未释放锁。
- 一次性分配资源。
- 使用死锁检测或银行家算法。
Linux线程同步
同步概念与竞态条件
- 同步:在数据安全前提下,按特定顺序访问临界资源,避免饥饿。
- 竞态条件:时序问题导致程序异常。
- 问题:单纯加锁可能导致强竞争力线程垄断资源,其他线程饥饿。
条件变量
条件变量描述资源就绪状态,配合互斥锁实现同步:
- 等待:线程挂起等待条件满足。
- 唤醒:另一线程使条件满足并通知。
条件变量函数
-
初始化 :
cint pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
- 静态:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 静态:
-
销毁 :
cint pthread_cond_destroy(pthread_cond_t *cond);
-
等待 :
cint pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
-
唤醒 :
cint pthread_cond_signal(pthread_cond_t *cond); // 唤醒首个 int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒全部
示例:主线程控制子线程活动
c
#include <iostream>
#include <pthread.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void* Routine(void* arg) {
pthread_detach(pthread_self());
std::cout << (char*)arg << " run..." << std::endl;
while (true) {
pthread_cond_wait(&cond, &mutex);
std::cout << (char*)arg << "活动..." << std::endl;
}
}
int main() {
pthread_mutex_init(&mutex, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, Routine, (void*)"thread 1");
pthread_create(&t2, nullptr, Routine, (void*)"thread 2");
pthread_create(&t3, nullptr, Routine, (void*)"thread 3");
while (true) {
getchar();
pthread_cond_signal(&cond); // 逐个唤醒
// pthread_cond_broadcast(&cond); // 全部唤醒
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
为什么pthread_cond_wait需要互斥量
-
同步需求:条件需由另一线程改变共享数据触发。
-
死锁问题:若不释放锁,等待时持有锁导致死锁。
-
功能 :
- 等待时释放锁。
- 被唤醒时自动加锁。
-
错误设计 :
cpthread_mutex_lock(&mutex); while (condition_is_false) { pthread_mutex_unlock(&mutex); // 非原子 pthread_cond_wait(&cond); // 可能错过信号 pthread_mutex_lock(&mutex); }
条件变量使用规范
-
等待 :
cpthread_mutex_lock(&mutex); while (condition_is_false) pthread_cond_wait(&cond, &mutex); // 修改条件 pthread_mutex_unlock(&mutex);
-
唤醒 :
cpthread_mutex_lock(&mutex); // 设置条件为真 pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex);
总结
- 互斥:通过互斥量保护临界资源,确保数据一致性。
- 同步:条件变量配合锁实现有序访问,避免竞态和饥饿。
- 安全:理解可重入与线程安全,防范死锁。