目录
2、互斥锁:本质上是一种临界资源,但互斥锁在同一时刻只能被一个线程使用,当一个线程试图去锁定另一个线程锁定的互斥所时,该线程会阻塞等待,直到使用互斥锁的线程解锁了该互斥锁
一、多进程与多线程对比
二、 临界资源 临界区 竞态
例1:临界资源 实现 输入输出
#include <myhead.h>
#define N 2
char buf[128] = "";//临界资源
// 定义线程体1 使用临界资源的代码 称为临界区
void *task1(void *arg)
{
while (1) // 输入信息
{
printf("输入:");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = 0;
}
}
// 定义线程体2 使用临界资源的代码 称为临界区
void *task2(void *arg)
{
while (1) // 循环输出信息
{
usleep(500000); // 500000us 0.5s 执行一次
printf("buf = %s\n", buf);
bzero(buf, sizeof(buf));
}
}
int main(int argc, char const *argv[])
{
// 创建两个任务
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("task2 error");
return -1;
}
// 主程序
printf("tid1 = %#lx, tid2 = %#lx\n", tid1, tid2);
// 阻塞回收线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
例2:对临界资源 进行 减减
#include <myhead.h>
#define N 2
int num = 520;//临界资源
// 定义线程体1 使用临界资源的代码 称为临界区
void *task1(void *arg)
{
while (1) // 输入信息
{
if (num > 0)
{
num--;
printf("task1 执行一次,剩余%d次\n",num);
usleep(1000);
}else
{
printf("task1 执行结束\n");
break;
}
}
pthread_exit(EXIT_SUCCESS);
}
// 定义线程体2 使用临界资源的代码 称为临界区
void *task2(void *arg)
{
while (1) // 输入信息
{
if (num > 0)
{
num--;
printf("task2 执行一次,剩余%d次\n",num);
usleep(1000);
}else
{
printf("task2 执行结束\n");
break;
}
}
pthread_exit(EXIT_SUCCESS);
}
int main(int argc, char const *argv[])
{
// 创建两个任务
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("task2 error");
return -1;
}
// 主程序
printf("tid1 = %#lx, tid2 = %#lx\n", tid1, tid2);
// 阻塞回收线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
例子3:临界资源抢占使用
#include <myhead.h>
#define N 2
int num = 520;//临界资源
// 定义线程体1 使用临界资源的函数 称为临界区
void *task1(void *arg)
{
while (1) // 输入信息
{
if (num > 0)
{
num--;
printf("task1 执行一次,剩余%d次\n",num);
usleep(1000);
}else
{
printf("task1 执行结束\n");
break;
}
}
pthread_exit(EXIT_SUCCESS);
}
// 定义线程体2 使用临界资源的函数 称为临界区
void *task2(void *arg)
{
while (1) // 输入信息
{
if (num > 0)
{
num--;
printf("task2 执行一次,剩余%d次\n",num);
usleep(1000);
}else
{
printf("task2 执行结束\n");
break;
}
}
pthread_exit(EXIT_SUCCESS);
}
// 定义线程体1 使用临界资源的函数 称为临界区
void *task3(void *arg)
{
while (1) // 输入信息
{
if (num > 0)
{
num--;
printf("task3 执行一次,剩余%d次\n",num);
usleep(1000);
}else
{
printf("task3 执行结束\n");
break;
}
}
pthread_exit(EXIT_SUCCESS);
}
int main(int argc, char const *argv[])
{
// 创建三个任务
pthread_t tid1, tid2,tid3;
if (pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("task2 error");
return -1;
}
if (pthread_create(&tid3, NULL, task3, NULL) != 0)
{
printf("task2 error");
return -1;
}
// 主程序
printf("tid1 = %#lx, tid2 = %#lx\n", tid1, tid2);
// 阻塞回收线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
return 0;
}
由于多个线程共同使用进程的资源,导致,线程在操作上容易出现不安全的状态
对于例3 减减 含有多步操作 相对耗时 在多个线程对临界资源抢占使用时,会出现某一个线程的 减减 未结束另一个线程的 开始执行,就会导致数据出错。
该现象称为 竞态
三、线程的同步互斥机制(用于解决竟态)
如上例3所示,当其中一个线程正在访问全局变量时,可能会出现时间片用完的情况,另一个线程将数据进行更改后,再执行 该线程时,导致数据的不一致。这种现象称竞态,为了解决竞态,引入了同步互斥机制
3.1基本概念
1)竟态:同一个进程的多个线程在访问临界资源时,会出现抢占的现象,导致线程中的数据错误的现象称为竞态
2)临界资源:多个线程共同访问的数据称为临界资源,可以是全局变量、堆区数据、外部文件
3)临界区:访问临界资源的代码段称为临界区
4)粒度:临界区的大小
5)同步:表示多个任务有先后顺序的执行
6)互斥:表示在访问临界区时,同一时刻只能有一个任务,当有任务在访问临界区时,其他任务只能等待
3.2线程互斥
1、在C语言中,线程的互斥通过互斥锁来完成
2、互斥锁:本质上是一种临界资源,但互斥锁在同一时刻只能被一个线程使用,当一个线程试图去锁定另一个线程锁定的互斥所时,该线程会阻塞等待,直到使用互斥锁的线程解锁了该互斥锁
3、互斥锁的相关API
#include <pthread.h>
1、创建互斥锁:只需要定义一个pthread_mutex_t类型的变量,就创建了一个互斥锁pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER; //静态初始化一个互斥锁
2、初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
功能:初始化一个互斥锁
参数1:互斥锁变量的地址
参数2:互斥锁的属性,一般填NULL,使用系统默认提供的属性
返回值:总是成功,不会失败(成功返回0)
3、获取锁资源int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:某个线程调用该函数表示获取锁资源(阻塞等待)
参数:互斥锁的地址
返回值:成功返回0,失败返回一个错误码
4、释放锁资源int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:将线程中的锁资源释放
参数:互斥锁的地址
返回值:成功返回0,失败返回一个错误码
5、销毁锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁程序中的锁资源
参数:互斥锁地址
4、对例3进行 互斥
#include <myhead.h>
#define N 2
int num = 520; // 临界资源
// 创建一个互斥锁
pthread_mutex_t mutex;
// 定义线程体1 使用临界资源的代码 称为临界区
void *task1(void *arg)
{
while (1) // 输入信息
{
// 上锁 获取锁资源
pthread_mutex_lock(&mutex);
if (num > 0)
{
num--;
printf("task1 执行一次,剩余%d次\n", num);
usleep(1000);
}
else
{
printf("task1 执行结束\n");
// 解锁 释放锁资源
pthread_mutex_unlock(&mutex); // 结束前解锁 避免死锁
break;
}
// 解锁 释放锁资源
pthread_mutex_unlock(&mutex);
}
pthread_exit(EXIT_SUCCESS);
}
// 定义线程体2 使用临界资源的代码 称为临界区
void *task2(void *arg)
{
while (1) // 输入信息
{
// 上锁 获取锁资源
pthread_mutex_lock(&mutex);
if (num > 0)
{
num--;
printf("task2 执行一次,剩余%d次\n", num);
usleep(1000);
}
else
{
printf("task2 执行结束\n");
// 解锁 释放锁资源
pthread_mutex_unlock(&mutex); // 避免死锁
break;
}
// 解锁 释放锁资源
pthread_mutex_unlock(&mutex);
}
pthread_exit(EXIT_SUCCESS);
}
// 定义线程体3 使用临界资源的函数 称为临界区
void *task3(void *arg)
{
while (1) // 输入信息
{
// 上锁 获取锁资源
pthread_mutex_lock(&mutex);
if (num > 0)
{
num--;
printf("task3 执行一次,剩余%d次\n", num);
usleep(1000);
}
else
{
printf("task3 执行结束\n");
// 解锁 释放锁资源
pthread_mutex_unlock(&mutex); // 避免死锁
break;
}
// 解锁 释放锁资源
pthread_mutex_unlock(&mutex);
}
pthread_exit(EXIT_SUCCESS);
}
int main(int argc, char const *argv[])
{
//初始化锁
pthread_mutex_init(&mutex, NULL);
// 创建三个任务
pthread_t tid1, tid2, tid3;
if (pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("task2 error");
return -1;
}
if (pthread_create(&tid3, NULL, task3, NULL) != 0)
{
printf("task2 error");
return -1;
}
// 主程序
printf("tid1 = %#lx, tid2 = %#lx\n", tid1, tid2);
// 阻塞回收线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
// 销毁锁资源
pthread_mutex_destroy(&mutex);
return 0;
}
3.3死锁
1、概念:在多线程编程中,死锁是一种情况,其中两个或多个线程被永久阻塞,因为每个线程都在等待其他线程释放它们需要的资源。在C语言中,这通常涉及互斥锁(mutexes),当多个互斥锁被不同的线程以不同的顺序获取时,很容易发生死锁。
2、产生条件:
1) 互斥条件:资源不能被多个线程共享,只能被一个线程在任一时刻所使用。
2) 持有和等待条件:一个线程至少持有一个资源,并且正在等待获取一个当前被其他线程持有的资源。
3.)不可抢占条件:资源不能被强制从一个线程抢占到另一个线程,线程必须自愿释放它的资源。
4.)循环等待条件:存在一个线程(或多个线程)的集合{P1, P2, ..., Pn},其中P1正在等待P2持有的资源,P2正在等待P3持有的资源,依此类推,直至Pn正在等待P1持有的资源。
3、例4:死锁示例
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t lock1, lock2;
void* thread1(void* arg) {
pthread_mutex_lock(&lock1);
sleep(1); // 确保线程2能锁住lock2
pthread_mutex_lock(&lock2);
printf("Thread 1\n");
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
return NULL;
}
void* thread2(void* arg) {
pthread_mutex_lock(&lock2);
sleep(1); // 确保线程1能锁住lock1
pthread_mutex_lock(&lock1);
printf("Thread 2\n");
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&lock1, NULL);
pthread_mutex_init(&lock2, NULL);
pthread_create(&t1, NULL, thread1, NULL);
pthread_create(&t2, NULL, thread2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&lock1);
pthread_mutex_destroy(&lock2);
return 0;
}
4、避免死锁的策略:
1.)避免持有和等待:尽可能让线程在开始执行前一次性获取所有必需的资源。
2.)资源排序:规定一个全局顺序来获取资源,并且强制所有线程按这个顺序获取资源。
3.)使用超时:在尝试获取资源时使用超时机制,这样线程在等待过长时间后可以放弃,回退,并重新尝试。
4.)检测死锁并恢复:运行时检测死锁的存在,一旦检测到死锁,采取措施(如终止线程或回滚操作)来解决。
3.4线程同步
1、多个线程任务有顺序的执行,由于多个任务有顺序的执行了,那么在同一时刻,对临界资源的访问就只一个线程
2、线程同步文件的经典问题是:生产者消费者问题
对于该问题而言,需要先执行生产者任务,然后执行消费者任务
3、对于线程同步问题,有两种机制来完成:无名信号量、条件变量
3.5线程同步 无名信号量
1、无名信号量本质上也是一个特殊的临界资源
2、无名信号量中维护了一个value值,该值表示能够申请的资源个数,当该值为0时,申请资源的线程将处于阻塞,直到其他线程将该无名信号量中的value值增加到大于0时即可
3、无名信号量API
1、创建无名信号量:只需要定义一个 sem_t 类型的变量,就创建了一个无名信号量
sem_t sem;
2、初始化无名信号量
#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:完成对无名信号量的初始化工作
参数1:要被初始化的无名信号量地址
参数2:无名信号量适用情况
0:表示多线程之间
非0:表示多进程间同步
参数3:无名信号量的资源初始值
返回值:成功返回0,失败返回-1并置位错误码3、申请资源:P操作,将资源减1操作
#include <semaphore.h>int sem_wait(sem_t *sem);
功能:申请无名信号量中的资源,使得该信号量中的value-1
参数:无名信号量地址
返回值:成功返回0,失败返回-1并置位错误码4、释放资源:V操作,将资源加1操作
#include <semaphore.h>int sem_post(sem_t *sem);
功能:将无名信号量中的资源加1操作
参数:无名信号量地址
返回值:成功返回0,失败返回-1并置位错误码5、销毁无名信号量
#include <semaphore.h>int sem_destroy(sem_t *sem);
功能:销毁一个无名信号量
参数:无名信号量地址
返回值:成功返回0,失败返回-1并置位错误码
4、同步 无名信号量 示例
未 使用 无名信号量
#include <myhead.h>
//创建生产者线程
void * task1(void *arg)
{
int num = 5;
while (num--)
{
printf("%#lx:执行一次生产\n",pthread_self());
sleep(1);
}
pthread_exit(EXIT_SUCCESS);
}
//创建消费者线程
void * task2(void *arg)
{
int num = 5;
while (num--)
{
printf("%#lx:执行一次消费\n",pthread_self());
}
pthread_exit(EXIT_SUCCESS);
}
int main(int argc, char const *argv[])
{
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("task1 error");
return -1;
}
pthread_join(tid2, NULL);
pthread_join(tid1, NULL);
return 0;
}
使用 无名信号量
#include <myhead.h>
//1、创建无名学号量
sem_t sem;
//创建生产者线程
void * task1(void *arg)
{
int num = 5;
while (num--)
{
printf("%#lx:执行一次生产\n",pthread_self());
//4、释放资源 无名信号量资源加1 允许消费者a线程执行
sem_post(&sem);
}
pthread_exit(EXIT_SUCCESS);
}
//创建消费者线程
void * task2(void *arg)
{
int num = 5;
while (num--)
{
//3、申请资源
sem_wait(&sem); //阻塞等待 生产者线程释放 无名信号量资源
//当无名信号量资源 为1时 执行 减1
//因为 无名信号量 初始资源为0 即便先执行消费者线程也会被阻塞
printf("%#lx:执行一次消费\n",pthread_self());
}
pthread_exit(EXIT_SUCCESS);
}
int main(int argc, char const *argv[])
{
//2、初始化无名信号量
sem_init(&sem,0,0);
//第一个0 表示同步用于多线程间
//第二e个0 表示无名信号量初始资源为0
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("task1 error");
return -1;
}
pthread_join(tid2, NULL);
pthread_join(tid1, NULL);
//5、销毁无名信号量
sem_destroy(&sem);
return 0;
}
3.6 线程同步 条件变量
1、条件变量的本质也是一个特殊的临界资源
2、条件变量中维护了一个队列,想要执行的消费者线程,需要先进入等待队列中,等生产者线程进行唤醒后,依次执行。这样就可以做到一个生产者和多个消费者之间的同步,但是消费者和消费者之间在进入等待队列这件事上是互斥的。
3、条件变量的API
1)创建条件变量
只需要定义一个pthread_cond_t类型的变量,就创建了一个条件变量
pthread_cond_t cond;2)初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
功能:初始化一个条件变量
参数1:条件变量的地址
参数2:条件变量的属性,一般填NULL
返回值: 成功返回0,失败返回非0错误码
3)将消费者线程放入到等待队列中
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
功能:消费者线程进入等待队列中
参数1:条件变量的地址
参数2:互斥锁的地址:因为多个消费者线程在进入等待队列上是竞态的
返回值: 成功返回0,失败返回非0错误码4)唤醒消费者线程
int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒等待队列中的第一个消费者线程
参数:条件变量的地址
返回值: 成功返回0,失败返回非0错误码
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒所有等待队列中的消费者线程
参数:条件变量的地址
返回值: 成功返回0,失败返回非0错误码
5)销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁一个条件变量
参数:条件变量的地址
返回值: 成功返回0,失败返回非0错误码
4、条件变量 示例
#include <myhead.h>
//1、定义一个条件变量
pthread_cond_t cond;
//11、创建互斥锁
pthread_mutex_t mutex;
// 创建生产者线程
void *task1(void *arg)
{
// int num = 5;
// while (num--)
// {
// sleep(1);
// printf("%#lx:执行一次生产\n", pthread_self());
// //4、唤醒消费者线程(单个)
// pthread_cond_signal(&cond);
// }
sleep(3);
printf("%#lx:执行5次生产\n", pthread_self());
pthread_cond_broadcast(&cond);//4、唤醒消费者线程(全部)
pthread_exit(EXIT_SUCCESS);
}
// 创建消费者线程
void *task2(void *arg)
{
//33、获取互斥锁
pthread_mutex_lock(&mutex);
//3、请求进入等待队列
pthread_cond_wait(&cond,&mutex);
printf("%#lx:执行一次消费\n", pthread_self());
//44、释放互斥锁
pthread_mutex_unlock(&mutex);
pthread_exit(EXIT_SUCCESS);
}
int main(int argc, char const *argv[])
{
//2、初始化统计变量
pthread_cond_init(&cond,NULL);
//22、初始化互斥锁
pthread_mutex_init(&mutex,NULL);
pthread_t tid1, tid2, tid3, tid4, tid5, tid6;
if (pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid3, NULL, task2, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid4, NULL, task2, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid5, NULL, task2, NULL) != 0)
{
printf("task1 error");
return -1;
}
if (pthread_create(&tid6, NULL, task2, NULL) != 0)
{
printf("task1 error");
return -1;
}
//输出每个线程的线程号
printf("tid2=%#lx tid3=%#lx tid4=%#lx tid5=%#lx tid6=%#lx \n",tid2,tid3,tid4,tid5,tid6);
pthread_join(tid2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
pthread_join(tid5, NULL);
pthread_join(tid6, NULL);
//5、销毁条件变量
pthread_cond_destroy(&cond);
//55、销毁锁资源
pthread_mutex_destroy(&mutex);
return 0;
}