linux系统编程——同步——互斥锁和条件变量
1. 互斥锁
互斥锁和条件变量 能用于 线程同步
如果 互斥锁 和 条件变量 存放在 共享内存中,还能 实现 进程同步
1.1 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 静态分配互斥锁
static pthread_mutex_t lock = PTHREAD_MUTEX_INITALIZER;
// 动态分配
pthread_mutex_t *lock2 = malloc(sizeof(*lock2));
phread_mutex_init()
1.2 上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
若资源已经被上锁,
- 调用 pthread_mutex_trylock,返回 EBUSY
- 调用 pthread_mutex_lock,被挂起。若多个线程被挂起,锁释放时,内核将唤醒 优先级最高的线程(所以必须设置不同的优先级给线程)
锁数据,不是函数
1.3 示例
生产者消费者问题
使用共享内存之类需要显示同步的情况
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define MAXNITEMS 0x0fffffff
#define MAXNTHREADS 100
int nitems;
struct {
pthread_mutex_t mutex;
int buf[MAXNITEMS];
int nput;
int nval;
} shared = {
.mutex = PTHREAD_MUTEX_INITIALIZER
};
void *produce(void *arg);
void *consume(void *arg);
int main(int argc, char *argv[])
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3) {
printf("usage : %s <items> <threads>\n", argv[0]);
return -1;
}
nitems = atoi(argv[1]) > MAXNITEMS ? MAXNITEMS : atoi(argv[1]);
nthreads = atoi(argv[2]) > MAXNTHREADS ? MAXNTHREADS : atoi(argv[2]);
for (i = 0; i < nthreads; i++) {
count[i] = 0; // 给每个线程一个计数器,记录线程 对共享内存区域操作了多少次
pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
for (i = 0; i < nthreads; i++) {
pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
pthread_create(&tid_consume, NULL, consume, NULL);
pthread_join(tid_consume, NULL);
return 0;
}
void *produce(void *arg)
{
for (;;) {
pthread_mutex_lock(&shared.mutex);
if (shared.nput >= nitems) { // 所有的区域都写完了,退出
pthread_mutex_unlock(&shared.mutex);
return NULL;
}
shared.buf[shared.nput] = shared.nval;
shared.nput++; // nput 和 nval 增量都是 1,且初始值都是 0, 所以没有冲突情况下 shared.buf[i] == i
shared.nval++;
pthread_mutex_unlock(&shared.mutex);
*(int *)arg += 1; // 增加线程操作计数
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; ++i) {
if (shared.buf[i] != i) { // 若出现冲突,则输出
printf("buf[%d] != %d\n", i, shared.buf[i]);
}
}
return NULL;
}
加锁输出
[root@VM-0-12-centos test]# ./a.out 1000000000 5
count[0] = 54572343
count[1] = 53444696
count[2] = 47935356
count[3] = 56164561
count[4] = 56318499
不加锁
[root@localhost test]# ./a.out 10000 4
count[0] = 8596
count[1] = 8192
count[2] = 0
count[3] = 0
buf[8978] != 8977
buf[9152] != 9151
对代码进行修改,让消费者和生产者一起工作
void consume_wait(int i)
{
for (;;) {
pthread_mutex_lock(&shared.mutex);
if (i < shared.nput) {
pthread_mutex_unlock(&shared.mutex);
return;
}
pthread_mutex_unlock(&shared.mutex);
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; ++i) {
consume_wait(i); // 消费 下标为 i 的元素前,检查是否被生产
if (shared.buf[i] != i) { // 若出现冲突,则输出
printf("buf[%d] != %d\n", i, shared.buf[i]);
}
}
return NULL;
}
1.4 互斥锁的不足
生产者使用轮询的方式检查条件,浪费cpu,所以希望 能阻塞 直到 条件满足。
这需要条件变量。
2. 条件变量
互斥锁用于 上锁,条件变量用于等待
条件变量类型:pthread_cond_t
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_wait,用于等待条件为真,指定相关条件变量的地址,和关联互斥锁的地址
pthread_cond_signal,与信号无关,用于通知条件已为真
2.1 使用
生产者 需要生产缓存,这里是 shared变量,由于多个生产者都要操作此变量,所以shared需要关联锁。
生产消费行为 需要同步,先有生产在有消费,所以需要计数器 nready.nready,而两个线程都要操作此计数器,所以需要锁 nready.mutex,若 消费者通过阻塞在锁上实现的同步 本质 还是轮询,所以需要通知机制,所以需要 条件变量 nready.cond,让消费者能阻塞在 条件
struct {
pthread_mutex_t mutex;
int buf[MAXNITEMS]; // 存放生产者输出数据的缓存
int nput; // next 生产数据缓存 索引
int nval; // next 生产数据的值得因子
} shared = {
.mutex = PTHREAD_MUTEX_INITIALIZER
};
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int nready;
} nready = {
.mutex = PTHREAD_MUTEX_INITIALIZER,
.cond = PTHREAD_COND_INITIALIZER
};
void *produce(void *arg)
{
for (;;) {
pthread_mutex_lock(&shared.mutex);
if (shared.nput >= nitems) { // 所有的区域都写完了,退出
pthread_mutex_unlock(&shared.mutex);
return NULL;
}
shared.buf[shared.nput] = shared.nval;
shared.nput++; // nput 和 nval 增量都是 1,且初始值都是 0, 所以没有冲突情况下 shared.buf[i] == i
shared.nval++;
pthread_mutex_unlock(&shared.mutex);
pthread_mutex_lock(&nready.mutex);
if (nready.nready == 0)
pthread_cond_signal(&nready.cond);
nready.nready++;
pthread_mutex_unlock(&nready.mutex);
*(int *)arg += 1;
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; ++i) {
pthread_mutex_lock(&nready.mutex);
while (nready.nready == 0)
pthread_cond_wait(&nready.cond, &nready.mutex);
nready.nready--;
pthread_mutex_unlock(&nready.mutex);
if (shared.buf[i] != i) { // 若出现冲突,则输出
printf("buf[%d] != %d\n", i, shared.buf[i]);
}
}
return NULL;
}
pthread_cond_wait :
- 调用时,完成两个工作
释放锁
线程进入睡眠,直到内核收到相关条件变量的通知操作,而唤醒线程 - 返回时(被通知唤醒),完成工作
尝试获得锁,若失败,阻塞在锁上
由于 pthread_cond_wait 返回前会尝试获得锁,若此时锁被 其他线程持有,被唤醒且被调度的线程将再次进入阻塞,为避免这种情况,可修改上面代码。
int dosignal;
phtread_mutex_lock(&nready.mutex);
dosignal = (nready.nready == 0);
nready.nready++;
phtread_mutex_unlock(&nready.mutex);
if (dosignal)
pthread_cond_signal(&nready.cond);
2.2 总结
条件变量的使用大致如下:
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
维护本条件的各个变量
} var = {
.mutex = PTHREAD_MUTEX_INITIALIZER,
.cond = PTHREAD_COND_INITIALIZER
};
pthread_mutex_lock(&var.mutex);
设置条件为真
pthread_cond_signal(&var.cond);
pthread_mutex_unlock(&var.mutex);
pthread_mutex_lock(&var.mutex);
while (条件为假)
pthread_cond_wait(&var.cond, &var.mutex);
修改条件
pthread_mutex_unlock(&var.mutex);
2.4 定时等待和广播
pthread_cond_signal 只唤醒一个线程,若要唤醒多个线程,应使用 pthread_cond_broadcast。
- 为什么需要唤醒多个线程
如生产消费者模型,当生产者完成的生产足够满足多个消费者,则应该唤醒多个线程 - 推荐始终使用广播
使用单播能优化程序,但由于必须明确每个等待线程,且唤醒哪个等待线程无关紧要。所以使用广播更安全,简单。
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
struct timespec {
time_t tv_sec; // sec
time_t tv_nsec; // nanosec
pthread_cond_timedwait 可以设置超时时间,若超时则返回 ETIME。
超时是绝对时间,不是相对时间。
3. 互斥量和条件变量属性
3.1 静态初始化
用于线程间同步,我们使用 常量值 PTHREAD_MUTEX_INITIALIZER 和 PTHREAD_COND_INITIALIZER 初始化 互斥量 和 条件变量。
这种方式让其 使用默认属性。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
3.2 动态初始化
使用如下函数,初始化和销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
attr参数若为NULL,则使用默认值初始化。
通过如下函数 初始化和销毁 attr
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr);
3.3 启用属性
一旦 互斥锁属性对象 条件变量属性对象 已初始化,就调用 不同函数启用或禁止特定属性。
如开启进程间共享的函数
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int value);
int pthread_condattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);
int pthread_condattr_setpshared(pthread_mutexattr_t *attr, int value);
value 的值可以是:
PTHREAD_PROCESS_PRIVATE 或 PTHREAD_PROCESS_SHARED
示例
pthread_mutex_t *mptr;
pthread_mutexattr_t mattr;
mptr = /* 进程间共享内存区 分配的 pthread_mutex_t 对象 */
pthread_mutexattr_init(&mattr);
pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(mptr, &mattr);
4. 持有锁期间进程终止
一个进程或线程持有锁,若持有锁期间被终止,锁是否被系统自动释放?
4.1 进程间共享
- 互斥锁 : 不会
- 读写锁 : 不会
- fnctl记录锁 : 进程终止时,内核自动清理锁
- systemV信号量 :应用程序可以选择进程终止时,内核是否自动清理某个信号量(SEM_UNDO)
- POSIX信号量 : 不会
4.2 线程间共享
当一个线程持有锁时,其可能退出,如 另一个线程取消他,或他自己调用 pthread_exit。
- 自愿(pthread_exit):线程应该知道自己持有锁,应该自己释放
- 非自愿(另一个线程取消):线程可以安装退出清理函数释放持有锁
若由于段错误等原因导致整个进程退出,就回到进程间共享情况
4.3 内核释放锁的意义
若进程退出,锁由内核释放,通常是没意义的,因为同步区数据是脏的。
但以下情况,内核释放锁是有意义的:
- 服务器可能使用一个 systemv信号量(打开SEM_UNDO)来统计当前处理客户数,如 fork一个子进程,信号量加一,子进程终止时,内核将信号量减一
- 守护进程希望系统只运行一个实例,守护进程在开始时在自己某个数据文件获得写入锁,然后运行期间持有锁,若有人试图启动守护进程的另外副本,则新副本由于无法持有锁而退出,若守护进程终止时,内核释放写入锁,从而允许启动该守护进程的副本。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?