操作系统中的同步和异步

操作系统中的同步和异步操作系统中同步、异步性概念首先我们从操作系统的发展中学习什么是异步性。在操作系统发展的初期阶段,CPU处理的是作业,而且是单道批处理。什么意思呢?就是一个作业从提交到结束,程序员都不能干预,此时整台计算机就为这一个作业服务(可想有多少资源被”浪费”),这样有一点好处就是整个程序是”封闭的”。这样的操作表明人和机器是没有交互的。那我们怎么实现人机交互呢?这个答案是中断。中断的引入,使得工作人员能…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新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;
}

Jetbrains全家桶1年46,售后保障稳定

由于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账号...

(0)
blank

相关推荐

  • clientwidth innerWidth offsetWidth

    clientwidth innerWidth offsetWidthclientWidth 在任何窗口系统中都是指用户内容能放置的空间clientWidth是每个document一个,一个窗口中可以放置多个document,比如你用frame或iframe就可以放置多个document,这相当于图形界面编程中的MDI(多文档窗口应用,比如Word,Excel就可以同时打开多个文档)。 window.innerWidth能获取…

  • 水仙花数

    水仙花数水仙花数

  • 色彩学基础知识

    色彩学基础知识一直觉得自己在色彩学上缺少知识,比如多种颜色如何搭配,这在绘图时常常让我纠结不已的,一直耽搁着,下面整理了一些关于色彩学的资料。1、色彩学基础知识RGB和CMKY颜色系统RGB颜色系统是一个基于三

  • web后端语言_C/C++作为web后端语言的缺点

    web后端语言_C/C++作为web后端语言的缺点C/C++C语言虽然是非常贴近操作系统的语言,能和操作系统API很好的交互,但是C语言并没有现代化工程开发所需要的面向对象功能,当然也缺乏泛型之类的功能,如果以CGI的形式开发,那么缺点非常明显,这也是第二代后端平台兴起的原因。C++具有现代化工程开发所需要的各种功能,但是它同样有缺点:缺乏字符串处理,Web开发最主要的就是字符串的处理,所有的一切几乎都要和字符串打交道,但是C…

  • FileProvider 的使用(Failed to find configured root that contains/storage/emulated/0/DCIM/ )

    FileProvider 的使用(Failed to find configured root that contains/storage/emulated/0/DCIM/ )首先扯点别的:今天不上班,在家里和剑宗喝了点酒,和同学聊了会天,也是挺开心,现在学会习。以前调用系统相机拍照的时候,流程是这样的privatevoidtakePhoto(){IntenttakePictureIntent=newIntent(MediaStore.ACTION_IMAGE_CAPTURE);if(takePictureIntent

    2022年10月29日
  • python recvfrom函数详解_recvfrom函数详解

    python recvfrom函数详解_recvfrom函数详解intret;srtuctsockaddr_infrom;ret=revcfrom(sock,recvbuf,BUFSIZErecvfrom函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。本函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。对于SOCK_STREAM类型的套接口,最多可接收缓冲区大小个数据。udp的recvfrom函数,能接收指定ip和端口发…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号