同步&互斥
现代操作系统基本属于多任务系统,同时有大量可调度进程&线程在运行。在多任务操作系统中,同时运行的多个任务可能:
- 同时访问或使用某一个资源
- 多个任务间存在依赖关系,A任务的执行需要B任务的结果
以上两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。
–同步–
是指散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
–互斥–
是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
–死锁–
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁的必要条件
- 互斥条件
一个资源每次只能被一个进程使用; - 请求与保持条件
一个进程因请求资源而阻塞时,对已获得的资源保持不放; - 不剥夺条件
进程已获得的资源,在末使用完之前,不能强行剥夺; - 循环等待条件
若干进程之间形成一种头尾相接的循环等待资源关系;
处理死锁问题
- 死锁预防:在设计时确定资源分配算法,为保证不发生死锁,而破坏产生死锁的必要条件
如:破坏必要条件2(开始处理前,一次性申请全部资源,若申请不到这放弃已申请的资源,后续重试)
破坏必要条件4(对于同一组资源,所有处理均以有序的方式申请资源,依次申请资源) - 死锁避免:设定适当的超时时间,到时放弃所有已占用资源。
- 死锁检测:运行中及时检测到死锁,并进行相应的处理。
为了解决同步与互斥问题,linux系统提供了相应的机制,以安全且高效的处理一些场景。
互斥锁
互斥锁是一种使用频繁的同步手段,也被称为互斥量。对比信号量,可以将互斥锁的使用理解为信号量初值仅为1的一种情况。互斥锁是属于系统的内核级对象,它能够使线程拥有某个资源的绝对访问权,互斥锁主要包括使用数量、线程ID,递归计数器等,其中线程ID表示当前拥有互斥锁的线程,递归计数器表示线程拥有互斥锁的次数。
- 当互斥锁的线程ID为0时,表示互斥锁不被任何线程所拥有,此时系统会发出该互斥锁的通知信号,等待该互斥锁的其他线程中的某一个线程会拥有该互斥锁,同时,互斥锁的线程ID为当前拥有该互斥锁的线程的线程ID。
- 当互斥锁的线程ID不为0时,表示当前有线程拥有该互斥锁。系统不会发出互斥锁的通知信号。其他等待互斥锁的线程继续等待,直到拥有改互斥锁的线程释放互斥对象的拥有权。
互斥锁是一种类型为pthread_mutex_t的互斥变量。
其有2种初始化方式,一是用常量结构体PTHREAD_MUTEX_INITIALIZER初始化,二是用初始化函数初始化。
pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER // 静态初始化方法
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
功能:
初始化一个互斥锁。
参数:
mutex:互斥锁地址。类型是 pthread_mutex_t 。
attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。
其共有4种属性值:
PTHREAD_MUTEX_TIMED_NP : 这是缺省值,相当于NULL。当一个线程加锁后,其他请求这个锁的线程会进入一个队列等待,锁释放后按队列优先级获得锁。具有公平性。
PTHREAD_MUTEX_RECURSIVE_NP : 嵌套锁,允许同一个线程多次获得同一个锁,并多次解锁。
PTHREAD_MUTEX_ERRORCHECK_NP : 检错锁,如果同一个线程请求同一个锁,返回EDEADLK;负责和 PTHREAD_MUTEX_TIMED_NP操作一样(进入等待队列)
PTHREAD_MUTEX_ADAPTIVE_NP : 适应锁,如果被锁上了,等解锁后重新竞争,不存在等待队列,而是解锁 后先到先得。
返回值:
成功:0,成功申请的锁默认是打开的。
失败:非 0 错误码
restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。
互斥量其他主要函数
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:
销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非 0 错误码
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非 0 错误码
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:
调用该函数时,若互斥锁未加锁,则上锁,返回 0;
若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非 0 错误码
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
对指定的互斥锁解锁。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非0错误码
c++11标准库中的mutex操作,直接利用标准库初始化、加锁、解锁,并提供了RAII机制的lock_guard,自动释放锁。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int main(){
mutex m;
auto func = [&m](){ // 互斥锁是一种资源,不能拷贝,只能移动
for(int i=0; i<5; i++){
m.lock(); // 加锁
cout << this_thread::get_id() << endl;
this_thread::sleep_for(500ms);
cout << this_thread::get_id() << endl;
m.unlock(); // 解锁
}
};
thread t(func);
func();
t.join();
}
//int main(){
// mutex m;
// auto func = [&m](){
// for(int i=0; i<5; i++){
// lock_guard<mutex> guard(m);
// cout << this_thread::get_id() << endl;
// this_thread::sleep_for(500ms);
// cout << this_thread::get_id() << endl;
// }
// };
//
// thread t(func);
// func();
// t.join();
//}
读写锁
在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点:如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作;如果有其它线程写数据,则其它线程都不允许读、写操作。其可以分为读锁和写锁,如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁;如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。
其相关函数如下:
pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER; // 静态初始化方法
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
功能:
用来初始化 rwlock 所指向的读写锁。
参数:
rwlock:指向要初始化的读写锁指针。
attr:读写锁的属性指针。如果 attr 为 NULL 则会使用默认的属性初始化读写锁,否则使用指定的 attr 初始化读写锁。
返回值:
成功:0,读写锁的状态将成为已初始化和已解锁。
失败:非 0 错误码。
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
用于销毁一个读写锁,并释放所有相关联的资源。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
以阻塞方式在读写锁上获取读锁(读锁定)。
如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。
线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用 pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取读锁。
如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
在读写锁上获取写锁(写锁定)。
如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取写锁。
如果有任何的读者或写者持有该锁,则立即失败返回。
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
无论是读锁或写锁,都可以通过此函数解锁。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
一般来说,读写锁可以大致分为读者优先、写者优先、读写平衡三种实现方式。pthread_rwlock是基于读写平等实现的一种读写锁,其他两种相对更容易实现,下面是pthread_rwlock_t的结构体定义:
typedef union
{
struct
{
int __lock;
unsigned int __nr_readers;
unsigned int __readers_wakeup;
unsigned int __writer_wakeup;
unsigned int __nr_readers_queued;
unsigned int __nr_writers_queued;
int __writer;
int __shared;
signed char __rwelision;
unsigned long int __pad2;
/* FLAGS must stay at this position in the structure to maintain
binary compatibility. */
unsigned int __flags;
# define __PTHREAD_RWLOCK_INT_FLAGS_SHARED 1
} __data;
char __size[__SIZEOF_PTHREAD_RWLOCK_T];
long int __align;
} pthread_rwlock_t;
其读写平等是利用**__nr_readers_queued和__nr_writers_queued**字段保证的,再获取读锁时,会判断__nr_writers_queued字段是否为0,若不为0说明存在其他线程存在写意愿或者尝试加写锁,且如果该线程的优先级高于或等于本线程时,本线程无法继续加读锁,进而被阻塞,排队等待完成前面的写操作。这样可以保证写操作不会被一直阻塞而饿死。
条件变量
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量的两种情况:
- 条件不满, 阻塞线程
- 当条件满足, 通知阻塞的线程开始工作
条件变量的类型: pthread_cond_t,条件变量不是锁,其用于达到某个条件时来阻塞等待条件的满足来解除阻塞。
其相关函数如下:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 静态初始化方法
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
功能:
初始化一个条件变量
参数:
cond:指向要初始化的条件变量指针。
attr:条件变量属性,通常为默认值,传NULL即可
返回值:
成功:0
失败:非0错误号
int pthread_cond_destroy(pthread_cond_t *cond);
功能:
销毁一个条件变量
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号
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:互斥锁
返回值:
成功:0
失败:非0错误号
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
功能:
限时等待一个条件变量
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
abstime:绝对时间
返回值:
成功:0
失败:非0错误号
// timespec定义及使用方法
struct timespec {
time_t tv_sec; /* seconds */ // 秒
long tv_nsec; /* nanosecondes*/ // 纳秒
}
time_t cur = time(NULL); //获取当前时间。
struct timespec t; //定义timespec 结构体变量t
t.tv_sec = cur + 1; // 定时1秒
pthread_cond_timedwait(&cond, &mutex, &t);
int pthread_cond_signal(pthread_cond_t *cond);
功能:
唤醒至少一个阻塞在条件变量上的线程
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
唤醒全部阻塞在条件变量上的线程
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号
信号量
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,反之则将阻塞。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。信号量主要用于进程或线程间的同步和互斥这两种典型情况。当信号量初始化为1时,又被称为互斥信号量,实际功能类似互斥量。
相关函数如下:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:
创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
sem:信号量的地址。
pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
value:信号量的初始值。
返回值:
成功:0
失败: - 1
int sem_destroy(sem_t *sem);
功能:
删除 sem 标识的信号量。
参数:
sem:信号量地址。
返回值:
成功:0
失败: - 1
int sem_wait(sem_t *sem);
功能:
将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。
参数:
sem:信号量的地址。
返回值:
成功:0
失败: - 1
int sem_trywait(sem_t *sem);
功能:
以非阻塞的方式来对信号量进行减 1 操作。若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。
参数:
sem:信号量的地址。
返回值:
成功:0
失败: - 1
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:
限时尝试将信号量的值减 1, abs_timeout:绝对时间。
参数:
sem:信号量的地址。
返回值:
成功:0
失败: - 1
int sem_post(sem_t *sem);
功能:
将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。
参数:
sem:信号量的地址。
返回值:
成功:0
失败:-1
int sem_getvalue(sem_t *sem, int *sval);
功能:
获取 sem 标识的信号量的值,保存在 sval 中。
参数:
sem:信号量地址。
sval:保存信号量值的地址。
返回值:
成功:0
失败:-1
经典同步问题
生产者消费者问题
原文链接:https://blog.csdn.net/aimat2020/article/details/121641563
生产者消费者问题,也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。其中生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
**----问题分析---- **
1.要保证不让生产者在缓存还是满的时候仍然要向内写数据;
2.不让消费者试图从空的缓存中取出数据。
生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生
产者生产之后,消费者才能消费,他们也是同步关系。
只有生产者和消费者两个进程,正好是这两个进程存在着互斥关系和同步关系。那么需要解决的是互斥
和同步PV操作的位置。我们使用了三个信号量:full 和 empty用来解决唤醒的问题 ,而信号量mutex作为互斥信号量,它用于控制互斥访问缓冲池,互斥信号量初值为 1;信号量 full 用于记录当前缓冲池中“满”缓冲区数,初值为0。信号量 empty 用于记录当前缓冲池中“空”缓冲区数,初值为n。新的数据添加到缓存中后,full 在增加,而 empty 则减少。如果生产者试图在 empty 为0时减少其值,生产者就会被阻塞。下一轮中有数据被消费掉时,empty就会增加,生产者就会被“唤醒”。
// 代码是网上找的哈
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#define TRUE 1
#define FALSE 0
#define SIZE 11
typedef int QueueData; //定义一个整型变量QueueData,与为基本数据类型定义新的别名方法一样
typedef struct _queue //队列结构体
{
int data[SIZE];
int front; // 指向队头的下标
int rear; // 指向队尾的下标
}Queue;
struct data //信号量结构体
{
sem_t count;
Queue q;
};
struct data sem;
pthread_mutex_t mutex; //互斥变量使用特定的数据类型
int num = 0;
int InitQueue (Queue *q) // 队列初始化
{
if (q == NULL)
{
return FALSE;
}
q->front = 0;
q->rear = 0;
return TRUE;
}
int QueueEmpty (Queue *q) //判断空队情况
{
if (q == NULL)
{
return FALSE;
}
return q->front == q->rear;
}
int QueueFull (Queue *q) //判断队满的情况
{
if (q == NULL)
{
return FALSE;
}
return q->front == (q->rear+1)%SIZE;
}
int DeQueue (Queue *q, int x) //出队函数
{
if (q == NULL)
{
return FALSE;
}
if (QueueEmpty(q))
{
return FALSE;
}
q->front = (q->front + 1) % SIZE;
x = q->data[q->front];
return TRUE;
}
int EnQueue (Queue *q, int x) //进队函数
{
if (q == NULL)
{
return FALSE;
}
if (QueueFull(q))
{
return FALSE;
}
q->rear = (q->rear+1) % SIZE;
q->data[q->rear] = x;
return TRUE;
}
void *Producer(void* arg)
{
int i=0;
while(i<10)
{
i++;
int time = rand() % 10 + 1; //随机使程序睡眠0点几秒
usleep(time * 100000);
sem_wait(&sem.count); //信号量的P操作(使信号量的值减一)
pthread_mutex_lock(&mutex); //互斥锁上锁
if(!QueueFull(&sem.q)) //若队未满
{
num++;
EnQueue (&sem.q, num); //消息进队
printf("生产了一条消息,count=%d\n", num);
}
else printf("Full\n");
pthread_mutex_unlock(&mutex); //互斥锁解锁
sem_post(&sem.count); //信号量的V操作(使信号量的值加一)
}
printf("i(producer)=%d\n",i);
}
void *Customer(void* arg)
{
int i=0;
while(i<10)
{
i++;
int time = rand() % 10 + 1; //随机使程序睡眠0点几秒
usleep(time * 100000);
sem_wait(&sem.count); //信号量的P操作
pthread_mutex_lock(&mutex); //互斥锁上锁
if(!QueueEmpty(&sem.q)) //若队未空
{
num--;
DeQueue (&sem.q, num); //消息出队
printf("消费了一条消息,count=%d\n",num);
}
else printf("Empty\n");
pthread_mutex_unlock(&mutex); //互斥锁解锁
sem_post(&sem.count); //信号量的V操作
}
printf("i(customer)=%d\n",i);
}
int main()
{
srand((unsigned int)time(NULL));
//信号量地址,信号量在线程间共享,信号量的初始值
sem_init(&sem.count, 0, 10); //信号量初始化(做多容纳10条消息,容纳了10条生产者将不会生产消息)
pthread_mutex_init(&mutex, NULL); //互斥锁初始化
InitQueue(&(sem.q)); //队列初始化
pthread_t producid;
pthread_t consumid;
pthread_create(&producid, NULL, Producer, NULL); //创建生产者线程
pthread_create(&consumid, NULL, Customer, NULL); //创建消费者线程
pthread_join(consumid, NULL); //线程等待,如果没有这一步,主程序会直接结束,导致线程也直接退出。
sem_destroy(&sem.count); //信号量的销毁
pthread_mutex_destroy(&mutex); //互斥锁的销毁
return 0;
}
哲学家进餐问题
原文链接:https://blog.csdn.net/aimat2020/article/details/121649441
该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。
----问题思路----
经过分析可知,放在桌子上的5根筷子属于**临界资源,**在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量来表示一根筷子,由这5个信号量组成一个信号量数组。其描述如下:
semaphore chopstick[5]={1,1,1,1,1);
所有的信号量初始化为1,第 i 位哲学家的行为可描述如下:
while(1)
{
think();
hungry();
wait(chopstick[i]);
wait(chopstick[(i+1)%5]);
eat();
signal(chopstick[i]);
signal(chopstick[(i+1)%5]);
}
因为筷子是临界资源,因此当一个线程在使用某根筷子的时候,应该给这根筷子加锁,使其不能被其他进程使用。
根据以上分析,可以使用pthread_create函数创建五个线程,可以使用pthread_mutex_t chopstick[5]表示有五根筷子,五个不同的临界资源,并用pthread_mutex_init(&chopstick[i], NULL);来初始化他们。
按照上述的解决方案,确实可以有效解决互斥使用筷子的问题,但是同时也可能造成死锁。假如5个哲学家同时饥饿而各自拿起自己左边的筷子,这时每个人右边的筷子都不存在了,从而进入无限期的等待。
其解决死锁有三种方式,一是使用信号量保证仅有4名哲学家同时进餐,则必要一名哲学家可以完成进餐;二是采用AND信号量语义,同时申请左右2个筷子的资源;三是使奇数哲学家先拿去左边的筷子,偶数哲学家先拿起右边的筷子,如此先互斥在申请空闲资源以保证可以有哲学家完成进餐。具体源码见原文。
// 使用信号量保证仅有4名哲学家同时进餐
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#define TRUE 1
#define FALSE 0
#define N 5
int philosophers[N]={0,1,2,3,4};
int num=4;
struct data //信号量结构体
{
sem_t chopstick[N];
pthread_mutex_t chops[N];
}sem;
void delay()
{
int time = rand() % 10 + 1; //随机使程序睡眠0点几秒
usleep(time * 100000);
}
void think(int i)
{
printf("philosopher%d is thinking!\n",i);
}
void hungry(int i)
{
printf("philosopher%d is hungry!\n",i);
}
void eat(int i)
{
printf("philosopher%d is eating!\n",i);
}
void* philosopher(void *arg)
{
int i=*(int *)arg;
int a=10;
while(a>=0)
{
think(i);//思考状态
delay();//延迟
hungry(i);//饥饿状态
if(num>=0)
{
num--;//P操作
sem_wait(&sem.chopstick[i]);//取左边筷子
sem_wait(&sem.chopstick[(i+1)%5]);//取右边筷子
eat(i);//就餐
sem_post(&sem.chopstick[i]);//放回左边筷子
sem_post(&sem.chopstick[(i+1)%5]);//放回右边筷子
printf("ready!\n");
a--;
}
else
{
printf("chopsticks is not enough!\n");
a--;
}
num++;//V操作
}
}
int main()
{
int i;
pthread_t phd[N];
srand((unsigned int)time(NULL));
for(i=0;i<5;i++)//信号量初始化
{
sem_init(&sem.chopstick[i],0,1);
}
for(i=0;i<5;i++)//创建线程
{
pthread_create(&phd[i],NULL,philosopher,&philosophers[i]);
}
for(i=0;i<5;i++)//线程等待
{
pthread_join(phd[i],NULL);
}
for(i=0;i<5;i++)//信号量销毁
{
sem_destroy(&sem.chopstick[i]);
}
return 0;
}
读者写者问题
原文链接:https://blog.csdn.net/aimat2020/article/details/121692122
一个数据问价或记录可以被多个进程共享,我们把只读该文件的进程称为“读者进程”,其他进程为“写者进程”。允许多个进程同时读一个共享对象,但不允许一个写者进程和其他写者进程或读者进程同时访问共享对象。即:保证一个写者进程必须与其他进程互斥的访问共享对象的同步问题;读者-写者问题常用来测试新同步原语。
----问题思路----
首先对于上述问题进行抽象:读者和写者是互斥的,写者和写者是互斥的,读者和读者不互斥;两类进程,一种是写者,另一种是读者。写者很好实现,因为它和其他任何进程都是互斥的,因此对每一个写者进程都给一个互斥信号量的P、V操作即可;而读者进程的问题就较为复杂,它与写者进程是互斥的,而又与其他的读者进程是同步的,因此不能简单的利用P、V操作解决。
读者优先
为实现Reader和Writer进程之间在读或写时的互斥而设置了一个互斥信号量wmutex。再设置一个整型变量conut表示正在读的进程数目。
- 仅当count=0时,Reader进程才需要执行wait(wmutex)操作;同理,仅当Reader进程在执行了count减一操作后其值为0时,才需要执行signal(wmutex)操作,以便让Writer进程操作;
- 由于count是一个可被多个Reader进程访问的临界资源,因此为其设置一个互斥信号量rmutex;
等待Reader进程全部结束之后才逐步执行Writer进程。我们称这样的算法为读者优先算法,也就是说,当存在读进程时,写操作将被延迟,并且只要有一个读进程活跃,随后而来的读进程都将被允许访问文件。这样的方式下,会导致写进程可能长时间等待,且存在写进程“饿死”的情况。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#define N 5
int count=0,a=5,b=5;
int r[N]={0,1,2,3,4};
sem_t wmutex,rmutex;
void delay()
{
int time = rand() % 10 + 1; //随机使程序睡眠0点几秒
usleep(time * 100000);
}
void* Reader(void *arg)
{
int i=*(int *)arg;
while(a>0)
{
a--;
delay();
sem_wait(&rmutex);
if(count==0)
sem_wait(&wmutex);
count++;
sem_post(&rmutex);
printf("Reader%d is reading!\n",i);
printf("Reader%d reads end!\n",i);
sem_wait(&rmutex);
count--;
if(count==0)
sem_post(&wmutex);
sem_post(&rmutex);
}
}
void* Writer(void* arg)
{
while(b>0)
{
b--;
delay();
sem_wait(&wmutex);
printf("writer is writing!\n");
printf("writer writes end!\n");
sem_post(&wmutex);
}
}
int main()
{
int i;
pthread_t writer,reader[N];
srand((unsigned int)time(NULL));
sem_init(&wmutex,0,1);//互斥锁初始化
sem_init(&rmutex,0,1);
for(i=0;i<5;i++)//创建线程
{
pthread_create(&reader[i],NULL,Reader,&r[i]);
}
pthread_create(&writer,NULL,Writer,NULL);
pthread_join(writer,NULL);//线程等待
sem_destroy(&rmutex); //互斥锁的销毁
sem_destroy(&wmutex);
return 0;
}
写者优先
所谓写者优先,即:当有读者进程正在执行,写者进程发出申请,这时应该拒绝其他读者进程的请求,等待当前读者进程结束后立即执行写者进程,只有在无写者进程执行的情况下才能够允许读者进程再次运行。为此,增加一个信号量并且在上面的程序中 writer()和reader()函数中各增加一对PV操作,就可以得到写进程优先的解决程序。
在读者优先的基础上:
- 增加信号量r,初值是1,用于禁止所有的读进程。
- 增加一个记数器,即整型变量writecount,记录写者数,初值是0(原count改为readcount)。 writecount为多个写者共享的变量,是临界资源。用互斥信号量wmutex控制, wmutex初值是1(原wmutex改为mutex1)。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#define N 5
int readcount=0,writecount=0,a=5,b=2;
int r[N]={0,1,2,3,4};
int w[N]={0,1};
sem_t wmutex,rmutex,mutex1,num;
void delay()
{
int time = rand() % 10 + 1; //随机使程序睡眠0点几秒
usleep(time * 100000);
}
void* Reader(void *arg)
{
int i=*(int *)arg;
while(a>0)
{
a--;
delay();//延迟
//进入共享文件前的准备
sem_wait(&num);//在无写者进程时进入
sem_wait(&rmutex);//与其他读者进程互斥的访问readcount
if(readcount==0)
sem_wait(&mutex1);//与写者进程互斥的访问共享文件
readcount++;
sem_post(&rmutex);
sem_post(&num);
//reader
printf("Reader%d is reading!\n",i);
printf("Reader%d reads end!\n",i);
//退出共享文件后的处理
sem_wait(&rmutex);
readcount--;
if(readcount==0)
sem_post(&mutex1);
sem_post(&rmutex);
}
}
void* Writer(void *arg)
{
int i=*(int *)arg;
while(b>0)
{
b--;
delay();
//进入共享文件前的准备
sem_wait(&wmutex);//保证多个写者进程能够互斥使用writecount
writecount++;
if(writecount==1)
sem_wait(&num);//用于禁止读者进程
sem_post(&wmutex);
//writer
sem_wait(&mutex1);//与其他所有进程互斥的访问共享文件
printf("writer%d is writing!\n",i);
printf("writer%d writes end!\n",i);
sem_post(&mutex1);
//退出共享文件后的处理
sem_wait(&wmutex);
writecount--;
if(writecount==0)
sem_post(&num);
sem_post(&wmutex);
}
}
int main()
{
int i;
pthread_t writer[N],reader[N];
srand((unsigned int)time(NULL));
sem_init(&wmutex,0,1);//互斥锁初始化
sem_init(&rmutex,0,1);
sem_init(&mutex1,0,1);
sem_init(&num,0,1);
for(i=0;i<5;i++)//创建线程
{
pthread_create(&reader[i],NULL,Reader,&r[i]);
}
for(i=0;i<2;i++)//创建线程
{
pthread_create(&writer[i],NULL,Writer,&w[i]);
}
for(i=0;i<2;i++)//等待线程
{
pthread_join(writer[i],NULL);
}
for(i=0;i<5;i++)//等待线程
{
pthread_join(reader[i],NULL);
}
sem_destroy(&rmutex); //互斥锁的销毁
sem_destroy(&wmutex);
sem_destroy(&mutex1);
sem_destroy(&num);
return 0;
}
读写公平
pthread_rwlock就是一种读写公平的读写锁,没找到合适的简单实现,有兴趣可以去看一下pthread源码。
其主要特点就是如果有写锁或者写意向,后续的读锁也要阻塞,不能一直叠加读锁以至于饿死写进程。