本文目录
前述:同步机制的引入及概念
为了更全面的学习,在学习本章之前,请查看《线程的创建与使用》这篇文章。
进程中的多线程之间使用的是共享的数据,那么如果多个线程同时操作同一个共享资源,那么岂不是很混乱了。为了解决这个问题,引入了线程间的同步机制,即每次对共享资源进行操作时,只能有一个线程操作,其他线程必须等待其操作完毕后再进行操作。
线程的同步机制是指在多线程编程中,用来协调线程之间的操作,以确保它们按预期顺序执行,避免竞态条件、死锁和其他并发问题。同步机制的主要目的是管理对共享资源的访问,确保数据一致性和程序的正确性。
序号 | 机制 | 描述 |
---|---|---|
1 | 互斥锁 | 互斥锁的主要作用是同步,确保同一时刻只有一个线程能够进入临界区,从而避免竞态条件。例如,多个线程需要访问或修改同一个共享变量时,可以用互斥锁保护这个变量。 |
2 | 条件变量 | 条件变量用于线程等待某个条件成立,通常与互斥锁结合使用。 |
3 | 信号量/灯 | 信号量是一个计数器,用于控制对资源的访问,允许多个线程同时访问一定数量的资源。 |
一、互斥锁
1. 定义
在 Posix Thread 中定义了一套专门用于线程互斥的 mutex 函数。mutex 是一种简单的加锁的方法来控制对共享资源的存取。这个互斥锁只有两种状态(上锁和解锁)。
问题:为什么需要对共享资源加锁?
答:因为多个线程共用进程的资源,要访问的是公共区间时(全局变量),当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,以实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程才能够对共享资源进行操作。若其他线程希望上锁一个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。
注意:要使用互斥锁时,一定要避免死锁的出现!死锁是指两个或多个线程相互等待对方释放资源,从而导致所有线程都无法继续执行。避免死锁是多线程编程中的一个重要问题。且不应该在信号处理函数中使用互斥锁。
2. 互斥锁常用方法
互斥锁类型: ( 1)快速锁(普通锁),(2)嵌套锁(递归锁),(3)检错锁。 这里我们使用最多的就是快速锁,即普通锁。
互斥锁有两种创建(初始化)方法:静态方式(使用宏)和动态方式(用函数创建)。我们常使用动态创建。
3. 相关函数
(1)头文件
#include <pthread.h>
(2)创建互斥锁
互斥锁需要在线程创建之前初始化好,以确保在任何线程尝试使用锁之前,锁已经准备就绪。这样做是为了防止竞争条件和潜在的未定义行为。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
//pthread_mutex_t *mutex :传入互斥锁句柄的地址。
//const pthread_mutexattr_t *mutexattr :填NULL,表示该锁为普通锁。
(3)销毁互斥锁
销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//pthread_mutex_t *mutex :传入互斥锁的句柄地址。
(4)加锁
对共享资源进行操作前,需要进行加锁操作。
int pthread_mutex_lock(pthread_mutex_t *mutex)
//pthread_mutex_t *mutex :传入互斥锁的句柄地址。
(5)解锁
对共享资源操作完毕后,需要进行解锁操作,以便其他线程使用这个共享资源。
int pthread_mutex_unlock(pthread_mutex_t *mutex)
//pthread_mutex_t *mutex :传入互斥锁的句柄地址。
4. 使用例程
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
pthread_mutex_t mutex; //互斥锁句柄
int Sum_tick= 10000000; //假如:总票数。(公共资源)利用互斥锁保护。
void *task(void *arg)
{
int index= *(int *)arg; //用index接收arg的值,区别第几个线程。
free(arg); //释放内存
int *slef_tick =(int *)malloc(sizeof(int *));
*slef_tick =0; //本窗口售出票数统计
//避免死锁的出现,必须要有解锁动作
while(1)
{
//如果该线程上锁,则其他线程需要等待解锁才能继续执行。
pthread_mutex_lock(&mutex); //上锁:对公共资源使用前
if(Sum_tick > 0) //还有票
{
Sum_tick--;
pthread_mutex_unlock(&mutex); //解锁:对公共资源操作完后
(*slef_tick)++;
}
else
{
pthread_mutex_unlock(&mutex); //解锁: 防止死锁出现,当tick < 0时也需要解锁。
break; //下班
}
}
printf("%d号窗口今日售出%d张票。\n",index, *slef_tick);
//3. 退出线程并返回slef的数值
pthread_exit((void *)slef_tick);
}
int main(int argc, char **argv)
{
int ret;
pthread_t thread[10]; //线程句柄
int *thread_return;
int sum=0; //sum记录所有子线程返回值的和。
//1. 在线程创建之前创建好互斥锁。 NULL为普通锁
ret=pthread_mutex_init(&mutex, NULL);
if(ret <0)
{
perror("pthread_mutex_init error!\n");
return -1;
}
//2. 创建10个线程:
for(int i=0;i<10;i++)
{
int *index=(int *)malloc(sizeof(int *));
*index=i;
ret=pthread_create(&thread[i], NULL, task, (void *)index); //把i的值传给函数func1的形参
if(ret!=0)
{
perror("pthread_create error!\n");
return -1;
}
}
//4. 等待线程退出,并释放线程资源
for(int i=0;i<10;i++)
{
pthread_join(thread[i], (void **)&thread_return);
sum = sum+ (*thread_return);
}
printf("今日景点总售出%d张票。\n",sum);
//5. 释放内存
free(thread_return);
//6. 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
为什么每个线程售出的票数都不一样呢?
答:这是因为线程间也存在竞争,谁抢到CPU的使用权谁就开始执行。我们可以查看CPU的核心数来确定每次可以同时有多少个线程同时运行。
二、条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制。目的是为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。使用条件变量必定要使用互斥锁(普通锁)!
主要包括两个动作:①线程等待条件变量的条件成立而挂起。②线程使条件成立(给出条件成立信号)。
1. 相关函数
(1)创建条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//pthread_cond_t *cond :条件变量的句柄。
//pthread_condattr_t *cond_attr :属性,通常位NULL。
(2)注销条件变量
只有在没有线程在该条件变量上等待的时候才能注销这个条件变量。因为 Linux 实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
int pthread_cond_destroy(pthread_cond_t *cond);
//pthread_cond_t *cond :条件变量的句柄。
(3)等待条件变量成立
等待方式有两种:无条件等待和计时等待。我们主要使用无条件等待函数。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求 pthread_cond_wait()(或 pthread_cond_timedwait())的竞争条件。
●无条件等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//pthread_cond_t *cond :要等待的条件变量句柄。
// pthread_mutex_t *mutex :相对应的互斥锁句柄。
特别注意: 在使用等待函数前,必须加上互斥锁。因为对共享资源的修改都是在条件变量成立后进行修改,因为不确定何时条件会成立。所以要确保在等待条件的过程中,互斥锁已经被锁定,保护共享资源。这里虽然在等待函数前加上了互斥锁,但是等待函数有个特性,就是在条件不成立时,会自动解开互斥锁并挂起(阻塞)等待条件成立,以免影响其他线程使用共享资源。当条件成立后,会解除阻塞并自动加上互斥锁(不需要手动上锁),然后继续执行。使用如下:
//步骤1:先上锁,再等待条件成立。
pthread_mutex_lock(&mutex);
//步骤2:等待条件成立,当不成立时,自动解开步骤1的锁,挂起等待。当条件成立后,自动上锁。
pthread_cond_wait(&cond,&mutex);
//步骤3:对共享资源进行修改
data++;
步骤4:解锁,这里解的锁不是步骤1的锁,而是步骤2条件成立后自动上的锁。
pthread_mutex_unlock(&mutex);
(4)条件变量激发(使条件变量成立)
激发条件有两种形式:
【1】激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个。
int pthread_cond_signal(pthread_cond_t *cond);
//pthread_cond_t *cond :要激活的条件变量句柄。
【2】激活所有等待线程。
int pthread_cond_broadcast(pthread_cond_t *cond);
2. 使用注意
不应该在同一个线程中既等待条件变量的成立,又激活该条件变量。这通常是两个不同角色的线程所执行的任务。等待条件变量的线程(等待者)和激活条件变量的线程(信号者)应该是不同的,以避免逻辑错误和死锁的情况。
3. 使用例程
功能:模拟有2个水果摊卖水果,一共有5箱水果。还有一个水果仓库用来供货。当水果摊的水果卖完后,发送条件变量激活信号给水果仓库,水果仓库供货。
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
pthread_mutex_t mutex; //互斥锁的句柄
pthread_cond_t cond; //条件变量的句柄
int Sum_fruits= 10; //假如:总箱数。(公共资源)利用互斥锁保护。
//------------------------------------函数体1--------------------------------------------
void *func1 (void *arg)
{
int index=*(int *)arg; //用num接收arg的值,也就是i的值。
free(arg);
int *slef_num = (int *)malloc(sizeof(int *));
*slef_num=0; //卖出水果箱数统计
while(1)
{
pthread_mutex_lock(&mutex); //上锁:对公共资源使用前
if(Sum_fruits > 0) //还有货
{ Sum_fruits--;
if(Sum_fruits ==0) pthread_cond_signal(&cond); //激发条件。激发一个没有等待指令的条件变量无意义。
pthread_mutex_unlock(&mutex); //解锁:对公共资源操作完后
(*slef_num) ++;
}
else
{
pthread_mutex_unlock(&mutex); //解锁: 防止死锁出现,当tick < 0时也需要解锁。
break; //下班
}
printf("%d号摊位售出一箱水果,剩余%d箱\n",index, Sum_fruits);
sleep(1);
}
printf("%d号摊位今日售出%d箱水果,下班\n",index, *slef_num );
//退出线程并返回slef的数值
pthread_exit((void *)slef_num);
}
//------------------------------------函数体2-------------------------------------------
void *func2 (void *arg) //仓库等待激发指令
{
while(1)
{
pthread_mutex_lock(&mutex); //上锁。
pthread_cond_wait(&cond,&mutex); //等待条件成立。
Sum_fruits =10; //送货
printf("送10箱水果到水果摊\n");
/*这里的解锁不是和第51行的上锁配对!!而是解开56行的等待条件成立后的锁*/
pthread_mutex_unlock(&mutex); //解锁。
}
}
//----------------------------------主函数----------------------------------------
int main(int argc, char **argv)
{
int ret;
pthread_t thread[2]; //创建 2个 水果摊 线程
pthread_t thread2; //创建 1个 水果仓库 线程
int *thread_result; //用于接收返回值
int sum=0; //用n来接收子线程的返回值slef。sum记录所有子线程返回值的和。
//1. 在线程创建之前创建好互斥锁 。NULL为普通锁。
ret=pthread_mutex_init(&mutex, NULL);
if(ret<0)
{
perror("pthread_mutex_init error!\n");
return -1;
}
//2. 在线程创建之前创建好条件变量。
ret=pthread_cond_init(&cond,NULL);
if(ret<0)
{
perror("pthread_cond_init error!\n");
return -1;
}
//3. 创建线程。2个卖水果的摊位。
for(int i=0;i<2;i++)
{
int *index=(int *)malloc(sizeof(int *));
*index=i;
ret=pthread_create(&thread[i], NULL, func1, (void *)index); //把i的值传给函数func1的形参
if(ret!=0)
{
perror("pthread_create error!\n");
return -1;
}
}
//4. 创建1个线程,用于水果仓库供货
ret=pthread_create(&thread2, NULL, func2, NULL);
if(ret!=0)
{
perror("pthread_create error!\n");
return -1;
}
//5. 等待子线程退出,并释放资源。
for(int i=0;i<2;i++)
{
pthread_join(thread[i], (void **)&thread_result);
sum = sum+ (*thread_result);
printf("sum=%d\n",sum);
}
free(thread_result); //释放malloc申请的内存资源。
//等待水果仓库 子线程退出,并释放其占用的资源,不接收返回值
pthread_join(thread2, NULL);
//销毁互斥锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
三、信号灯
1. 分类
二元信号灯:也叫互斥量,只能取0和1两个值。它用于实现互斥访问,即一次只允许一个线程访问共享资源。
计数信号灯:计数范围为0到N,允许多个线程访问有限数量的共享资源。
2. 信号灯操作
等待(P操作):检查信号灯的值。如果信号灯的值大于0,则将其减1并继续执行;如果信号灯的值为0,则阻塞等待,直到信号灯的值大于0。
释放(V操作):将信号灯的值加1,并唤醒等待的线程(如果有)。
3. 相关函数
(0)头文件
#include <semaphore.h>
(1)创建信号灯
成功返回 0,失败返回负数。
int sem_init(sem_t *sem, int pshared, unsigned int value);
/*
sem_t *sem:要操作的信号灯句柄。
int pshared:是否为多进程共享而不仅仅是用于一个进程之间的多线程共享。通常为0,表示线程间。 1 - 进程同步。
unsigned int value:为信号灯的初值。
*/
(2)注销灯
被注销的信号灯 sem 要求已没有线程在等待该信号灯,否则返回-1,且置 errno 为EBUSY。除此之外,Linux Threads 的信号灯注销函数不做其他动作。
int sem_destroy(sem_t * sem);
//sem_t *sem:要操作的信号灯句柄。
(3)点灯
点灯操作将信号灯值原子地加 1,表示增加一个可访问的资源。只有信号灯值大于 0,才能访问公共资源。 当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞。
int sem_post(sem_t * sem); //相当于解锁
//sem_t * sem:要操作的信号灯句柄。
(4)灭灯
sem_wait 主要被用来阻塞当前线程直到信号量 sem 的值大于 0,解除阻塞后将 sem 的值减一,表明公共资源经使用后减少。等待灯亮(信号灯值大于 0),然后将信号灯原子地减 1,并返回。
int sem_wait(sem_t * sem); //相当于加锁
//sem_t * sem:要操作的信号灯句柄。
(5)获取灯值
int sem_getvalue(sem_t * sem, int * sval);
//sem_t * sem:要操作的信号灯句柄。
//int * sval:将值存放在这个指针所致的地址下。
4. 使用例程
功能:模拟有2个水果摊卖水果,一共有5箱水果。还有一个水果仓库用来供货。当水果摊的水果卖完后,发送条件变量激活信号给水果仓库,水果仓库供货。(使用信号灯)
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>
pthread_mutex_t mutex; //互斥锁的句柄
sem_t sem; //信号灯句柄
int Sum_fruits= 10; //假如:总箱数。(公共资源)利用互斥锁保护。
//------------------------------------函数体1--------------------------------------------
void *func1 (void *arg)
{
int index=*(int *)arg; //用num接收arg的值,也就是i的值。
free(arg);
int *slef_num = (int *)malloc(sizeof(int *));
*slef_num=0; //卖出水果箱数统计
while(1)
{
pthread_mutex_lock(&mutex); //上锁:对公共资源使用前
if(Sum_fruits > 0) //还有货
{ Sum_fruits--;
if(Sum_fruits ==0) sem_post(&sem); //点灯:+1
pthread_mutex_unlock(&mutex); //解锁:对公共资源操作完后
(*slef_num) ++;
}
else
{
pthread_mutex_unlock(&mutex); //解锁: 防止死锁出现,当tick < 0时也需要解锁。
break; //下班
}
printf("%d号摊位售出一箱水果,剩余%d箱\n",index, Sum_fruits);
sleep(1);
}
printf("%d号摊位今日售出%d箱水果,下班\n",index, *slef_num );
//退出线程并返回slef的数值
pthread_exit((void *)slef_num);
}
//------------------------------------函数体2-------------------------------------------
void *func2 (void *arg) //仓库等待激发指令
{
while(1)
{
sem_wait(&sem); //灭灯: -1
Sum_fruits=10; //送货
printf("送10箱水果到水果摊\n");
}
}
//----------------------------------主函数----------------------------------------
int main(int argc, char **argv)
{
int ret;
pthread_t thread[2]; //创建 2个 水果摊 线程
pthread_t thread2; //创建 1个 水果仓库 线程
int *thread_result; //用于接收返回值
int sum=0; //用n来接收子线程的返回值slef。sum记录所有子线程返回值的和。
//1. 在线程创建之前创建好互斥锁 。NULL为普通锁。
ret=pthread_mutex_init(&mutex, NULL);
if(ret<0)
{
perror("pthread_mutex_init error!\n");
return -1;
}
//2. 在线程创建之前创建好信号灯 。
ret=sem_init(&sem, 0, 0);
if(ret<0)
{
perror("sem_init error!\n");
return -1;
}
//3. 创建线程。2个卖水果的摊位。
for(int i=0;i<2;i++)
{
int *index=(int *)malloc(sizeof(int *));
*index=i;
ret=pthread_create(&thread[i], NULL, func1, (void *)index); //把i的值传给函数func1的形参
if(ret!=0)
{
perror("pthread_create error!\n");
return -1;
}
}
//4. 创建1个线程,用于水果仓库供货
ret=pthread_create(&thread2, NULL, func2, NULL);
if(ret!=0)
{
perror("pthread_create error!\n");
return -1;
}
//5. 等待子线程退出,并释放资源。
for(int i=0;i<2;i++)
{
pthread_join(thread[i], (void **)&thread_result);
sum = sum+ (*thread_result);
printf("sum=%d\n",sum);
}
free(thread_result); //释放malloc申请的内存资源。
//等待水果仓库 子线程退出,并释放其占用的资源,不接收返回值
pthread_join(thread2, NULL);
//销毁信号灯
sem_destroy(&sem);
//销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
四、原子操作(内核层)
原子操作(atomic operations)是指对变量进行不可分割的操作。这些操作在多处理器环境中非常重要,因为它们保证了对共享变量的操作不会被中断,从而避免数据竞争和不一致性。
1. 优势
避免数据竞争:在多处理器或多线程环境中,多个处理器或线程可能同时访问和修改共享变量。原子操作确保这些操作是不可分割的,不会被其他操作中断,从而避免数据竞争。
简化代码:使用原子操作可以避免使用复杂的锁机制,使代码更加简洁和高效。
2. 常用的原子操作函数(内核层函数)
头文件:#include <linux/atomic.h>
●对于32位原子变量:atomic_t num;
。下列函数调用一次,只执行一次。
atomic_inc(&num);//对原子变量 v 进行递增操作。
atomic_dec(&num);//对原子变量 v 进行递减操作。
atomic_add(i, &num);//将 i 加到原子变量 v 上。
atomic_sub(i, &num);//从原子变量 v 中减去 i。
atomic_set(&num, i);//将原子变量 v 设置为 i。
atomic_read(&num);//读取原子变量 v 的值。
●对于64位原子变量:atomic64_t num;
。下列函数调用一次,只执行一次。
atomic64_inc(&num);//对原子变量 v 进行递增操作。
atomic64_dec(&num);//对原子变量 v 进行递减操作。
atomic64_add(i, &num);//将 i 加到原子变量 v 上。
atomic64_sub(i, &num);//从原子变量 v 中减去 i。
atomic64_set(&num, i);//将原子变量 v 设置为 i。
atomic64_read(&num);//读取原子变量 v 的值。
3. 示例代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/atomic.h>
static atomic_t my_counter = ATOMIC_INIT(0); //将原子值初始化为0
static int my_module_init(void) {
printk(KERN_INFO "My Kernel Module: Initialization\n");
// 原子地递增计数器
atomic_inc(&my_counter);
printk(KERN_INFO "Counter Value: %d\n", atomic_read(&my_counter));
return 0; // 成功返回0
}
static void my_module_exit(void) {
printk(KERN_INFO "My Kernel Module: Exit\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
五、习题
题目1:创建4个线程,线程按顺序执行并打印0~9。即第一个线程打印完0-9后,第二个线程开始执行打印…
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
// 信号量数组,用于控制线程的顺序
sem_t semaphores[4];
// 线程函数
void* print_numbers(void* arg) {
int thread_index = *((int*)arg);
// 等待前一个线程完成
sem_wait(&semaphores[thread_index]);
// 打印0~9
for (int i = 0; i < 10; ++i) {
printf("Thread %d: %d\n", thread_index, i);
}
// 释放下一个线程
sem_post(&semaphores[thread_index + 1]);
return NULL;
}
int main() {
pthread_t threads[4];
int thread_indices[4];
// 初始化信号量
for (int i = 0; i < 4; ++i) {
sem_init(&semaphores[i], 0, 0);
}
// 第一个信号量设为1,允许第一个线程执行
sem_post(&semaphores[0]);
// 创建并启动4个线程
for (int i = 0; i < 4; ++i) {
thread_indices[i] = i;
pthread_create(&threads[i], NULL, print_numbers, &thread_indices[i]);
}
// 等待所有线程完成
for (int i = 0; i < 4; ++i) {
pthread_join(threads[i], NULL);
}
// 销毁信号量
for (int i = 0; i < 4; ++i) {
sem_destroy(&semaphores[i]);
}
return 0;
}