目录
锁
互斥锁(mutex)
使用流程
- 定义锁
- 初始化锁
- 加锁
- 解锁
- 销毁锁
定义锁
// 需要先定义一个pthread_mutex_t类型的锁(eg:)
pthread_mutex_t mLock;
初始化锁
对锁的初始化,有两种方式: (man pthread_mutex_destroy)
// 1, 一种是调用pthread_mutex_init函数
int pthread_mutex_init(
pthread_mutex_t *mutex, // 要初始化的锁
const pthread_mutexattr_t *attr // 锁的属性类型
);
// 2, 另一种是直接将锁变量初始化为PTHREAD_MUTEX_INITIALIZER
pthread_mutex_t mLock = PTHREAD_MUTEX_INITIALIZER;
加锁
使用pthread_mutex_lock加锁: (加锁后进入临界区)
#include <pthread.h>
// lock a mutex
int pthread_mutex_lock(
pthread_mutex_t *mutex // 锁
);
解锁
使用pthread_mutex_unlock把锁置为未锁: (临界区代码执行结束,解锁)
#include <pthread.h>
// unlock a mutex
int pthread_mutex_unlock(
pthread_mutex_t *mutex // 锁
);
销毁锁
pthread_mutex_destroy(&mLock);
demo:对全局变量分别进行加法
#include <my_header.h>
#define times 1000
int global = 0;
void *pthreadFunc(void *arg) {
pthread_mutex_t mLock = *((pthread_mutex_t *)arg);
for (int i = 0; i < times; i++) {
pthread_mutex_lock(&mLock);
global++;
pthread_mutex_unlock(&mLock);
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_mutex_t mLock;
pthread_mutex_init(&mLock,NULL);
pthread_t pid;
int ret = pthread_create(&pid,NULL,pthreadFunc,&mLock);
THREAD_ERROR_CHECK(ret,"pthread_create");
for (int i = 0; i < times; i++) {
pthread_mutex_lock(&mLock);
global++;
pthread_mutex_unlock(&mLock);
}
pthread_join(pid,NULL);
printf("global : %d\n",global);
pthread_mutex_destroy(&mLock);
return 0;
}
常用获取时间函数 gettimeofday
#include <sys/time.h>
// get time
int gettimeofday(
struct timeval *tv, // 是一个指向timeval结构体的指针,用于存储获取的时间和日期
struct timezone *tz // 是一个指向timezone结构体的指针,用于指定时区信息,通常设置为NULL。
)
struct timeval{
time_t tv_sec; // seconds 秒
suseconds_t tv_usec; // microseconds 微妙
}
demo
#include <my_header.h>
int main(int argc, char *argv[])
{
struct timeval begin,end;
gettimeofday(&begin,NULL);
sleep(10);
gettimeofday(&end,NULL);
printf("diff : %ld us\n",(end.tv_sec - begin.tv_sec ) * 1000000 + end.tv_usec - begin.tv_usec);
return 0;
}
非阻塞锁 pthread_mutex_trylock
#include <pthread.h>
// lock a mutex
int pthread_mutex_trylock(
pthread_mutex_t *mutex
)
// int返回值, 获取到锁则返回0; 没有获取到锁则返回错误码
死锁问题
#include <my_header.h>
pthread_mutex_t mLock1;
pthread_mutex_t mLock2;
void *func(void *arg) {
pthread_mutex_lock(&mLock2);
sleep(1);
pthread_mutex_lock(&mLock1);
printf("This is son pthread\n");
pthread_mutex_unlock(&mLock1);
pthread_mutex_unlock(&mLock2);
return NULL;
}
int main(int argc, char *argv[])
{
pthread_mutex_init(&mLock1,NULL);
pthread_mutex_init(&mLock2,NULL);
pthread_t pid;
pthread_create(&pid,NULL,func,NULL);
pthread_mutex_lock(&mLock1);
sleep(1);
pthread_mutex_lock(&mLock2);
printf("This is main\n ");
pthread_mutex_unlock(&mLock1);
pthread_mutex_unlock(&mLock2);
thread_join(pid, NULL);
pthread_mutex_destroy(&mLock1);
pthread_mutex_destroy(&mLock2);
return 0;
}
pthread_mutex_trylock解决死锁demo
#include <my_header.h>
pthread_mutex_t mLock1;
pthread_mutex_t mLock2;
void *func(void *arg) {
while(1){
pthread_mutex_lock(&mLock2);
sleep(1);
int res_trylock = pthread_mutex_trylock(&mLock1);
if (res_trylock != 0) {
pthread_mutex_unlock(&mLock2);
sleep(1); // 不加的话很容易导致刚释放mLock2马上子线程又给mLock2加锁(原因为释放的时候时间片还没用掉,就会用来获取锁),从而主线程拿不到mLock2,
printf("try_lock fail\n");
continue;
}
printf("This is son pthread\n");
pthread_mutex_unlock(&mLock1);
pthread_mutex_unlock(&mLock2);
break;
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_mutex_init(&mLock1,NULL);
pthread_mutex_init(&mLock2,NULL);
pthread_t pid;
pthread_create(&pid,NULL,func,NULL);
pthread_mutex_lock(&mLock1);
sleep(1);
pthread_mutex_lock(&mLock2);
printf("This is main\n ");
pthread_mutex_unlock(&mLock1);
pthread_mutex_unlock(&mLock2);
pthread_join(pid,NULL);
pthread_mutex_destroy(&mLock1);
pthread_mutex_destroy(&mLock2);
return 0;
}
不断循环检查锁是否被释放,也是一种自旋锁的思想
while(1){
pthread_mutex_lock(&mLock2);
sleep(1);
int res_trylock = pthread_mutex_trylock(&mLock1);
if (res_trylock != 0) {
pthread_mutex_unlock(&mLock2);
sleep(1); // 不加的话很容易导致刚释放mLock2马上子线程又给mLock2加锁(原因为释放的时候时间片还没用掉,就会用来获取锁),从而主线程拿不到mLock2,
printf("try_lock fail\n");
continue;
}
printf("This is son pthread\n");
pthread_mutex_unlock(&mLock1);
pthread_mutex_unlock(&mLock2);
break;
}
其他锁(只做了解,知道概念即可)
自旋锁
自旋锁(Spin Lock)是一种用于线程同步的锁机制,它不同于传统的互斥锁,它不会导致线程进入睡眠状态,而是会持续检测锁的释放情况,即“自旋”,直到获取到锁为止(忙等待,消耗cpu资源)
自旋锁(pthread_spinlock_t
)和普通互斥锁(pthread_mutex_t
)在实际效果上是类似的,因为它们都用于线程同步,确保在多线程环境中对共享资源的安全访问。然而,它们在实现上有一些关键区别:
- 等待方式:
- 自旋锁:线程在尝试获取自旋锁时会一直“忙等待”,即持续检查锁是否可用,直到获取为止。
- 普通互斥锁:当无法获得锁时,线程会被阻塞挂起,等待锁变为可用状态。
- 适用场景:
- 自旋锁适用于短时间内占用锁的情况:如果拥有锁的线程会快速释放锁,或者在锁被占用时其他线程很快就能继续执行其他工作,使用自旋锁可能效率更高。
- 普通互斥锁适用于长时间占用锁的情况,或者在等待锁释放时可以执行其他任务:如果对锁的占用时间较长,或线程在等待期间有其他工作可做,使用互斥锁可能更合适。
读写锁
读锁: 它允许多个线程同时以读模式访问共享资源。当一个线程持有读锁时,其他线程可以同时获取读锁来读取数据,但只能有一个线程可以获取写锁来写入数据。
写锁: 它只允许一个线程以写模式访问共享资源。当一个线程持有写锁时,其他线程无法获取读锁或写锁,必须等待该锁被释放后才能访问共享资源。
锁的类型 (我感觉不太重要,用到时候再来看吧,用NULL比较多)
我们在初始化锁的时候, 调用pthread_mutex_init(锁, 类型)
, 可以给锁设置不同的类型 (可 man pthread_mutexattr_gettype
查看)
PTHREAD_MUTEX_NORMAL // 普通锁. PTHREAD_MUTEX_ERRORCHECK // 检错锁. 同一线程中, 对某个锁重复上锁, 会返回错误, 去解锁未锁的锁,也会返回错误.是一种锁的错误检查机制 PTHREAD_MUTEX_RECURSIVE // 递归锁/可重入锁. 一个线程中可对该锁重复上锁, 通过计数标记上锁次数, 每次上锁计数+1,每次解锁技术-1; 当计数为0, 其它线程才能获取该锁. PTHREAD_MUTEX_DEFAULT // 默认锁,和普通锁表现等价.
函数定义
定义锁类型
// 需要先定义一个pthread_mutexattr_t的类型(eg:) pthread_mutexattr_t mutexattr;
初始化类型
#include <pthread.h> // initialze the mutex attributes object int pthread_mutexattr_init( pthread_mutexattr_t *attr // 类型变量 )
设置类型
#include <pthread.h> // set mutex type attribute int pthread_mutexattr_settype( pthread_mutexattr_t *attr, // 类型变量 int type // 锁的具体类型 )
使用
#include <pthread.h> // 初始化锁的时候, 给锁传入类型 int pthread_mutex_init( pthread_mutex_t *mutex, // 要初始化的锁 const pthread_mutexattr_t *attr // 锁的类型 );
销毁
#include <pthread.h> // destroy the mutex attributes object int pthread_mutexattr_destroy( pthread_mutexattr_t *attr // 类型变量 )
代码示例
EgCode: 正常锁
#include <testfun.h> // 使用正常锁 int main(int argc,char*argv[]) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); pthread_mutex_t mLock; pthread_mutex_init(&mLock, &attr); pthread_mutex_lock(&mLock); int res_errorcheck_lock = pthread_mutex_lock(&mLock); THREAD_ERROR_CHECK(res_errorcheck_lock, "lock repetition"); pthread_mutex_unlock(&mLock); pthread_mutex_destroy(&mLock); pthread_mutexattr_destroy(&attr); return 0; }
EgCode: 检错锁
#include <testfun.h> // 使用检错锁 int main(int argc,char*argv[]) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); pthread_mutex_t mLock; pthread_mutex_init(&mLock, &attr); pthread_mutex_lock(&mLock); int res_errorcheck_lock = pthread_mutex_lock(&mLock); THREAD_ERROR_CHECK(res_errorcheck_lock, "lock repetition"); pthread_mutex_unlock(&mLock); pthread_mutex_destroy(&mLock); pthread_mutexattr_destroy(&attr); return 0; }
EgCode: 递归锁/可重复锁
#include <testfun.h> // 使用重入锁 int main(int argc,char*argv[]) { pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_t mLock; pthread_mutex_init(&mLock, &attr); pthread_mutex_lock(&mLock); int res_errorcheck_lock = pthread_mutex_lock(&mLock); THREAD_ERROR_CHECK(res_errorcheck_lock, "lock repetition"); pthread_mutex_unlock(&mLock); pthread_mutex_unlock(&mLock); pthread_mutex_destroy(&mLock); pthread_mutexattr_destroy(&attr); return 0; }
检测题目 卖票程序
以一个卖票的逻辑为例:
// 一个人卖票: 票未必每一次都能卖掉, 每一次买票的人在随机的状态下选择是否买票
// 另一个人加票
// 在初始20张票的情况下:50%概率每次卖一张
// 当第一次票小于5张的时候再追加一次票: 10张票(一共只能加一次哦)
#include <my_header.h>
typedef struct share_state{
int flag; // 1表示加过一次票了,0表示没加过
int ticketNum;
pthread_mutex_t mLock;
}share_state_t;
void *sell(void *arg) {
// 注意这里一定要用指针,不要用结构体,不然会导致和sell用的不是同一个结构体
share_state_t *shareState = (share_state_t *) arg;
while(1) {
pthread_mutex_lock(&shareState->mLock);
// 判断是否小于等于0张,若小于等于0且已经加过票了,那就释放锁退出循环
if (shareState->ticketNum <= 0 && shareState->flag == 1) {
pthread_mutex_unlock(&shareState->mLock);
break;
}
// 没有加过票,那么若小于等于0张,则不卖,释放锁,到下一个循环
// 大于0张那么就卖票
struct timeval now;
gettimeofday(&now,NULL);
srand(now.tv_usec);
double rate = rand() * 1.0 / RAND_MAX;
if (rate <= 0.5 && shareState->ticketNum > 0) {
shareState->ticketNum--;
printf("sell a ticket now ticketNum : %d\n",shareState->ticketNum);
}
pthread_mutex_unlock(&shareState->mLock);
sleep(1);
}
return NULL;
}
void *add(void *arg) {
share_state_t *shareState = (share_state_t *) arg;
while(1) {
pthread_mutex_lock(&shareState->mLock);
if (shareState->ticketNum <= 5) {
// printf("add yes\n");
shareState->ticketNum += 10;
shareState->flag = 1;
printf("add tickets 10,now ticketNum : %d\n",shareState->ticketNum);
pthread_mutex_unlock(&shareState->mLock);
break;
}
pthread_mutex_unlock(&shareState->mLock);
sleep(1);
}
return NULL;
}
int main(int argc, char *argv[])
{
share_state_t shareState;
shareState.flag = 0;
shareState.ticketNum = 20;
pthread_mutex_init(&shareState.mLock,NULL);
pthread_t pid1,pid2;
pthread_create(&pid1,NULL,sell,&shareState);
pthread_create(&pid2,NULL,add,&shareState);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
return 0;
}
条件变量
问题
我们发现上述写代码的时候,比如
我们会用sleep函数来避免释放锁后线程马上获得锁,从而导致一直在一个线程循环的情况,但是用sleep函数来避免这种问题是一种合适的方式吗,不是的,我们可以用条件变量来解决这种“加锁-检查条件不满足-解锁”的行为
原理
线程可以在不满足共享资源的某个条件
时等待/挂起,直到另一个线程发出通知,告诉它条件已经满足, 进一步唤醒这个等待。
Eg:
当A线程持有锁的时候,A认为自己做某些操作的条件还不够成熟,A可以主动让自己阻塞并且释放锁(陷入阻塞和解锁是原子的)
当锁被A释放,其他的线程比如B线程可以持有锁(有些情况下可以不持有锁)去修改条件的内容
B进行逻辑操作的过程中, 一旦B认为现在是一个合适的时机唤醒A时,B可以通过唤醒操作通知到A线程。
A线程收到唤醒通知之后,会首先恢复运行并加锁,再继续执行后续的指令。
这其中涉及到两个动作: A主动阻塞, B唤醒A
pthread_cond
我们可以在线程运行过程中, 通过调用pthread_cond_wait
让不满足条件的线程主动阻塞, 等待被唤醒.
当通过pthread_cond_wait
陷入阻塞的时候, 会先释放锁.
当pthread_cond_wait
被从阻塞状态唤醒的时候, 会先加锁, 然后继续执行其后的代码逻辑.
使用方法
定义条件变量
pthread_cond_t cond;
初始化条件变量
#include <pthread.h> // initialize condition variables int pthread_cond_init( pthread_cond_t *cond, // 条件变量的指针 pthread_condattr_t *attr // 条件变量属性对象 (默认NULL) )// 返回值: 初始化成功返回0;否则,返回一个错误码
陷入阻塞并释放锁
#include <pthread.h> // wait on a condition int pthread_cond_wait( pthread_cond_t *cond, // 条件变量的指针 pthread_mutex_t *muten // 要操作(释放和获取)的锁 )// 返回值: 成功返回0;否则,返回一个错误码
唤醒以指定条件变量阻塞的线程, 并使其重新获取锁
#include <pthread.h> // signal a condition int pthread_cond_signal( pthread_cond_t *cond // 条件变量的指针 )// 返回值: 成功返回0;否则,返回一个错误码
销毁条件变量
#include <pthread.h> // destroy a condition variables int pthread_cond_destroy( pthread_cond_t *cond // 条件变量的指针 )// 返回值: 成功返回0;否则,返回一个错误码
改进卖票程序
#include <my_header.h>
typedef struct share_state{
int flag; // 1表示加过一次票了,0表示没加过
int ticketNum;
pthread_mutex_t mLock;
pthread_cond_t cond;
}share_state_t;
void *sell(void *arg) {
share_state_t *shareState = (share_state_t *) arg;
while(1) {
pthread_mutex_lock(&shareState->mLock);
// 判断是否小于等于0张,若小于等于0且已经加过票了,那就释放锁退出循环
if (shareState->ticketNum <= 0 && shareState->flag == 1) {
pthread_mutex_unlock(&shareState->mLock);
break;
}
// 没有加过票,那么若小于等于0张,则不卖,释放锁,到下一个循环
// 大于0张那么就卖票
struct timeval now;
gettimeofday(&now,NULL);
srand(now.tv_usec);
double rate = rand() * 1.0 / RAND_MAX;
// printf("rate : %lf\n",rate);
if (rate <= 0.5 && shareState->ticketNum > 0) {
shareState->ticketNum--;
printf("sell a ticket now ticketNum : %d\n",shareState->ticketNum);
}
// 小于等于5张,就会指定阻塞,可以发现这里有指定哪个锁
if (shareState->ticketNum <= 5) {
pthread_cond_signal(&shareState->cond);
}
pthread_mutex_unlock(&shareState->mLock);
// sleep(1);
}
return NULL;
}
void *add(void *arg) {
share_state_t *shareState = (share_state_t *) arg;
while(1) {
pthread_mutex_lock(&shareState->mLock);
// printf("add,ticketNum:%d\n",shareState->ticketNum);
// 大于5张就阻塞,小于5张被唤醒
if (shareState->ticketNum > 5) {
pthread_cond_wait(&shareState->cond,&shareState->mLock);
}
shareState->flag = 1;
shareState->ticketNum += 10;
printf("add 10 tickets,now ticketNum: %d\n",shareState->ticketNum);
pthread_mutex_unlock(&shareState->mLock);
break;
// sleep(1);
}
return NULL;
}
int main(int argc, char *argv[])
{
share_state_t shareState;
shareState.flag = 0;
shareState.ticketNum = 20;
pthread_mutex_init(&shareState.mLock,NULL);
// 初始化 cond
pthread_cond_init(&shareState.cond,NULL);
pthread_t pid1,pid2;
pthread_create(&pid1,NULL,sell,&shareState);
pthread_create(&pid2,NULL,add,&shareState);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
pthread_mutex_destroy(&shareState.mLock);
return 0;
}
pthread_cond_timedwait
pthread_cond_timedwait
是一个可设置超时的pthread_cond_wait
// pthread_cond_timedwait返回有两种可能的情况。如果在设置的等待时间内条件变量被其他线程唤醒并满足条件,函数会返回0。如果等待超时,函数会返回ETIMEDOUT。
函数定义
条件变量的声明/初始化/唤醒/销毁, 皆同
pthread_cond_wait
#include <pthread.h> // wait on a condition int pthread_cond_timedwait( pthread_cond_t *cond, // 条件变量的指针 pthread_mutex_t *muten, // 要操作(释放和获取)的锁 timespec *abstime // 超时时间 )// 返回值: 成功返回0;否则,返回一个错误码
// timespec类型 struct timespec { __time_t tv_sec;//秒 __syscall_slong_t tv_nsec; //纳秒 }; // __time_t -> typedef long int __time_t; // __syscall_slong_t -> typedef long int __syscall_slong_t;
代码示例
#include <testfun.h> int main(int argc,char*argv[]) { pthread_mutex_t mLock; pthread_cond_t cond; pthread_mutex_init(&mLock, NULL); pthread_cond_init(&cond, NULL); pthread_mutex_lock(&mLock); time_t now = time(NULL); struct timespec end; end.tv_sec = now+10; end.tv_nsec = 0; int res = pthread_cond_timedwait(&cond, &mLock, &end); THREAD_ERROR_CHECK(res, "timedwait"); pthread_mutex_unlock(&mLock); return 0; }
pthread_cond_broadcast
函数说明
pthread_cond_broadcast 用于唤醒以指定条件变量阻塞的线程, 并使其重新获取锁, 但是和pthread_cond_signal不同的是, 虽然都是唤醒阻塞线程, pthread_cond_signal 每次从阻塞队列中取出一个唤醒, 而pthread_cond_broadcast 是以广播的方式把指定条件变量的阻塞队列线程全部唤醒.
ps: 要注意虚假唤醒问题(eg: 以一个生产者唤醒多个消费者为例)
// 虚假唤醒: pthread_cond_broadcast 唤醒了多个线程, 每一个进程一定会立即获得锁? 不会 eg: 广播: 唤醒了5个线程, 5个线程使用的还是同一把锁, 这个醒的5个线程, 都要去试图获取那个锁, 1个成功, 4个等待锁 1个成功获得锁的线程做了针对性的事情, 导致条件变化了; --> 其余线程喊醒了, 不满足条件, 做不了事, -> 虚假唤醒 --> 一种良好的解决办法 while(num >= 5){ 重新wait; } --> pthread_cond_broadcast 唤醒了多个线程: 要获取的不是同一把锁, 分别去试图获取自己设置的锁
代码示例
#include <testfun.h> typedef struct share_value{ int num; pthread_mutex_t mLock; pthread_cond_t cond; } share_value_t; void *fun(void *arg){ share_value_t *pShareValue = (share_value_t *)arg; pthread_mutex_lock(&pShareValue->mLock); pShareValue->num++; int childNum = pShareValue->num; pthread_mutex_unlock(&pShareValue->mLock); sleep(childNum); printf("i am %d child thread \n", childNum); pthread_mutex_lock(&pShareValue->mLock); printf("i am %d child thread before \n", childNum); pthread_cond_wait(&pShareValue->cond,&pShareValue->mLock); printf("i am %d child thread after \n", childNum); pthread_mutex_unlock(&pShareValue->mLock); } int main(int argc,char*argv[]) { share_value_t shareValue; shareValue.num = 0; pthread_mutex_init(&shareValue.mLock, NULL); pthread_cond_init(&shareValue.cond, NULL); pthread_t pid1, pid2; pthread_create(&pid1, NULL, fun, &shareValue); pthread_create(&pid2, NULL, fun, &shareValue); sleep(5); pthread_mutex_lock(&shareValue.mLock); pthread_cond_broadcast(&shareValue.cond); // pthread_cond_signal(&shareValue.cond); pthread_mutex_unlock(&shareValue.mLock); pthread_join(pid1, NULL); pthread_join(pid2, NULL); pthread_mutex_destroy(&shareValue.mLock); pthread_cond_destroy(&shareValue.cond); return 0; }
简单练习
1
现在有两个线程t1和t2,t1 打印 A 和 C,t2 打印 B。书写代码,使用条件变量每次的显示顺序都是A->B->C。
#include <my_header.h>
pthread_mutex_t key1;
pthread_mutex_t key2;
void *t1(void *arg) {
printf("A->");
pthread_mutex_unlock(&key2);
pthread_mutex_lock(&key1);
printf("C\n");
pthread_mutex_unlock(&key1);
return NULL;
}
void *t2(void *arg) {
pthread_mutex_lock(&key2);
printf("B->");
pthread_mutex_unlock(&key1);
return NULL;
}
int main(int argc, char *argv[])
{
pthread_mutex_init(&key1,NULL);
pthread_mutex_init(&key2,NULL);
pthread_t pid1,pid2;
pthread_mutex_lock(&key1);
pthread_mutex_lock(&key2);
pthread_create(&pid1,NULL,t1,NULL);
pthread_create(&pid2,NULL,t2,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
return 0;
}