进程同步
进程同时处理同一串数据, 会造成不确定性,比如有多个进程同时对一个文件进行读写,那么读文件的进程无法确定自己读到的数据是否是它本来想要的数据,还是被修改的数据,除此以外,当先读后写时,由于缓冲区没有写入数据,读进程无数据可读,就会因此被阻塞(使用管道通信)。
这种两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精准时序,称为数据竞争,而这种多个程序可以并发执行,但是由于系统资源有限,程序的执行不是一贯到底的,以不可预知的速度向前推进,这又被称为异步性。
这种受访问顺序影响的数据是没有意义的(程序的运行不能有二义性),所以为了能够使得进程有能够有一定的顺序来访问数据,从而引入了同步的概念。
所谓进程同步就是指协调这些完成某个共同任务的并发线程,在某些位置上指定线程的先后执行次序、传递信号或消息
本文将主要讲解如何使用信号量实现进程同步。
信号量基本概念
信号量相当于一个信号灯,在程序实现中往往是一个非负整数。在实际生活中,如火车进站前会看到的信号灯,若灯亮说明火车可以进站,否则不能进站,这里的信号灯就可以看作是信号量,火车看作是进程,能否进站即能否访问资源。
在进程进入一个关键代码段之前,进程必须获取一个信号量;一旦该关键代码段执行完毕了,那么该线程必须释放信号量。其它想进入该关键代码段的进程必须等待直到第一个进程释放信号量。
信号量
- 作用:控制多进程共享资源的访问(资源有限并且不共享)
- 本质:任一时刻只能有一个进程访问临界区(代码),数据更新的代码。
PV操作
PV操作即是针对信号量进行的相应操作,PV操作由P操作原语和V操作原语组成(原语是不可中断的过程)。
当进程执行P操作,若信号量大于零(有共享资源),则信号量减一,进程继续执行;若信号量为零,则进程等待。
当进程执行V操作,若信号量大于零(有共享资源),则信号量加一;若信号量为零,则唤醒等待进程。如下图所示:
关于信号量的函数
使用信号量的相关函数时需要添加头文件#include <semapore.h>
,链接库为pthread
。
接下来我们具体学习关于信号量的相关函数,根据信号量是否命名分为命名信号量(基于文件实现)和匿名信号量(基于内存)。
命名信号量相关函数
操作 | 函数定义 |
---|---|
创建 | sem_t *sem_open(const char *name, int oflag, mode_t mode,unsigned int value) |
删除 | int sem_unlink(const char *name) |
打开 | sem_t *sem_open(const char *name, int oflag) |
关闭 | int sem_close(sem_t *sem) |
挂出 | int sem_post(sem_t *sem) |
等待 | int sem_wait(sem_t *sem) |
尝试等待 | int sem_trywait(sem_t *sem) |
获取信号量的值 | int sem_getvalue(sem_t *sem, int *sval) |
匿名信号量相关函数
操作 | 函数 |
---|---|
初始化 | int sem_init (sem_t *sem , int pshared, unsigned int value) |
销毁 | int sem_destroy(sem_t *sem) |
挂出 | int sem_post(sem_t *sem) |
等待 | int sem_wait(sem_t *sem) |
尝试等待 | int sem_trywait(sem_t *sem) |
获取信号量的值 | int sem_getvalue(sem_t *sem, int *sval) |
信号量的使用
命名信号量
我们首先创建两个进程,具体代码如下:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
fork();
for(int i=0; i<5; i++){
cout << getpid() << ":before" << endl;
sleep(1); // 模拟耗时操作
cout << getpid() << ":before" << endl;
}
}
执行结果如下:
显然pid为6099和6100的两个进程的执行顺序显然是不受控制的,接下来我们借用信号量来限制两个进程的执行顺序,具体代码如下(代码备注中有关于函数的细节使用):
#include <iostream>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/mman.h>
using namespace std;
int main(){
sem_t* p = sem_open("/test",O_CREAT|O_RDWR,0666,1);
// 信号量命名必须是以/开头,其并不是根目录的意思,该文件存于/dev/shm文件下
// 标志位O_CREAT|O_RDWR有两个作用:该信号量不存在就创建,存在就打开
// 0666为该信号量文件的权限
// 1 为信号量个数(也可以设置为其他正整数)
fork();
for(int i=0; i<5; ++i){
usleep(100);
sem_wait(p); // P操作,出现阻塞,初始值减一
// 临界区开始
cout << getpid() << "before" << endl;
sleep(1); //模拟耗时操作
cout << getpid() << "after" << endl;
// 临界区结束
sem_post(p); // 信号量初始值+1,唤醒阻塞进程,阻塞进程唤醒不定
}
sem_close(p);
p = NULL;
}
执行结果如下:
显然此时一个进程进入临界区执行操作离开后另一个进程才会开始执行,由此两个进程开始有序执行,实现了同步操作。
需要注意的是,在代码备注中我们也提到了,创建的文件将会默认在/dev/shm
下创建,且某些系统默认将会在该文件名前增加sem.前缀,我们可以使用ls查看,如下图:
匿名信号量
以上我们使用了一个命名的信号量来实现了进程同步,除此以外,我们也可以使用匿名的信号量来实现同步。
我们知道fork所生成的父子进程的内存资源是共享的,故而我们可以借助这个特点,来创建一个父子进程都可以访问到的内存来初始化信号量,这里我们就需要借助匿名的共享内存来实现这个操作。(如果需要了解共享内存相关知识,可以查看:【进程间通信2】使用共享内存实现进程间的通信(附C++实现代码))
故而,匿名信号量是基于共享内存来实现的,它不涉及需要指定某个特定文件,故而创建这种匿名信号量不会生成/dev/shm下的文件。
我们给出具体代码如下:
#include <iostream>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/mman.h>
using namespace std;
int main(){
sem_t* p = (sem_t*)mmap(NULL,sizeof(sem_t),PROT_WRITE|PROT_READ,MAP_SHARED|MAP_ANON,-1,0); // 申请动态内存
sem_init(p,1,1);
// 创建匿名信号量,必须要对信号量进行初始化
// 第一个1指的是进程间的信号量(0的话表示是线程)
// 第二个1指的是信号量个数
fork();
for(int i=0; i<5; ++i){
usleep(100);
sem_wait(p); // 出现阻塞,初始值减一
cout << getpid() << "before" << endl;
sleep(1); //模拟耗时操作
cout << getpid() << "after" << endl;
sem_post(p); // 信号量初始值+1,唤醒阻塞进程,阻塞进程唤醒不定
}
sem_close(p);
sem_destroy(p); // 销毁信号量
munmap(p,sizeof(sem_t)); // 释放申请的内存
p = NULL;
}
执行结果如下:
多值信号量
在实际生活中的资源往往不只一个,在这部分我们针对停车场这个应用场景,在停车场没有车位的情况下,车无法进入停车场停车,如有车位则正常停车。具体代码如下:
park.cpp:用于创建多值信号量。模拟创建停车场的过程,信号量个数就是我们的停车场车位个数。
注意这里只需要创建,所以sem_open函数中的标志位为O_CREAT
。
#include <iostream>
#include <semaphore.h>
#include <fcntl.h>
using namespace std;
// a.out name num
// name:信号量名
// num:信号量个数
int main(int argc, char* argv[]){
if(3!=argc){
printf("Usage:%s name num\n",argv[0]);
return 1;
}
sem_open(argv[1], O_CREAT, 0666, atoi(argv[2]));
}
需注意该文件运行的命令格式为可执行文件名 创建的信号量文件名 该文件的信号量个数
,我在我的命令行的创建命令为./a.out /park 5
。
car.cpp:模拟汽车出入停车场的过程,进入一辆车之后车位减少一个
// a.out name num
// name:信号量名称
int main(int argc, char* argv[]){
if(2!=argc){
printf("Usage:%s name",argv[0]);
return 1;
}
sem_t* p = sem_open(argv[1],O_RDWR);
sem_wait(p); // 进入停车场,占用一个车位
cout << getpid() << ":enter" << endl;
sleep(3); // 模拟停车时间
sem_post(p); // 出停车场,得到一个空车位
cout << getpid() << ":exit" << endl;
sem_close(p);
}
然后我们就可以运行代码如下: