大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全家桶1年46,售后保障稳定
操作系统中同步、异步性概念
首先我们从操作系统的发展中学习什么是异步性。在操作系统发展的初期阶段,CPU处理的是作业,而且是单道批处理。什么意思呢?就是一个作业从提交到结束,程序员都不能干预,此时整台计算机就为这一个作业服务(可想有多少资源被”浪费”),这样有一点好处就是整个程序是”封闭的”。这样的操作表明人和机器是没有交互的。那我们怎么实现人机交互呢?这个答案是中断。中断的引入,使得工作人员能在程序运行出问题的时候也能做出相应的处理。那么在当前程序中断后,计算机总不能让CPU不做事吧,所以人们引入了新的概念——进程。当A进程不能继续执行的时候(可能是因为资源不足、竞争,或是等待I/O处理),A进程会阻塞,而B进程有足够的资源,这时操作系统便把CPU分配给B进程。当然,这里还涉及到了中断处理程序。当A进程让出CPU之前,中断处理程序要做的是保护现场,即A进程的相关参数。当A进程等待的事件完成了,便可以返回中断点重新开始工作。
简单介绍发展史有助于我们更深刻的理解异步性的概念(当时我就是这样一步一步把异步性、同步概念串起来的)。进程引入后,让CPU的吞吐量得到了提升(若是单道批处理,作业等待I/O,那么这个时候CPU也要等)。但带来的问题是程序的运行失去了封闭性。异步性是指进程以不可预知的速度向前推进。在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系(一般是资源问题)。内存中的每个进程何时执行,何时暂停,以怎样的速度向前推进,程序总共需要多少时间才能完成等,都是不可预知的。例如,当正在执行的进程提出某种资源请求时,如打印机请求,而此时打印机正在为其他的进程打印。由于打印机是临界资源,因此正在执行的进程必须等待,并且要放弃处理机。直到打印机空闲,并再次把处理机分配给该进程时,该进程才能继续执行。由于资源等因素的限制,进程的执行通常都不是 一气呵成,而是以 停停走走 的方式运行。
试想以下两个简单的小程序是两个进程,其中i是公共资源。
#include<stdio.h>//程序A
int i = 1;
int main()
{
i = i + 10;
//中间包含若干与i无关的操作
printf("Ai = %d", i);
return 0;
}
#include<stdio.h>//程序B
int i = 1;
int main()
{
i++;
//中间包含若干与i无关的操作
printf("Bi = %d", i);
return 0;
}
由于A和B是并发执行的,并且推进速度是不可预知的,所以最终的结果有多种情况。以下只分析两种①:Ai = 11 Bi = 12 即:A先运行i = i + 10;并打印出来,B再运行i++。②:Ai = 12 Bi = 12 即:A先运行 i = i + 10此时并没有打印,而B运行了i++后,A和B分别将i的值打印出来。其他情况也可以这样分析。因为异步性的关系,我们得到的答案可能是错误的,或是我们不想要的。为了解决这个问题,就必须引入同步机制,使得程序能按照规则运行下去,从而得到我们想要的答案。
创建父子进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid =fork();
if( pid == 0 ) //子进程返回值为0
{
while(1)
{
printf("This is child\n");
sleep(1);
}
}
else
{
while(1)
{
printf("This is parent\n");
sleep(1);
}
}
return 0;
}
可以看到,父子进程之间打印的信息并没有固定的先后顺序。当父子进程同时去访问资源时,也不能确定获取资源的先后顺序。这就表明进程的异步性可能出现我们不想要的结果,或者说是错误的结果。解决上述问题的方法就是”同步”。
同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。例如:上面两个程序可以看成5+3*5中的加法程序和乘法程序。若先执行乘法程序再执行加法程序,则5+3*5=20。这个答案一定是对的吗?其实不然。如果我们想要的答案是40,就要先执行加法程序再执行乘法程序,(5+3)*5=40。我们通过加 () 改变了运算的先后顺序,使先乘除后加减变成了先加减后乘除,这就是一种同步机制。
当我们理解了异步、同步的概念后,可以简单了解一下互斥的概念。互斥亦称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时,系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。为禁止两个进程同时进入临界区,同步机制应遵循以下准则:
空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
有限等待:对请求访问的进程,应保证能在有限时间内进入临界区。
让权等待:当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。
线程同步例子(使用互斥锁):
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
/*全局变量*/
int sum = 0;
/*互斥量 */
pthread_mutex_t mutex;
/*声明线程运行服务程序*/
void* pthread_function1 (void*);
void* pthread_function2 (void*);
int main (void)
{
/*线程的标识符*/
pthread_t pt_1 = 0;
pthread_t pt_2 = 0;
int ret = 0;
/*互斥初始化*/
pthread_mutex_init (&mutex, NULL);
/*分别创建线程1、2*/
ret = pthread_create( &pt_1, //线程标识符指针
NULL, //默认属性
pthread_function1, //运行函数
NULL); //无参数
if (ret != 0)
{
perror ("pthread_1_create");
}
ret = pthread_create( &pt_2, //线程标识符指针
NULL, //默认属性
pthread_function2, //运行函数
NULL); //无参数
if (ret != 0)
{
perror ("pthread_2_create");
}
/*等待线程1、2的结束*/
pthread_join (pt_1, NULL);
pthread_join (pt_2, NULL);
printf ("main programme exit!\n");
return 0;
}
/*线程1的服务程序*/
void* pthread_function1 (void*a)
{
int i = 0;
printf ("This is pthread_1!\n");
for( i=0; i<3; i++ )
{
pthread_mutex_lock(&mutex); /*获取互斥锁*/
/*临界资源*/
sum++;
printf ("Thread_1 add one to num:%d\n",sum);
pthread_mutex_unlock(&mutex); /*释放互斥锁*/
/*注意,这里以防线程的抢占,以造成一个线程在另一个线程sleep时多次访问互斥资源,所以sleep要在得到互斥锁后调用*/
sleep (1);
}
pthread_exit ( NULL );
}
/*线程2的服务程序*/
void* pthread_function2 (void*a)
{
int i = 0;
printf ("This is pthread_2!\n");
for( i=0; i<5; i++ )
{
pthread_mutex_lock(&mutex); /*获取互斥锁*/
/*临界资源*/
sum++;
printf ("Thread_2 add one to num:%d\n",sum);
pthread_mutex_unlock(&mutex); /*释放互斥锁*/
/*注意,这里以防线程的抢占,以造成一个线程在另一个线程sleep时多次访问互斥资源,所以sleep要在得到互斥锁后调用*/
sleep (1);
}
pthread_exit ( NULL );
}
Linux下编译时需要加 -lpthread
注意第一个字母是大写,windows C语言中单位是毫秒(ms)。
Sleep (500);
就是到这里停半秒,然后继续向下执行。
包含在#include <windows.h>头文件
在Linux C语言中 sleep的单位是秒(s)
sleep(5);//停5秒
包含在 #include <unistd.h>头文件
由于程序先创建的是 Thread_1,所以 Thread_1 先加锁,即拥有使用公共资源的权限。Thread_1 在加锁后休眠2秒,此时 Thread_2 被阻塞。若不加锁,Thread_2 能直接对公共资源进行操作。当 Thread_1 的工作完成,它释放互斥锁资源,之后运行 Thread_2。同理,当 Thread_2 运行时,Thread_1 被阻塞,直至 Thread_2 完成工作并释放互斥锁资源。
经典进程同步问题:生产者-消费者问题
(1) 描述:一组生产者进程和一组消费者进程共享一个初始为空、大小为 n 的缓冲区,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。
(2) 分析:
① 关系分析。生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,他们也是同步关系。
② 整理思路。这里比较简单,只有生产者和消费者两个进程,正好是这两个进程存在着互斥关系和同步关系。那么需要解决的是互斥和同步 PV 操作的位置。
③ 信号量设置。信号量 mutex 作为互斥信号量,它用于控制互斥访问缓冲池,互斥信号量初值为1;信号量 full 用于记录当前缓冲池中
“满”缓冲区数,初值为0。信号量 empty 用于记录当前缓冲池中”空”缓冲区数,初值为 n。生产者-消费者进程的伪代码如下:
semaphore mutex=1; //临界区互斥信号量
semaphore empty=n; //空闲缓冲区
semaphore full=0; //缓冲区初始化为空
producer () { //生产者进程
while(1){
produce an item in nextp; //生产数据
P(empty); //获取空缓冲区单元
P(mutex); //进入临界区.
add nextp to buffer; //将数据放入缓冲区
V(mutex); //离开临界区,释放互斥信号量
V(full); //满缓冲区数加1
}
}
consumer () { //消费者进程
while(1){
P(full); //获取满缓冲区单元
P(mutex); // 进入临界区
remove an item from buffer; //从缓冲区中取出数据
V (mutex); //离开临界区,释放互斥信号量
V (empty) ; //空缓冲区数加1
consume the item; //消费数据
}
}
该类问题要注意对缓冲区大小为 n 的处理,当缓冲区中有空时便可对 empty 变量执行 P 操作,一旦取走一个产品便要执行 V 操作以释放空闲区。对 empty 和 full 变量的 P 操作必须放在对 mutex 的P操作之前。如果生产者进程先执行 P(mutex),然后执行 P(empty),消费者执行 P(mutex),然后执行P(fall),这样可不可以?答案是否定的。设想生产者进程已经将缓冲区放满,消费者进程并没有取产品,即empty = 0,当下次仍然是生产者进程运行时,它先执行 P(mutex) 封锁信号量,再执行 P(empty) 时将被阻塞,希望消费者取出产品后将其唤醒。轮到消费者进程运行时,它先执行 P(mutex),然而由于生产者进程已经封锁 mutex 信号量,消费者进程也会被阻塞,这样一来生产者、消费者进程都将阻塞,都指望对方唤醒自己,陷入了无休止的等待。同理,如果消费者进程已经将缓冲区取空,即 full = 0,下次如果还是消费者先运行,也会出现类似的死锁。不过生产者释放信号量时,mutex、full 先释放哪一个无所谓,消费者先释放mutex 还是 empty 都可以。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/219610.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...