18.4 线程同步
18.4 线程同步
相关信息
线程同步用于解决线程访问顺序引发的问题。
互斥量 Mutual Exclusion
相互排斥
也叫做互斥锁
互斥锁的作用:保证同一时间只有一个线程能够访问同一块临界区(造成异常的代码)
使用mutex的逻辑:
- 在访问临界区前进行lock,访问结束后进行unlock。
- 如果线程0访问后没有unlock,那么线程1会一直等待解锁,造成线程1无法访问临界区 - 死锁。
- 多个线程使用的是同一个mutex,通过该mutex的状态判断临界区的访问情况。
查看mutex相关函数帮助
- 查询pthread_mutex_init 的帮助,提示manual 中没有对应entry的信息。
mingstudent@mingstudent:/mnt/c/Users/mingstudent/Desktop$ man pthread_mutex_init
No manual entry for pthread_mutex_init- 使用下面的命令安装POSIX 开发手册
sudo apt install manpages-posix-dev- 安装成功后再次查询
PROLOG
This manual page is part of the POSIX Programmer's Manual. The Linux implementation of this interface may
differ (consult the corresponding Linux manual page for details of Linux behavior), or the interface may not
be implemented on Linux.
NAME
pthread_mutex_init — destroy and initialize a mutex
SYNOPSIS
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;mutex的类型为:pthread_mutex_t
创建一个mutex的方式有两种:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;- 1、2行,使用函数进行初始化
- 使用宏返回的无属性配置(attr = NULL)的mutex
问:这个宏具体是一个函数还是一个特殊的值?是在编译时期完成 还是 运行时器生成?
推荐使用pthread_mutex_init 函数进行初始化
因为可以通过函数返回值判断初始化是否成功
测试
实现:
pthread_mutex_t mutex;
void* thread_inc(void* arg)
{
long i;
pthread_mutex_lock(&mutex);
for(i=0; i<5000000000; i++)
num++;
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tid0, tid1;
pthread_mutex_init(&mutex, NULL);
//。。。
}| 行号 | 功能 | 说明 |
|---|---|---|
| 1 | 定义用于锁定临界区的mutex | 全局变量,多线程共享 |
| 6、9 | 给临界区上锁、解锁 | |
| 15 | 使用mutex前初始化mutex |
效果:
ming@ubuntu:/media/sf_share/Network/build$ ./Network
num: 0
ming@ubuntu:/media/sf_share/Network/build$ ./Network
num: 0加了锁之后多线程不会同时修改全局变量num,多次运行结果正确。
临界区的范围
- 只要包含了可能引起问题的代码的区域就可以是临界区
- =》临界区的范围可大可小
for(i=0; i<5000000000; i++)
num++;比如上面的代码中,临界区可以是1、2 两条语句,也可以是第2行 - 单条语句。
加锁的操作可以是对 for循环 和 num操作 一起加锁,也可以是仅对num操作加锁
for(i=0; i<5000000000; i++)
{
pthread_mutex_lock(&mutex);
num++;
pthread_mutex_unlock(&mutex);
}两者的区别:
| 方式 | lock、unlock次数 | 特点 | 时间 |
|---|---|---|---|
| for + num :循环前后加锁、解锁 | 1 | 相当于其中一个线程要完全等待另一个线程执行完对num的操作然后才可以访问 ”最大限度减少互斥量lock、unlock函数调用的次数“,对互斥锁的操作也需要运算 | begin:1769273439 - 2026-01-25 00:50:39 end: 1769273462 - 2026-01-25 00:51:02 共:23s |
| num:循环内 | 循环次数 | 两个线程交替对num进行操作 | begin:1769273575 - 2026-01-25 00:52:55 end: 1769273730 - 2026-01-25 00:55:30 共:155s |
两种方式运行时间相差132s,可以看到如果循环次数过多,频繁lock、unlock 严重影响性能。
增加时间戳打印:
#include<time.h>
int main()
{
time_t now = time(NULL);
printf("begin time: %ld\n", (long)now);
}| 行号 | 功能 | 说明 |
|---|---|---|
| 4 | time() returns the time as the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). | time返回距离Epoch的秒数 |
用在线工具转换成时分秒格式
信号量 semaphore
sem_init
NAME
sem_init - initialize an unnamed semaphore
SYNOPSIS
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);| 参数 | 值 | 用途 | 说明 |
|---|---|---|---|
| sem | sem_init() initializes the unnamed semaphore at the address pointed to by sem. | 保存信号量的变量地址 | |
| pshared | 0; nonzero | The pshared argument indicates whether this semaphore is to be shared between the threads of a process, or between processes. If pshared has the value 0, then the semaphore is shared between the threads of a process, and should be located at some address that is visible to all threads ... | 信号量是在一个进程内的线程之间共享,还是在多个进程之间共享 |
| value | The value argument specifies the initial value for the semaphore. | 信号量的初始值 | |
| RETURN | 0; -1 | returns 0 on success; on error, -1 is returned, and errno is set to indicate the error. |
sem_post
NAME
sem_post - unlock a semaphore
SYNOPSIS
#include <semaphore.h>
int sem_post(sem_t *sem);
Link with -pthread.
DESCRIPTION
sem_post() increments (unlocks) the semaphore pointed to by sem. If the semaphore's value consequently becomes greater than zero, then another process or thread blocked in a sem_wait(3) call will be woken up and proceed to lock the semaphore.- 信号量有一个初始值,类型为unsigned int ( >= 0 )
- post 将 semaphore 加1, wait 将 semaphore 减1
- 信号量的值不能小于0
- 如果当前sempahore的值是0,使用post后,阻塞在sem_wait()的线程会被唤醒 - 执行,同时semaphore 被lock
sem_wait
NAME
sem_wait, sem_timedwait, sem_trywait - lock a semaphore
SYNOPSIS
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
Link with -pthread.
DESCRIPTION
sem_wait() decrements (locks) the semaphore pointed to by sem. If the semaphore's value is greater than zero, then the decrement proceeds, and the function returns, immediately. If the semaphore currently has the value zero, then the call blocks until either it becomes possible to perform the decrement (i.e., the semaphore value rises above zero), or a signal handler interrupts the call.
sem_trywait() is the same as sem_wait(), except that if the decrement cannot be immediately performed, then call returns an error (errno set to EAGAIN) instead of blocking.判断semaphore的值 是否 为正。 如果 为正(>0),将semaphore减1 并 立即返回,处理临界区。如果 == 0 阻塞等待直到 semaphore的值为0
使用semaphore的过程
在临界区前调用wait等待semaphore的值 >0,使用完临界区后调用post增加semaphore的值
对信号量的理解
信号量的初始值可以看作是可用资源的数量。
| 情景 | mutex | semaphore |
|---|---|---|
| 厕所 | 家里的厕所,一个人使用其余人等待 | 公共厕所,可用有多个小的隔间 或者 坑位😏 |
将临界区看作资源,每次使用前都需要确认有资源可用。
当semaphore的最大值为1时和mutex 是一个效果。
测试
实现:
sem_t sem;
void* thread_inc(void* arg)
{
long i;
sem_wait(&sem);
for(i=0; i<5000000000; i++)
num++;
sem_post(&sem);
return NULL;
}
int main(int argc, char* argv[])
{
sem_init(&sem, 0, 1);
//。。。
}效果:
耗时
begin:1769345034 2026-01-25 20:43:54
end: 1769345056 2026-01-25 20:44:16
共22s 和 使用mutex的效率基本一致
指定访问同一内存空间的线程执行顺序
mutex 和 semaphore 除了可以避免多线程同时访问同一内存空间,还可以指定多线程访问的顺序。
书中的示例:
线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加。该过程共进行5次,完成后输出总和并退出程序。
线程A、B的执行有先后顺序。
使用semaphore 需要明确定义semaphore 表示的含义
相关信息
为了完成上述要求构建程序,应按照线程A、线程B的顺序访问变量num,且需要线程同步
实现:
sem_t sem_read;
sem_t sem_cal;
int num = 0;
void* read(void *)
{
for(int i=0; i < 5; i++)
{
sem_wait(&sem_read);
printf("please input the number:");
scanf("%d", &num);
sem_post(&sem_cal);
}
return NULL;
}
void* cal(void *)
{
int sum = 0;
for(int i=0; i < 5; i++)
{
sem_wait(&sem_cal);
sum += num;
printf("current sum = %d\n", sum);
sem_post(&sem_read);
}
printf("sum = %d\n", sum);
return NULL;
}
int main(int argc, char* argv[])
{
sem_init(&sem_read, 0, 1);
sem_init(&sem_cal, 0, 0);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, read, NULL);
pthread_create(&tid2, NULL, cal, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&sem_read);
sem_destroy(&sem_cal);
return 0;
}| 行号 | 功能 | 说明 |
|---|---|---|
| 33 | 资源:获取用户输入的次数;是否可以获取用户输入 | |
| 34 | 资源:是否可以进行计算;是否已获取到用户输入 |
计算总和的线程相当于消费者,获取用户输入的线程相当于生产者。只有在生产者生产了资料 - 这里是num,消费者才可以进行消费。这样就确定了线程对num的访问顺序。
效果:
ming@ubuntu:/media/sf_share/Network/build$ ./Network
please input the number:4
current sum = 4
please input the number:8
current sum = 12
please input the number:-4
current sum = 8
please input the number:-2
current sum = 6
please input the number:5
current sum = 11
sum = 11问:为什么要用两个semaphore来进行同步?一个行不行?
- semaphore 只在值为0时对sem_wait()进行阻塞 - 无法访问临界区(保护临界区),在大于0时不会阻塞。
如果cal 和 read 线程都是wait -》访问 -》post,那么无法确定cal 和 read的执行顺序。
- semaphore 初始值是1,可能cal先启动,导致计算的次数少了1次
- 如果在read中只使用 post 增加 semaphore,在cal中只使用wait 减小 semaphore,无法对临界区进行保护
void* read(void *)
{
for(int i=0; i < 5; i++)
{
printf("please input the number:");
scanf("%d", &num);
sem_post(&sem_read);
}
return NULL;
}
void* cal(void *)
{
int sum = 0;
for(int i=0; i < 5; i++)
{
sem_wait(&sem_read);
sum += num;
}
printf("sum = %d\n", sum);
return NULL;
}read 和 cal 可能同时访问num
- 如果要让read一直读取输入满5个,而不是等cal计算一次再读取,可以结合mutex对临界区上锁
semaphore 用来控制访问顺序,mutex用来保护临界区