多任务互斥与同步
1. 互斥和同步概述
同步和互斥是用于解决如下两个问题:
1)在多任务操作系统中,同时运行的多个任务可能都需要访问/使用同一种资源。
2)多个任务之间有依赖关系,某个任务的运行依赖于另一个任务
互斥:
-
一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源
-
POSIX标准中进程和线程同步和互斥的方法,主要有信号量好互斥锁两种方式
同步:
两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。
【注】同步是特殊的互斥
2. 互斥锁
2.1 互斥锁的概念
用于线程的互斥
mutex互斥锁是一种简单的加锁的方法来控制对共享资源的访问
互斥锁只有两种状态,即加锁(lock)和解锁(unlock)
互斥锁的操作流程如下
1)在访问共享资源前,对互斥锁进行加锁
2)在访问完成后释放互斥锁
3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放
2.2 初始化互斥锁
不管多少个任务,如果是完成互斥,只需要一把锁
mutex
用pthread_mutex_t
数据类型表示,在使用互斥锁前,必须先对它进行初始化
2.2.1 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
2.2.2 动态初始化
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
参数:
mutex
:互斥锁地址。类型是pthread_mutex_t
attr
:设置互斥量的属性,通常可采用默认属性,即可将attr
设为NULL
返回值:成功返回0,失败返回非0
2.3 互斥锁上锁
对互斥锁上锁,若已经上锁,则调用者一直阻塞到互斥锁解锁【阻塞的】
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回非0
2.4 互斥锁上锁2
对互斥锁上锁,若已经上锁,则上锁失败,函数立即返回。【非阻塞的】
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回非0
2.5 互斥锁解锁
对指定的互斥锁解锁
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回非0
2.6 销毁互斥锁
销毁指定的一个互斥锁
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回非0
2.7 死锁
如:两个锁使用错误,导致死锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex1,mutex2;
void *task1(void *data)
{
char *taskName = (char *)data;
pthread_mutex_lock(&mutex1);
printf("%s 获取锁1成功,等待1秒之后获取锁2\n",taskName);
sleep(1);
pthread_mutex_lock(&mutex2);
printf("%s 获取锁2成功\n",taskName);
printf("%s\n",taskName);
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void *task2(void *data)
{
char *taskName = (char *)data;
pthread_mutex_lock(&mutex2);
printf("%s 获取锁2成功,等待1秒之后获取锁1\n",taskName);
sleep(1);
pthread_mutex_lock(&mutex1);
printf("%s 获取锁1成功\n",taskName);
printf("%s\n",taskName);
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
int main(int argc,char const *argv[])
{
//动态初始化互斥锁
pthread_mutex_init(&mutex1,NULL);
pthread_mutex_init(&mutex2,NULL);
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,task1,"hello");
pthread_create(&tid2,NULL,task2,"good");
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
//销毁锁
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);
return 0;
}
解决问题,一定要按顺序加锁与解锁
void *task1(void *data)
{
char *taskName = (char *)data;
pthread_mutex_lock(&mutex1);
printf("%s 获取锁1成功, 等待1秒之后获取锁2\n", taskName);
sleep(1);
pthread_mutex_lock(&mutex2);
printf("%s 获取锁2成功\n", taskName);
printf("%s\n", taskName);
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void *task2(void *data)
{
char *taskName = (char *)data;
pthread_mutex_lock(&mutex1);
printf("%s 获取锁1成功, 等待1秒之后获取锁2\n", taskName);
sleep(1);
pthread_mutex_lock(&mutex2);
printf("%s 获取锁2成功\n", taskName);
printf("%s\n", taskName);
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
3. 读写锁
如果就两个任务,一个读,一个写,建议使用互斥锁。
如果3个或3个以上的任务,多个读或多个写,建议使用读写锁
3.1 读写锁的概述
读写锁(Read-Write-Lock)是一种并发控制机制,用于在多线程环境下对共享资源进行读写操作的同步和互斥。
读写锁允许多个线程同时对共享资源进行读取操作,但在有线程进行写入操作时,其他线程无法进行读取或写入操作,从而保证了对共享资源的安全访问
读写锁通常有两种状态:读取状态和写入状态。当没有线程进行写入操作时,多个线程可以同时获取读取锁,从而并发地读取共享资源。这样可以提高并发性能,因为读取操作不会修改共享资源,不会产生数据竞争。
当有线程获取写入锁时,其他线程无法获取读取锁或写入锁,从而实现了独占式的写入操作。这是为了保证数据的一致性和完整性,因为写入操作可能会修改共享资源,需要排他地进行
读写锁的基本特点如下:
- 多个线程可以同时获取读取锁,实现并发的读取操作
- 写入锁是独占的,当有线程获取写入锁时,其他线程无法获取读取锁或写入锁
- 当有线程持有读取锁时,其他线程可以继续获取读取锁,但不能获取写入锁
- 读取锁和写入锁之间的优先级关系可以根据具体的实现策略而定,如读优先或写优先
读写锁的使用场景通常是在读操作远远多于写操作的情况下,通过允许多个线程同时读取来提高并发性能。然而,需要注意的是,如果读操作的频率非常高,而写操作的频率很低,那么读写锁可能会导致写入操作的饥饿,即写入操作一直得不到执行。
3.2 读写锁的API
读写锁的类型:
pthread_rwlock_t
3.2.1 动态初始化
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict_rwlock,const pthread_rwlockattr_t *restrict_attr);
参数:
rwlock
:指向要初始化的读写锁指针attr
:读写锁的属性指针。如果attr为NULL则会使用默认的属性初始化读写锁,否则使用指定的attr初始化读写锁
3.2.2 静态初始化
pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方法等价于使用NULL指定的attr参数调用pthread_init()
来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER
宏不进行错误检查
3.2.3 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
3.2.4 申请读锁
阻塞方式申请:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
以阻塞方式在读写锁上获取读锁(读锁定)
如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。
线程可以成功调用pthread_rwlock_rdlock()
函数n次,但是之后该线程必须调用pthread_rwlock_unlock()
函数n次才能解除锁定。
非阻塞方式申请:
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取读锁
如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。
3.2.5 申请写锁
阻塞方式申请:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
在读写锁上获取写锁(写锁定)
如果没有写者持有该锁,并且没有写者的读者持有该锁,则调用线程会获取写锁
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁
非阻塞方式申请:
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取写锁
如果有任何的读者或写者持有该锁,则立即返回失败
3.2.6 释放读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
无论是读锁或写锁,都可以通过此函数解锁
例:银行存取款
设计两个线程任务函数,一个完成取款,一个完成存款
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
void *task1(void *data)
{
int *money = (int *)data;
//存款任务
pthread_rwlock_wrlock(&rwlock);
//查询余额
printf("金额:%d\n",*money);
sleep(1);
//从键盘读取存入的金额
printf("请输入存款金额:");
fflush(stdout);
int m;
scanf("%d",&m);
*money += m;
//修改余额并打印结果
printf("存款成功,余额为:%d\n",*money);
pthread_rwlock_unlock(&rwlock);
pthread_exit(NULL);
}
void *task2(void *data)
{
int *money = (int*)data;
//取款任务
pthread_rwlock_wrlock(&rwlock);
//查询余额
printf("余额:%d\n",*money);
sleep(1);
//从键盘读取取出的金额
printf("请输入取款金额:");
fflush(stdout);
int m;
scanf("%d",&m);
if(*money>=m)
{
*money-=m;
//修改余额并打印结果
printf("取款成功,余额为:%d\n",money);
}
else
{
printf("取款失败,余额不足\n");
}
pthread_rwlock_unlock(&rwlock);
pthread_exit(NULL);
}
int main(int argc,char const *argv[])
{
int money = 1000;//初始存款
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,task1,&money);
pthread_create(&tid2,NULL,task2,&money);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
4. 条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁。
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止,通常条件变量和互斥锁同时使用。
条件变量的两个动作:条件不满足,阻塞线程;当条件满足,通知阻塞的线程开始工作
条件变量的类型:
pthread_cond_t
4.1 条件变量初始化
动态条件变量初始化
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
attr
:条件变量属性,通常为默认值,传NULL即可。
静态初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
4.2 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
4.3 等待条件满足
4.3.1 阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict_cond,pthread_mutex_t *restrict_mutex);
功能:
- a)阻塞等待条件变量cond(参1)满足
- b)释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
- a)b)两步为一个原子操作
- c)当被唤醒,
pthread_cond_wait
函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex)
;
参数:
cond
:指向要初始化的条件变量指针mutex
:互斥锁
4.3.2 限时等待一个条件变量
int pthread_cond_timewait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
cond
:指向要初始化的条件变量指针mutex
:互斥锁abstime
:绝对时间
4.4 唤醒等待的线程
//唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
//唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
4.5 条件变量的工作原理
4.6 案例:生产者与消费者
一个仓库刚开始有3个产品
如果生产者生产后进入仓库存放产品,消费者就不能进入仓库购买
如果消费者进入仓库购买产品,生产者就不能进入仓库存放产品
如果仓库的商品为0,消费者不能进入仓库购买
如:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//互斥锁和条件变量的初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *producer_task(void *data)
{
int *n=(int*)data;
while(1)
{
pthread_mutex_lock(mutex);
(*n)++;
printf("生产线程(%ld)生产了%d产品\n",pthread_self(),*n);
//发出通知,让等待消费的线程恢复(条件满足)
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit(NULL);
}
void *consumer_task(void *data)
{
int *n=(int*)data;
while(1)
{
pthread_mutex_lock(mutex);
while(*n==0)
{
pthread_cond_wait(&cond,&mutex);
}
printf("消费者(%ld)消费了%d产品\n",pthread_self(),*n);
(*n)--;
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_exit(NULL);
}
int main(int argc,char const *argv[])
{
int num=3;//3个产品
pthread_t threads[5];
//创建2个生产线程
for(int i=0;i<2;i++)
{
pthread_create(&threads[i],NULL,producer_task,&num);
}
//创建3个消费者线程
for(int i=2;i<5;i++)
{
pthread_create(&threads[i],NULL,consumer_task,&num);
}
for(int i=0;i<5;i++)
{
pthread_join(threads[i],NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
5. 信号量
5.1 信号量的概念
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
当信号量值大于0时,则可以访问,否则将阻塞,PV
原语是对信号量的操作,一次P操作使信号量减1,一次V操作使信号量加1。
信号量数据类型为:sem_t
信号量完成互斥
不管有多少个任务,只要是互斥,只要一个信号量,并且初始化1
信号量用于同步
有几个任务就需要有几个信号量,先执行为任务的信号初始化为1,其他信号量初始化为0,所有任务P自己的信号,V下一个任务的信号量
5.2 信号量的API
5.2.1 信号量的初始化
创建一个信号量并初始化它的值,一个无名信号量在被使用前必须先初始化
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
参数:
sem
:信号量的地址pshared
:等于0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。value
:信号量的初始值
返回值:成功0,失败-1
5.2.2 P操作
功能:将信号量减1,如果信号量的值为0则阻塞,大于0可以减1
int sem_wait(sem_t *sem);
功能:尝试将信号量减1,如果信号量的值为0不阻塞,立即返回,大于0减1
int sem_trywait(sem_t *sem);
返回值:成功0失败-1
5.2.3 V操作
将信号量的值加1并发出信号唤醒等待线程
int sem_post(sem_t *sem);
返回值:成功0失败-1
5.2.4 销毁信号量
int sem_destroy(sem_t *sem);
返回值:成功0失败-1
5.2.5 获取信号量的计数值
int sem_getvalue(sem_t *sem,int *sval);
银行案例信号量实现:先存款再取款,通过信号量实现同步线程操作
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
sem_t sem1,sem2;//信号量
void *task1(void *data)
{
int *money=(int *)data;
//存款任务
sem_wait(&sem1);
//查询余额
printf("存款任务-余额:%d\n",*money);
sleep(1);
//从键盘读取存入的金额
printf("请输入存款金额:");
fflush(stdout);
int m;
scanf("%d",&m);
*money += m;
//修改余额并打印结果
printf("存款成功,余额为:%d\n",*money);
sem_post(&sem2);
pthread_exit(NULL);
}
void *task2(void *data)
{
int *money=(int *)data;
//取款任务
sem_wait(&sem2);
//查询余额
printf("余额:%d\n",*money);
sleep(1);
//从键盘读取取出的金额
printf("请输入存款金额:");
fflush(stdout);
int m;
scanf("%d",&m);
if(*mony>=m)
{
*money-=m;
//修改余额并打印结果
printf("取款成功,余额为:%d\n",*money);
}else{
printf("取款失败,余额不足\n");
}
sem_post(&sem1);
pthread_exit(NULL);
}
int main(int argc,char const *argv[])
{
//初始化信号量
sem_init(&sem1,0,1);//存款的信号量初始化
sem_init(&sem2,0,0);//取款的信号量初始化
int money = 1000;//存款
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,task1,&money);
pthread_create(&tid2,NULL,task2,&money);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
sem_destroy(&sem1);
sem_destroy(&sem2);
return 0;
}
6. 有名信号量
6.1 有名信号量概念
其实posix的信号有两种:
- 1.无名信号量
- 2.有名信号量
无名信号量一般用于线程间同步或互斥。而有名信号量一般用于进程间同步或互斥
6.2 创建有名信号量
创建一个信号量
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
//信号量存在
sem_t *sem_open(const char *name,int oflag);
//信号量不存在
sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned int value);
参数:
name
:信号量文件名flags
:sem_open
函数的行为标志,同open的flagmode
:文件权限(可读、可写、可执行)的设置value
:信号量初始值
返回值:成功返回信号量的地址,失败返回SEM_FAILED
6.3 信号量的关闭
int sem_close(sem_t *sem);
返回值:成功返回0,失败返回-1
6.4 信号量的删除
删除信号量的文件
int sem_unlink(const char *name);
返回值:成功0,失败-1
如:
#include <sys/stat.h>
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
void printer(char *msg)
{
while(*msg)
{
printf("%c",*msg++);
fflush(stdout);
sleep(1);
}
}
父子进程间有名信号的互斥
int main()
{
sem_t *sem = sem_open("mysem",O_CREAT|O_RDWR,0666,1);
int pid = fork();
if(pid==0)
{
sem_wait(sem);
printer("dijun666");
sem_post(sem);
_exit(0);
}
else if(pid>0)
{
sem_wait(sem);
printer("changxihe666");
sem_post(sem);
wait(NULL);
printf("\n--over--\n");
sem_close(sem);
sem_unlink("mysem");//删除信号量文件
}
return 0;
}
父子进程,有名信号量的同步
int main()
{
sem_t *sem1 = sem_open("mysem1",O_CREAT|O_RDWR,0666,1);
sem_t *sem2 = sem_open("mysem2",O_CREAT|O_RDWR,0666,0);
int pid = fork();
if(pid==0)
{
sem_wait(sem1);
printer("dijun666");
sem_post(sem2);
_exit(0);
}
else if(pid>0)
{
sem_wait(sem2);
printer("changxihe666");
sem_post(sem1);
wait(NULL);
printf("\n--over--\n");
sem_close(sem1);
sem_close(sem2);
sem_unlink("mysem1");//删除信号量文件
sem_unlink("mysem2");
}
return 0;
}
无血缘关系的进程的互斥
int main()
{
//打开信号量文件
sem_t *sem = sem_open("mysem",O_CREAT|O_RDWR,0666,1);
sem_wait(sem);
#ifdef DIJUN
printer("dijun666\n");
#else
printer("changxihe888\n");
#endif
sem_post(sem);
sem_close(sem);
return 0;
}
无血缘关系的进程的同步
int main()
{
//打开信号量文件
sem_t *sem1 = sem_open("mysem1",O_CREAT|O_RDWR,0666,1);
sem_t *sem2 = sem_open("mysem2",O_CREAT|O_RDWR,0666,1);
#ifdef DIJUN
sem_wait(sem2);
printer("dijun666\n");
sem_post(sem1);
#else
sem_wait(sem1);
printer("changxihe888\n");
sem_post(sem2);
#endif
sem_close(sem1)
sem_close(sem2);
return 0;
}
标签:同步,int,信号量,互斥,mutex,pthread,sem,多任务
From: https://www.cnblogs.com/dijun666/p/17704050.html