day04
线程竞争
一、基本概念
竞争与同步
同一个进程中的线程共享进程中的绝大部分资源,当它们随意竞争时可能会导致资源被破坏、脏数据、不完整问题
通过一些手段让线程在竞争资源时相互协调、避免出现以上问题,这就称为线程同步
原子操作:
操作过程中不能被打断的操作
临界资源、临界区、竞态条件:
能够被多个进程访问但是又无法同时访问的资源称为临界资源
每个进程中访问临界资源的那段代码称为临界区,能够被多个线程访问但又无法同时访问的代码片段
多个线程在临界区内执行时,由于线程的执行顺序具有随机性,从而导致结果不确定,称为竞态条件
二、互斥量(互斥锁)
pthread_mutex_t:是一种数据类型,用来定义互斥锁变量
int pthread_mutex_init(pthread_mutex_t* mutex,
const pthread_mutexattr_t* attr);
功能:对互斥量进行初始化,完成后锁属于开锁状态
mutex:要初始化的互斥量
attr:互斥量的属性设置,一般默认给NULL即可
也可以在定义时通过PTHREAD_MUTEX_INITIALIZER初始化互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:对互斥量加锁,成功则返回继续执行,失败则阻塞休眠等待,直到互斥量解锁并成功加锁才唤醒返回
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:对互斥量解锁,对别人上锁的互斥量解锁,返回EBUSY
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:对互斥量尝试加锁,成功返回0,失败返回EBUSY
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁互斥量
三、读写锁
读写锁将线程访问共享数据时发出的请求分为两种:
读请求:
只读取共享的数据不做任何修改
写请求:
存在修改共享数的行为
1、当有多个线程同时发出读请求,可以同时进行
2、当有多个线程同时发出写请求时,只能一个一个执行
3、当发出读请求的线程正在执行时,发出写请求的线程必须等待前面所有读请求线程执行完后才能执行
4、当发出写请求的线程正在执行时,发出读请求的线程必须等待前面所有写请求线程执行完后才能执行
当读写锁被发出读请求的线程占用时,称为"读锁",当读写锁被发出写请求的线程占用时,称为"写锁"
1、当读写锁未被任何线程占用,发出读请求、写请求的线程都可以占用,如果同时请求,默认优先给读请求占用
2、当变成读锁(多个线程占用)时,读请求的线程可以占用不会阻塞,但是写请求的线程会阻塞等待读锁解锁
3、当变成写锁(多个线程占用)时,读、写请求的线程会阻塞等待写锁解锁
pthread_rwlock_init 初始化读写锁
pthread_rwlock_wrlock 写锁上锁
pthread_rwlock_rdlock 读锁上锁
pthread_rwlock_unlock 解锁
pthread_rwlock_destroy 销毁
四、自旋锁
在任意时刻,最多只能有一个线程单元获得该锁
对于互斥锁,如果资源已经被占用,申请者会进入休眠态
对于自旋锁,则不会引起申请者休眠,而是一直在申请处进入循环,不停地查看自旋锁是否解锁,直到解锁之后并加锁成功后才退出循环往下执行
一般情况下使用互斥锁,如果知道被锁住的代码执行时间很短(或者主动让其变短),那么应该选择使用系统开销(运行态<->休眠态)较少的自旋锁更合适
五、信号量
与进程间通信的XIS机制中的信号量原理相似,线程之间使用的"全局"的计数器,用于控制访问有限的共享数据的线程数
#include <semaphore.h>
sem_t 是一种数据类型,用于定义信号量变量
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量
sem:要初始化的信号量
pshared:
0 只能在本进程使用
非零 该信号量可以以共享内存的方式被多个进程共享使用,Linux不支持
value:信号量的初始值
int sem_wait(sem_t *sem);
功能:对信号量-1,如果信号量的值为0,则阻塞等待
int sem_trywait(sem_t *sem);
功能:对信号量尝试-1,如果信号量的值为0,则立即返回EAGAIN,成功返回0
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:对信号量-1,如果信号量的值为0,则等待一段时间,超时还没能减则返回ETIMEOUT,成功返回0
int sem_post(sem_t *sem);
功能:对信号量的值+1
int sem_destroy(sem_t *sem);
功能:销毁信号量
六、死锁
1、什么是死锁:(答)
多个进程或线程相互等待对方的资源,在得到新资源前不会释放自己的旧资源,形成循环等待
2、产生死锁的四大必要条件(**答**)
资源互斥:资源只有两种状态,可用、不可用,资源不能被同时使用,同一时刻只能被一个进程或线程使用
占用且请求:已经得到资源的进程或线程,会继续请求新的资源,并且持续占用旧资源
资源不可被剥夺:当资源已经分配给进程或线程后,不能被其他线程或进程强制性获取,除非占用者主动释放
环路等待:死锁发生时,系统中必定有两个或以上的进程或线程的执行路线形成等待环路
注意:一旦产生死锁基本无解,现在的操作系统无法解决死锁,因此只能防止死锁的产生
3、防止死锁的产生方法
破坏互斥条件:
想办法让资源能够共享
缺点:受到环境或资金的影响,无法让资源共享
破环占用且请求:
采用静态预分配的方式,在进程或线程运行前尝试一次性申请所有的资源,在资源没有得到全部满足前不投入运行
缺点:可能会导致系统资源的浪费,因为有些资源是很靠后才会使用,但是已经提前分配占用,导致其他不会产生死锁的进程或线程也无法使用
破坏资源不可剥夺:
当一个进程或线程占用了不可被剥夺的资源时,并且请求新资源无法得到满足时,那么就把全部占用的资源主动释放,过一段时间后重新开始
缺点:该策略的实现难度高,释放已经占用的资源会导致前一阶段的工作失效,还要反复申请释放资源,浪费时间、资源
破坏环路等待:
给每个资源进行编号,进程或线程都按照编号的顺序来请求资源,并且必须拿到前一个编号资源后,才能去拿后一个
缺点:资源的编号顺序要相对稳定,否则在运行期间资源的增加或删减都会让过程受极大的影响
4、如何判断死锁
1、画出资源分配图
2、简化资源分配图
3、使用死锁的定理判断:看有没有环路
了解:银行家算法
七、条件变量
当某些设置的条件满足时,可以让线程自己进入睡眠态,也可以在另一些设置的条件满足时,可以被其他线程唤醒,需要配合互斥量使用
pthread_cond_t 是一种数据类型,用于定义条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
功能:初始化条件变量,也可以使用PTHREAD_COND_INITIALIZER在定义时初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
功能:让当前线程睡入条件变量cond,并主动解锁自己的互斥量mutex
int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒因为cond睡眠的其中一个线程,线程醒来前要确保能对原来的互斥量重新加锁
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
功能:让当前线程睡入cond,最多睡abstime时间,超时会返回
使用条件变量、互斥量可以实现生产者和消费者模型
八、生产者与消费者模型
生产者:生产数据的线程
消费者:使用数据的线程
仓库:存储数据的临时缓冲区(仓库解决生产、消费不匹配的问题)
可能产生的问题:
生产快于消费:仓库爆满,撑死
消费快于生产:仓库空虚,饿死
利用条件变量来解决以上问题:
当仓库(缓冲区)满的时候,生产者睡入条件变量(full),并通知消费者全部醒来(从empty)
当仓库(缓冲区)空的时候,消费者睡入条件变量(empty),并通知生产者全部醒来(从full)
应用:利用生产者消费者者模型构建 线程池、数据池
了解:哲学家就餐问题
九、Windows编程的准备工作
Windows下的网络编程与Linux的区别:
1、头文件不同,只需要使用<winsock2.h>
2、要添加库 -lws2_32
3、开始前要初始化网络库
WSADATA wsa;
if(WSAStartup(MAKEWORD(2,2),&wsa))
{
perror("WSAStartup");
return -1;
}
4、部分Linux标准库函数不能使用、可以换名使用
例如close改成closesocket
不能使用read、write发送接收数据
必须使用send、recv
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理