目录
线程之间的同步与互斥,一般都是通过互斥锁来实现,用来解决线程与线程之间互相抢夺公共资源的情况,那么如果是进程与进程之间的同步与互斥又应该怎么实现呢,这里需要用到信号量这个概念
【信号量】
1.信号量的概述
信号量(类型sem_t)被广泛的应用于线程和进程之间的同步与互斥,信号量的本质其实是一个非负的整数计数器,它被用来控制对公共资源的访问,当信号量大于0的时候,才允许访问不会发生阻塞
信号量的控制使用的是PV原语,P操作可以使当前信号量减一,V操作可以使当前信号量加一,任务之间就是通过这个PV原语来完成线程之间的同步与互斥。
2.信号量用于互斥
如果信号量是用于任务之间的互斥,那么仅仅只需要一个信号量就足够了,因为互斥是随机进入任务的,不需要有顺序之分
信号量作用于互斥其实和互斥锁还是有点相似的,因为信号量本质就是一个非负的整数计数器,那么只需要在每个任务之间完成 P 任务 V 操作即可。
那么怎么理解这个P 任务 V 操作呢,P也就是信号量减一,我们默认情况下设置一个信号量为1,那么多个任务之间就会争夺使用P操作让信号量可以成功减一,哪个任务成功让信号量减一,哪个任务就可成功执行(有点像抢夺互斥锁的上锁),执行完毕后,使用V操作让信号量加一(互斥锁的解锁),此时其他任务又可以抢夺让信号量减一(一个互斥锁使用完毕解锁后,其他线程抢夺上锁)
3.信号量作用于同步
如果信号量作用于同步,那执行多少个任务就需要多少个信号量,因为同步的话不像互斥,同步是有执行任务的顺序的,就是需要使用PV原话去操控任务之间的执行顺序
我们都知道,信号量是通过判断是否大于0来阻塞任务进行的,如果信号量不是大于0,那么不会执行任务,那么我们可以首先设置第一个执行的任务的信号量是1,成功的让这个任务的P操作可以执行,然后再在这个任务的结尾使用V操作让下一个想要执行的任务的信号量加一,此时下一个任务的P操作可以执行,任务也就可以执行,如此往复,就可以通过信号量来控制任务之间的执行顺序,做到同步
【信号量的API】
1.信号量初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能
初始化一个信号量
参数
sem:信号量的地址
pshared:
0:作用于线程之间共享(常用)
不等于0:信号量进程间共享
value:信号量的初始值
返回值
成功:0
失败:-1
2.信号量减一(P操作)
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
功能
wait:将信号量减一,如果信号量为0,无法成功减一,此时阻塞直到P操作成功执行
trywait:尝试将信号量减一,如果信号量为0,无法减一,直接返回不阻塞
参数
sem:信号量地址
返回值
成功:0
失败:-1
3.信号量加一(V操作)
#include <semaphore.h>
int sem_post(sem_t *sem);
功能
将信号量加一
参数
sem:信号量地址
返回值
成功:0
失败:-1
4.信号量的销毁
#include <semaphore.h>
int sem_destroy(sem_t *sem);
功能
销毁信号量
参数
sem:信号量地址
返回值
成功:0
失败:-1
【信号量作用于线程的互斥】
通过程序来演示信号量对线程间的互斥现象,三个线程分别输出hello、world、!!!、怎么做到避免三个线程互相抢夺输出资源
如果没有利用信号量控制,线程之间会互相抢夺资源。
使用信号量后(因为是线程之间为互斥关系,仅仅需要一个信号量)
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
//定义一个信号量
sem_t sem;
//多线程
void* pthread1_fun(void* argc)
{
//执行P操作
sem_wait(&sem);
char* str = (char*)argc;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
//执行V操作
sem_post(&sem);
return NULL;
}
void* pthread2_fun(void* argc)
{
//执行P操作
sem_wait(&sem);
char* str = (char*)argc;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
//执行V操作
sem_post(&sem);
return NULL;
}
void* pthread3_fun(void* argc)
{
//执行P操作
sem_wait(&sem);
char* str = (char*)argc;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
//执行V操作
sem_post(&sem);
return NULL;
}
int main(int argc, char const *argv[])
{
//创建三个线程
pthread_t pthread1 , pthread2 , pthread3;
//初始化信号量
sem_init(&sem, 0, 1);
//初始化线程
pthread_create(&pthread1, NULL, pthread1_fun , "hello");
pthread_create(&pthread2, NULL, pthread2_fun , "world");
pthread_create(&pthread3, NULL, pthread3_fun , "!!!!!");
//回收线程
pthread_join(pthread1, NULL);
pthread_join(pthread2, NULL);
pthread_join(pthread3, NULL);
//销毁信号量
sem_destroy(&sem);
return 0;
}
【信号量作用于线程的同步】
线程的同步相较于线程的互斥,最主要的区别就是同步的线程之间是区分线程的执行顺序的,那么有几个线程就需要用到几个条件变量
这里设置的线程执行顺序为线程1,线程2,线程3
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
//因为三个线程同步,创建三个信号量
sem_t sem1, sem2, sem3;
//多线程
void* pthread1_fun(void* argc)
{
//执行信号量1的P操作
sem_wait(&sem1);
char* str = (char*)argc;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
//执行信号量2的V操作(让线程2可以解除阻塞)
sem_post(&sem2);
return NULL;
}
void* pthread2_fun(void* argc)
{
//执行信号量2的P操作
sem_wait(&sem2);
char* str = (char*)argc;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
//执行信号量3的V操作(让线程3可以解除阻塞)
sem_post(&sem3);
return NULL;
}
void* pthread3_fun(void* argc)
{
//执行信号量3的P操作
sem_wait(&sem3);
char* str = (char*)argc;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
//执行信号量1的V操作(让线程1可以解除阻塞)
sem_post(&sem1);
return NULL;
}
int main(int argc, char const *argv[])
{
//创建三个线程
pthread_t pthread1 , pthread2 , pthread3;
//初始化信号量,想要第一个执行的线程初始信号量必须为1
sem_init(&sem1, 0, 1);
sem_init(&sem2, 0, 0);
sem_init(&sem3, 0, 0);
//初始化线程
pthread_create(&pthread1, NULL, pthread1_fun , "hello");
pthread_create(&pthread2, NULL, pthread2_fun , "world");
pthread_create(&pthread3, NULL, pthread3_fun , "!!!!!");
//回收线程
pthread_join(pthread1, NULL);
pthread_join(pthread2, NULL);
pthread_join(pthread3, NULL);
//销毁信号量
sem_destroy(&sem1);
sem_destroy(&sem2);
sem_destroy(&sem3);
return 0;
}
【无名信号量 作用于 血缘关系进程的互斥】
我们用条件变量解决了线程之间同步和互斥的问题,但是该如何解决进程之间的同步和互斥问题呢。线程因为可以共享进程的资源,也就是全局变量,所以可以很容易的通过条件变量来解决,但是进程因为是互相独立的空间,又改如何解决呢。我们要利用好进程和进程之间可以共同访问的资源,这里使用内存映射(mmap)
如果想要实现内存映射的话,之前我们是通过打开文件来获得文件描述符,通过磁盘文件来映射到虚拟内存当中,在这里我们不在打开文件,而是使用匿名映射,不在需要打开文件。(匿名映射的文件描述符选项设置为-1,偏移量为0,flags选项除了MAP_SHARED还要按位或上MAP_ANONYMOUS)
sem_t* sem = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS , -1, 0);
其实通过信号量解决进程之间同步以及互斥和处理线程上思路是相似的,唯一不同的地方就是信号量设置位置的不同,线程只要为全局变量即可,进程则是要通过磁盘映射来操作两个进程都可以操作的空间,来做到操控信号量。
两个进程同时输出导致的输出错误
这里我们想要使用父进程输出world,子进程输出hello,但是因为没有条件变量限制,导致两个进程同时执行,输出混杂
在使用信号量处理后,可以正常输出
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/mman.h>
void my_printf(char* data)
{
char* str = (char*)data;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
}
int main(int argc, char const *argv[])
{
//在磁盘映射区定义信号量
//MAP_ANONYMOUS匿名映射,文件描述符-1
sem_t* sem = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS , -1, 0);
//初始化条件变量
//第二变量设置为1,作用于进程
sem_init(sem, 1, 1);
//创建子线程
pid_t pid = fork();
//创建子进程
if( 0 == pid )
{
//P操作,让信号量减一
sem_wait(sem);
my_printf("hello");
//V操作,让信号量加一
sem_post(sem);
}
//创建父进程
else if( pid > 0 )
{
//P操作,让信号量减一
sem_wait(sem);
my_printf("world");
//V操作,让信号量加一
sem_post(sem);
}
//信号量的摧毁
sem_destroy(sem);
return 0;
}
【无名信号量 作用于 血缘关系进程的同步】
无名信号量作用于有血缘关系进程之间的同步,思路是和线程一样的,也是通过设置第一个开始的进程,然后再该进程末尾通过设置下一个进程信号量的值为1(通过对应信号量的V操作),让下一个进程开始,以此循环
信号量的定义方式是和线程的主要区别
//在磁盘映射区定义信号量
//MAP_ANONYMOUS匿名映射,文件描述符-1
//创建两个信号量,因为作用于进程间同步
sem_t* sem = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS , -1, 0);
sem_t* sem1 = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS , -1, 0);
这里我们通过进程之间的同步让子进程先开始运行
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/mman.h>
void my_printf(char* data)
{
char* str = (char*)data;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
}
int main(int argc, char const *argv[])
{
//在磁盘映射区定义信号量
//MAP_ANONYMOUS匿名映射,文件描述符-1
//创建两个信号量,因为作用于进程间同步
sem_t* sem = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS , -1, 0);
sem_t* sem1 = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS , -1, 0);
//初始化条件变量
//第二变量设置为1,作用于进程
sem_init(sem, 1, 0);
sem_init(sem1, 1, 1);
//创建子线程
pid_t pid = fork();
//创建子进程
if( 0 == pid )
{
//P操作,让信号量减一
sem_wait(sem1);
my_printf("hello");
//V操作,让信号量加一
sem_post(sem);
}
//创建父进程
else if( pid > 0 )
{
//P操作,让信号量减一
sem_wait(sem);
my_printf("world");
//V操作,让信号量加一
sem_post(sem1);
}
//信号量的摧毁
sem_destroy(sem);
sem_destroy(sem1);
return 0;
}
【有名信号量 作用于 非血缘关系进程的互斥】
无名信号量和有名信号量的区别之一就是,无名信号量一般作用在存在血缘关系的进程之间的,因为两个存在血缘关系的进程可以保证内存映射中,使用匿名映射可以通过同一块空间映射,但是如果两个进程独立,那么只能使用有名信号量,因为两个独立的进程无法保证映射的空间是一样的。
线程和进程之间信号量控制同步互斥其实大体的思路都是一样的,主要的问题就是解决任务与任务之间到底要怎么样才可以访问同一块空间,操控同一块空间中的信号量
1.有名信号量的创建API
有名信号量的本质类似于文件,存在与磁盘当中,然后通过磁盘映射,让两个独立的进程都可以访问这块空间
#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:有名信号的名字
oflag:读写权限,如果不存在创建(宏是和文件操作的open相同)
mode:文件操作权限,一般为0666
value:信号量的初始值
返回值
成功:信号量的地址
2.信号量的关闭
#include <semaphore.h>
int sem_close(sem_t *sem);
功能
关闭信号量
参数
信号量地址
返回值
成功:0
失败:-1
3.信号量文件删除
#include <semaphore.h>
int sem_unlink(const char *name);
功能
删除信号量文件
参数
信号量文件名
返回值
成功:0
失败:-1
4.有名信号量的P、V、销毁操作
有名信号量的P、V以及销毁操作是和无名信号量的完全相同的
5.通过有名信号量实现进程之间的互斥
这里创建了两个进程,一个负责打印hello----A,另一个打印hello----B,假如我们这里先让进程B可以先执行,观察现象会发现,B执行的时候,A是无法执行的,这就是通过信号量实现的进程之间的互斥现象
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
void my_printf(char* data)
{
char* str = (char*)data;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
}
int main(int argc, char const *argv[])
{
//创建一个有名信号量
sem_t* sem = sem_open("sem", O_CREAT | O_RDWR , 0666 , 1 );
//执行P操作
sem_wait(sem);
#ifdef A
my_printf("hello---A");
#endif // A
#ifdef B
my_printf("hello---B");
#endif // B
//执行V操作
sem_post(sem);
return 0;
}
【有名信号量 作用于 非血缘关系进程的同步】
不管是进程之间还是线程之间,只要是同步,几个任务就必须有几个信号量,因为必须以此来控制任务之间的执行顺序,任务与任务之间是互相制约的,而不是像互斥,之间没有制约,执行顺序是随机的
这里我们让可执行文件A先执行,此时即使我们先执行了B,B也会因为信号量无法成功减1而阻塞
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
void my_printf(char* data)
{
char* str = (char*)data;
while( *str != '\0' )
{
printf("%c",*str);
//刷新缓冲区到输出流
fflush(stdout);
sleep(1);
str++;
}
}
int main(int argc, char const *argv[])
{
//创建一个有名信号量
//这里先执行信号量sem1
sem_t* sem1 = sem_open("sem1", O_CREAT | O_RDWR , 0666 , 1 );
sem_t* sem2 = sem_open("sem2", O_CREAT | O_RDWR , 0666 , 0 );
#ifdef A
//执行A 的 P操作
sem_wait(sem1);
my_printf("hello---A");
//执行B 的 V操作
sem_post(sem2);
//关闭 A 的信号量
sem_close(sem1);
#endif // A
#ifdef B
//执行B 的 P操作
sem_wait(sem2);
my_printf("hello---B");
//执行下一个任务 的 V操作
//sem_post( );
//关闭 B 的信号量
sem_close(sem2);
#endif // B
return 0;
}