线程同步(Thread Synchronization)
一句话定义:
线程同步(Thread Synchronization) 就是多个线程之间有秩序地访问共享资源,避免“抢”数据、乱套、程序崩掉。
多个线程同时运行的时候,如果它们访问同一份数据(共享资源),可能会出问题
1. 竞态条件和锁
竞态(Race Condition)是什么?
“竞态”又叫竞态条件(Race Condition),是并发/多线程编程里最常见、也最危险的问题之一。
当多个线程或进程“同时”访问/修改同一个资源(变量、文件、内存)**时,
如果操作之间**没有合适的同步机制(加锁),就会出现执行顺序不确定的现象,
最终导致程序结果错误或行为异常。
这就叫:竞态。
举个例子:
假设有两个线程,同时对一个变量 num 做 num++:
// 初始值 num = 0
Thread A: 读取 num -> 0
Thread B: 读取 num -> 0
Thread A: num = 0 + 1 = 1
Thread B: num = 0 + 1 = 1 ❌
最终结果:num = 1,但应该是 2!
本来 num 应该加两次变成 2,结果因为两个线程同时读了 0,最后被覆盖了,变成了 1,这就是一个典型的竞态。
遇到这种情况怎么办?别急,这正是我们接下来要介绍的重点——锁的使用。
为了帮助大家更好地理解,我会结合几个实际例子,带你认识几种常用的锁类型,搞清楚它们分别适合哪些场景,用起来又有什么区别。
2.互斥锁
1. 互斥锁(Mutex)简介
在多线程编程中,互斥锁(Mutex,Mutual Exclusion) 是最基础也是最常用的一种线程同步机制。
它的作用是:
一次只允许一个线程访问某一段关键代码,其他线程必须等待,避免共享资源被同时修改,从而防止竞态条件(Race Condition)。
就像排队打水,一个水龙头只能一个人用,别人必须等前面的人用完。
2.代码示例
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define THREAD_COUNT 20000
static pthread_mutex_t counter_mutex=PTHREAD_MUTEX_INITIALIZER;
//创建多个线程
void*add(void * arg)
{
//转化传入参数
int *p=(int*)arg;
//在累加之前,上一个锁,保证同一时间只有一个线程对其累加
pthread_mutex_lock(&counter_mutex);
(*p)++;
//累加完之后释放锁
pthread_mutex_unlock(&counter_mutex);
return (void *) 0;
}
int main(int argc, char const *argv[])
{
pthread_t pid[THREAD_COUNT];
int num=0;
for(size_t i=0;i<THREAD_COUNT;i++)
{
//创建的线程功能是给传入的参数累加1
pthread_create(pid+i,NULL,add,&num);
}
//等待所有线程执行完成
for(size_t i=0;i<THREAD_COUNT;i++)
{
pthread_join(pid[i],NULL);
}
//打印最终累加结果
printf("累加结果:%d\n", num);
return 0;
}
输出结果如下:

3.读写锁
1.读写锁(Read-Write Lock)简介
在多线程中,有些共享资源读取操作远多于写入操作,如果每次都用互斥锁来限制访问,就显得太保守了。
这时候,读写锁就登场了。
✅ 多个线程可以同时读共享资源
❌ 写线程必须独占,不能同时有读或写线程
简单理解就是:
- 多个“读者”可以一起看书 📖
- 但只要有人要写字 ✍️,所有人都必须放下书等他写完!
2.代码示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//static pthread_rwlock_t rwlock=__PTHREAD_RWLOCK_INITIALIZER;
pthread_rwlock_t rwlock;
int shared_data = 0;
void*lock_read(void*arg)
{
//读写锁的读是可以由多个线程统一读取的
//获取读锁
pthread_rwlock_rdlock(&rwlock);
printf("当前是%s,shared_data为:%d\n",(char*)arg,shared_data);
pthread_rwlock_unlock(&rwlock);
}
void*lock_write(void*arg)
{
//给多个线程写入添加写锁
//同一时间只有一个线程获取写锁,会造成两个线程顺序执行
pthread_rwlock_wrlock(&rwlock);
int tmp=shared_data+1;
sleep(1);
shared_data=tmp;
printf("当前是%s,shared_data为:%d\n",(char*)arg,shared_data);
//写入完成之后释放写锁
pthread_rwlock_unlock(&rwlock);
}
int main(int argc, char const *argv[])
{
//显示初始化读写锁
pthread_rwlock_init(&rwlock,NULL);
pthread_t write1,write2,reader1, reader2, reader3, reader4, reader5, reader6;
//创建两个写线程
pthread_create(&write1,NULL,lock_write,"write1");
pthread_create(&write2,NULL,lock_write,"write2");
//休眠等待
sleep(3);
pthread_create(&reader1,NULL,lock_read,"reader1");
pthread_create(&reader2,NULL,lock_read,"reader2");
pthread_create(&reader3,NULL,lock_read,"reader3");
pthread_create(&reader4,NULL,lock_read,"reader4");
pthread_create(&reader5,NULL,lock_read,"reader5");
pthread_create(&reader6,NULL,lock_read,"reader6");
//主线程等待创建的子线程运行完成
pthread_join(write1, NULL);
pthread_join(write2, NULL);
pthread_join(reader1, NULL);
pthread_join(reader2, NULL);
pthread_join(reader3, NULL);
pthread_join(reader4, NULL);
pthread_join(reader5, NULL);
pthread_join(reader6, NULL);
//销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
输出结果如下图所示:

我觉得读写锁是一种“变身”
比如创建一个 a,它可以变成
bc的锁,也就是读模式、写模式,最后释放锁将bc还原成原来的锁也就是a
我们可以这样理解:
a是一个 锁的变量(如rwlock)pthread_rwlock_rdlock(&a):“读模式”=bpthread_rwlock_wrlock(&a):“写模式”=cpthread_rwlock_unlock(&a):“释放”=(b,c)-> a
3.写线程“饿死”了?
在上面代码中我们通过手动设置睡眠让读写进程顺利进行,如果我们没有加入睡眠会出现这种情况:

进程顺序变化不一默认情况下,读写锁是读优先的,所以线程执行的顺序可能并不固定。
这时候就有个问题了:
假如有个线程一直想写点东西,但别的线程一个接一个地抢着来读,它就只能一边干瞪眼一边排队……
写不进去、插不进队,这种被“饿”到的情况,就是“写饥饿”。
这时候我们可以请出:pthread_rwlockattr_t —— 它是读写锁的属性结构体,
可以用来配置锁的行为策略,比如:改成“写优先”或公平策略,从而缓解或避免“写饥饿”问题。
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock,&attr);
pthread_rwlockattr_destroy(&attr);
在原基础代码上,添加代码,就可以让写的权限大于读,就能解决写饥饿现象。
pthread_rwlockattr_t就像是“配置权限的一张表”,你在上面勾选(比如写优先),然后拿这张表去创建锁(初始化锁),一旦锁创建好了,这张表的作用就结束了,就可以安全地删掉(destroy)。
4.自旋锁
1.自旋锁简介(spinlock)
什么是自旋锁?
自旋锁(Spinlock) 是一种轻量级锁机制,用于线程同步。当一个线程尝试获取锁时:
- 如果锁已经被其他线程占用,它不会睡眠阻塞;
- 而是会不断“自旋”等待锁的释放(即在原地不停地循环尝试获取锁)。
这就像是:
“我就在门口等,门一开我就冲进去,而不是去睡觉等通知。”
使用场景
- 适用于锁占用时间非常短的临界区。
- 如果锁占用时间长,自旋会导致 CPU 空转浪费资源。
自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销。不能在用户空间使用,这里就不多介绍了。
5.补充!!!
1.条件变量
条件变量(condition variable)和互斥锁(mutex)常常是一起使用的,它们是多线程编程中实现线程同步和通信的重要工具
互斥锁用来保护“访问共享资源”的原子性,条件变量用来“等待某个条件的发生”。
只用锁,线程只能“傻等”(轮询),容易浪费资源;
条件变量让线程可以“聪明地等”,别人来通知你再干活,效率高又优雅。
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化条件变量
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//期望功能是读或写的一方 一直进行读写操作 直到缓存读满或者写满 暂时释放锁
//写数据
void * producer(void *arg)
{
int item=1;
pthread_mutex_lock(&mutex);
while (1)
{
//获取锁
//如果缓存区写满,使用条件变量暂停当前线程
if(count==BUFFER_SIZE)
{
//暂停线程
pthread_cond_wait(&cond,&mutex);
}
buffer[count++]=item++;
printf("我发送了一个数字%d\n",buffer[count-1]);
//唤醒消费者
pthread_cond_signal(&cond);
}
//解锁
pthread_mutex_unlock(&mutex);
}
//读数据
void *consumer(void*arg)
{
pthread_mutex_lock(&mutex);
while (1)
{
if(count==0)
{
//无数据可读
//暂停线程
pthread_cond_wait(&cond,&mutex);
}
printf("我收到的数字%d\n",buffer[--count]);
pthread_cond_signal(&cond);
}
pthread_mutex_unlock(&mutex);
}
int main(int argc, char const *argv[])
{
//创建两个线程,一个从buf中写,一个从buf中读
pthread_t producer_thread,consumer_thread;
pthread_create(&producer_thread,NULL,producer,NULL);
pthread_create(&consumer_thread,NULL,consumer,NULL);
//主线程等待两个线程完成
pthread_join(producer_thread,NULL);
pthread_join(consumer_thread,NULL);
return 0;
}
输出结果如下:

2.信号量
锁是用来保证线程安全访问共享资源的;信号量决定了线程该不该继续干活。
信号量是线程之间沟通的信号,告诉线程:干活,还是等一等。
有名信号量 vs 无名信号量 区别:
| 项目 | 有名信号量 (sem_open) | 无名信号量 (sem_init) |
|---|---|---|
| 是否有名字 | ✅ 有名字,系统中全局可见 | ❌ 没有名字,只在内存中可用 |
| 使用方式 | 类似文件,通过 sem_open("/name", ...) 打开 | 直接初始化 sem_t 结构体 |
| 是否需要共享内存 | 否,自动在系统中维护 | 是,若用于进程间通信需使用共享内存(如 mmap) |
| 生命周期 | 独立于进程,需显式 sem_unlink() 删除 | 随变量生命周期自动销毁 |
| 适用场景 | 多进程同步,跨程序同步 | 线程同步或使用共享内存的进程同步 |
接下来对无名信号量介绍:
2.1无名信号量
1)作为二进制信号量用于线程间通信
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
sem_t unnamed_sem;
int shard_num = 0;
void *plusOne(void *argv) {
sem_wait(&unnamed_sem);
int tmp = shard_num + 1;
shard_num = tmp;
sem_post(&unnamed_sem);
}
int main() {
sem_init(&unnamed_sem, 0, 1);//第二个参数是0表示线程,1表示进程,第三个参数表示几个资源可用
pthread_t tid[10000];
for (int i = 0; i < 10000; i++) {
pthread_create(tid + i, NULL, plusOne, NULL);
}
for (int i = 0; i < 10000; i++) {
pthread_join(tid[i], NULL);
}
printf("shard_num is %d\n", shard_num);
sem_destroy(&unnamed_sem);
return 0;
}
疑问?
信号量也能实现互斥锁
那为什么还要专门用互斥锁?
虽然信号量能实现互斥锁功能,但互斥锁是专为互斥而设计的同步机制,在某些场景下更合适。
| 项目 | 信号量(Semaphore) | 互斥锁(Mutex) |
|---|---|---|
| 功能 | 通用同步工具 | 专门用于线程间互斥访问 |
| 初始值 | 可以是任意非负整数 | 通常只有锁定/释放两个状态 |
| 线程归属 | 没有线程所有权概念 | 有线程所有权,谁加锁谁解锁 |
| 解锁限制 | 任意线程可以调用 post() | 只有加锁线程才能解锁(更安全) |
| 使用复杂度 | 稍复杂 | 简洁直接,适合纯粹互斥 |
| 典型用法 | 资源池、生产者消费者模型等 | 临界区保护 |
什么时候该用信号量 vs 用互斥锁?
| 场景 | 推荐使用 |
|---|---|
| 保护一个临界区,防止并发访问 | 互斥锁(mutex) |
| 资源池中有多个资源(比如 10 个) | 计数信号量 |
| 实现线程或进程之间的同步关系 | 信号量 |
信号量是万能工具,互斥锁是专用工具。
如果你只是想实现“只能一个线程访问某段代码”的功能,互斥锁更合适更安全;如果你有更复杂的同步需求(比如资源计数、线程等待唤醒),用信号量。
2)作为二进制信号量用于进程间通信
“使用共享内存与命名信号量实现父子进程同步与数据共享”
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
char *shm_sem_name = "unnamed_sem_shm_sem";
char *shm_value_name = "unnamed_sem_shm_value";
// 创建内存共享对象
int sem_fd = shm_open(shm_sem_name, O_CREAT | O_RDWR, 0666);
int value_fd = shm_open(shm_value_name, O_CREAT | O_RDWR, 0666);
// 调整内存共享对象的大小
ftruncate(sem_fd, sizeof(sem_t));
ftruncate(value_fd, sizeof(int));
// 将内存共享对象映射到共享内存区域
sem_t *sem = mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED, sem_fd, 0);
int *value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, value_fd, 0);
// 初始化信号量和共享变量的值
sem_init(sem, 1, 1);
*value = 0;
int pid = fork();
if (pid > 0)
{
sem_wait(sem);
int tmp = *value + 1;
sleep(1);
*value = tmp;
sem_post(sem);
// 等待子进程执行完毕
waitpid(pid, NULL, 0);
printf("this is father, child finished\n");
printf("the final value is %d\n", *value);
}
else if (pid == 0)
{
sem_wait(sem);
int tmp = *value + 1;
sleep(1);
*value = tmp;
sem_post(sem);
}
else
{
perror("fork");
}
// 父进程执行到这里,子进程已执行完毕,可以销毁信号量
if (pid > 0)
{
if (sem_destroy(sem) == -1)
{
perror("sem_destory");
}
}
// 无论父子进程都应该解除共享内存的映射,并关闭共享对象的文件描述符
if (munmap(sem, sizeof(sem)) == -1)
{
perror("munmap sem");
}
if (munmap(value, sizeof(int)) == -1)
{
perror("munmap value");
}
if (close(sem_fd) == -1)
{
perror("close sem");
}
if (close(value_fd) == -1)
{
perror("close value");
}
// 如果调用时别的进程仍在使用共享对象,则等待所有进程释放资源后,才会销毁相关资源。
// shm_unlink只能调用一次,这里在父进程中调用shm_unlink
if (pid > 0)
{
if (shm_unlink(shm_sem_name) == -1)
{
perror("father shm_unlink shm_sem_name");
}
if (shm_unlink(shm_value_name) == -1)
{
perror("father shm_unlink shm_value_name");
}
}
return 0;
使用方法是类似的,输出结果如下:

3)作为计数信号量用于线程间通信
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
sem_t *full;
sem_t *empty;
int shard_num;
int rand_num()
{
srand(time(NULL));
return rand();
}
void *producer(void *argv)
{
for (int i = 0; i < 5; i++)
{
sem_wait(empty);
printf("\n==========> 第 %d 轮数据传输 <=========\n\n", i + 1);
sleep(1);
shard_num = rand_num();
printf("producer has sent data\n");
sem_post(full);
}
}
void *consumer(void *argv)
{
for (int i = 0; i < 5; i++)
{
sem_wait(full);
printf("consumer has read data\n");
printf("the shard_num is %d\n", shard_num);
sleep(1);
sem_post(empty);
}
}
int main()
{
full = malloc(sizeof(sem_t));
empty = malloc(sizeof(sem_t));
sem_init(empty, 0, 1);
sem_init(full, 0, 0);
pthread_t producer_id, consumer_id;
pthread_create(&producer_id, NULL, producer, NULL);
pthread_create(&consumer_id, NULL, consumer, NULL);
pthread_join(producer_id, NULL);
pthread_join(consumer_id, NULL);
sem_destroy(empty);
sem_destroy(full);
return 0;
}
输出结果如下:

这里我对这段代码做简单讲解:
void *producer(void *argv)
{
for (int i = 0; i < 5; i++)
{
sem_wait(empty);
printf("\n==========> 第 %d 轮数据传输 <=========\n\n", i + 1);
sleep(1);
shard_num = rand_num();
printf("producer has sent data\n");
sem_post(full);
}
}
void *consumer(void *argv)
{
for (int i = 0; i < 5; i++)
{
sem_wait(full);
printf("consumer has read data\n");
printf("the shard_num is %d\n", shard_num);
sleep(1);
sem_post(empty);
}
}
在前面我们定义了
empty为1,full为0sem_init(empty, 0, 1);
sem_init(full, 0, 0);
sem_wait()可以理解为-1,sem_post()为+1,当小于0的时候阻塞这里我们对empty和full都-1,发现full<0所以
consumer()函数阻塞,也就是先发送,之后post让full+1这时候
full为1,empyt为0,这样我们就能交替运行了
4)作为计数信号量用于进程间通信
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
char *shm_name="unnamed_sem_shm";
int fd=shm_open(shm_name,O_CREAT|O_RDWR,0644);
ftruncate(fd,sizeof(sem_t));
//完成映射
sem_t *sem= mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
//初始化信号量
sem_init(sem,1,0);
//创建父子进程
pid_t pid=fork();
if(pid<0)
{perror("fork");}
else if(pid==0)
{
//子进程
sleep(1);
printf("这是子进程\n");
sem_post(sem);
}
else
{
//父进程
sem_wait(sem);
printf("这是父进程\n");
waitpid(pid,NULL,0);
}
if(pid>0)
{
if((sem_destroy(sem))==-1)
{
perror("sem_destroy");
}
}
if(munmap(sem,sizeof(sem))==-1)
{
perror("munmap");
}
if(close(fd)==-1)
{
perror("close");
}
if(pid>0)
{
if(shm_unlink(shm_name)==-1)
{
perror("shm_unlink");
}
}
return 0;
}
输出结果如下:

接下来对有名信号量介绍:
2.2有名信号量
有名信号量与无名信号量创建方式不填一样,但逻辑基本一致,这里只展示进程间的通讯
1)有名信号量用作二进制信号量
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
char *sem_name = "/named_sem";
char *shm_name = "/named_sem_shm";
// 初始化有名信号量
sem_t *sem = sem_open(sem_name, O_CREAT, 0666, 1);
// 初始化内存共享对象
int fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
// 调整内存共享对象的大小
ftruncate(fd, sizeof(int));
// 将内存共享对象映射到内存空间
int *value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 初始化共享变量指针指向位置的值
*value = 0;
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
}
sem_wait(sem);
int tmp = *value + 1;
sleep(1);
*value = tmp;
sem_post(sem);
// 每个进程都应该在使用完毕后关闭对信号量的连接
sem_close(sem);
if (pid > 0)
{
waitpid(pid, NULL, 0);
printf("子进程执行结束,value = %d\n", *value);
// 有名信号量的取消链接只能执行一次
sem_unlink(sem_name);
}
// 父子进程都解除内存共享对象的映射,并关闭相应的文件描述符
munmap(value, sizeof(int));
close(fd);
// 只有父进程应该释放内存共享对象
if (pid > 0)
{
if (shm_unlink(shm_name) == -1)
{
perror("shm_unlink");
}
}
return 0;
}
输出结果如下:

通过信号量避免了竞态条件
2)有名信号量用作计数信号量
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
char *sem_name = "/named_sem";
// 初始化有名信号量
sem_t *sem = sem_open(sem_name, O_CREAT, 0666, 0);
pid_t pid = fork();
if (pid > 0) {
sem_wait(sem);
printf("this is father\n");
// 等待子进程执行完毕
waitpid(pid, NULL, 0);
// 释放引用
sem_close(sem);
// 释放有名信号量
if(sem_unlink(sem_name) == -1) {
perror("sem_unlink");
}
} else if(pid == 0) {
sleep(1);
printf("this is son\n");
sem_post(sem);
// 释放引用
sem_close(sem);
} else
{
perror("fork");
}
return 0;
}
输出结果如下:

总结
本文围绕竞态现象,介绍了多种常见的线程同步机制,包括互斥锁、读写锁、自旋锁的使用方法和适用场景,同时补充了条件变量与信号量的基本概念和用法。希望这篇文章能对你有所帮助,助你更好地学习和理解相关内容。
2066






