摘要:对同步与异步,阻塞和非阻塞,挂起和阻塞的记录

声明:后面会不断穿插这样的一些概念,一定要深入理解一些关键的基本思想。这些基本概念很多的参考资料参差不齐,讲解不是很清楚,本章将详细,用最通俗易懂的语言解释,什么是线程、进程、同步、异步、阻塞、非阻塞、并发、并行这些很容易弄混的概念,本次的系列文章较长,后续会讲解python协程的实现方式。看完本文,你讲明白一下一些基本的东西:

(1)并发(并发只是实现异步的手段之一)并不是没有阻塞的,依然有阻塞,相对的分析,并发依然有阻塞。

(2)怎么理解“事件循环”,那个线程一直在各个方法之间永不停歇的游走,遇到一个yield from 就悬挂起来,然后又走到另外一个方法,依次进行下去,知道事件循环所有的方法执行完毕。

(3)并发(异步)一定会比同步快吗?当然不是了,参见后面文章的实验。

(4)并发分为真并发、伪并发,并发与并行的区别在于“是否同时”

(5)异步是最终的目的,并发和并行都可以实现异步,线程是决定了是使用并发还是并行的手段。

(6)最好的实现方式当然是并行了,

首先介绍一些最基本的概念核心思想

一、进程、线程

1.1进程(Process):

是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。是应用程序的一个运行例程,是应用程序的一次动态执行过程。

1.2线程(Thread):

是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。

线程的本质:线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。

1.3进程和线程的区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

2) 线程的划分尺度小于进程,使得多线程程序的并发性高。

3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。操作系统将cpu时间片分给进程,进程再分配给所有的线程。进程——资源分配的最小单位,线程——程序执行的最小单位。

1.4不同平台下的进程和线程

windwos下:

windows下面既有进程也有线程。一个正在运行的应用程序在操作系统中被视为一个进程,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。

linux下:

linux下不存在线程只有进程。一个正在运行的应用程序在操作系统中被视为一个进程,进程通过fork生成一个子进程。

二、 同步(Sync)和异步(Async)

2.1同步:

所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。

简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。

2.2异步:

异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

对于通知调用者的三种方式,具体如下:

状态:即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。

通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。

回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。

2.3同步和异步的区别

总结来说,同步和异步的区别:请求发出后,是否需要等待结果,才能继续执行其他操作。

三、阻塞和非阻塞

阻塞和非阻塞这两个概念仅仅与等待消息通知时的状态有关。跟同步、异步没什么太大关系,也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

总结:同步执行一般都会有阻塞,但也有可能没阻塞;异步执行也有可能有阻塞,也可能没有阻塞。后面会讲到。

四、并发并行

并发

在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态。这种方式我们称之为并发(Concurrent)

并行

当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)

并发和并行的区别

(1)你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。因为在完成吃饭这件事情之前,打电话这件事你是完全没开始的,是一个一个来的)

(2)你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。因为吃饭和电话两件事情都处于启动状态,而不是一件事做完才启动另一件事,但是虽然几件事情都开始了,但因为是一个线程,还是一个一个交替去做的,这也是python协程的思想。

(3)你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。因为这是同时在进行多件事情,而不是交替执行。

怎么区别呢?区分它们最关键的点就是:是否是『同时』

并发的关键是你有处理多个任务的能力,不一定要同时;但是并行的关键是你有同时处理多个任务的能力。

五、关键概念的区分

(1)阻塞/非阻塞:关注的是程序在等待调用结果(消息,返回值)时的状态

(2)同步/异步:关注的是消息通知的机制。即等到你做完通知我,我再开始做,还是你先做你的,我先做我的 ,你做完了再来通知我就可以了。

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

上面的两组概念是可以两两搭配的,即

(3)同步阻塞、同步非阻塞,异步阻塞、异步非阻塞。

举个简单的例子来描述这四种情况,老张要做两件事,用水壶烧开水,看电视,两件事情即两个任务,两个函数。

同步阻塞:老张把水壶放到火上,就坐在那里等水开,开了之后我再去看电视。(同步阻塞)

同步非阻塞:老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)

老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

异步阻塞:老张把响水壶放到火上,然后就坐在旁边等着听那个烧开的提示音。(异步阻塞)

异步非阻塞:老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)乍一看,这“同步阻塞、异步阻塞”似乎没有什么区别,但实际上是有区别的,所谓同步异步,指的是消息通知的机制。区别在哪里呢?

在这个例子中同步异步只是对于水壶而言。在使用普通水壶的时候,我要自己主动去观察水是不是烧开了,自己主动去获取烧开的这个结果,即所谓的同步;但是在响水壶的时候,我不需要再管水烧到什么程度了,因为只要水烧开了,那个滴滴的噪声就会通知我的,即所谓的异步。

他们的相同点是,在烧水的过程中,老王啥也没干,即“阻塞”。

(4)四种总结——同步/异步与阻塞/非阻塞

同步阻塞形式:效率是最低的。拿上面的例子来说,在烧水的过程中,什么别的事都不做。

同步非阻塞形式:实际上是效率低下的。因为老王需要不断的在看电视与烧水之间来回跑动,看一下电视,又要去看一下水烧开没有,这样来回跑很多次,在程序中,程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。

异步阻塞形式:异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞。 这个效率其实跟同步阻塞差不多的。

异步非阻塞形式:效率更高。因为老王把水烧好之后就不用管了,可以安安心心去看电视,不用来回奔波看水烧开了没,因为水烧开了会有提示告诉他水烧好了,这样效率岂不是更高。

那有没有更好的办法?当然有,如果老王还有一个帮手老张,让老王自己看电视、同时老张去烧开水,这样岂不是更好?这就是所谓的并行。(5)并发/并行、同步/异步、阻塞/非阻塞

并发/并行:即能够开启多个任务,多个任务交替执行为并发,多个任务同时执行为并行

同步/异步:关注的是消息通知的机制,主动等候消息则为同步、被动听消息则为异步

阻塞/非阻塞:关注的是等候消息的过程中有没有干其他事。

总结:上面的几组概念,时刻穿插的,并没有完全的等价关系,所以经常有人说,异步就是非阻塞,同步就是阻塞,并发就是非阻塞、并行就是非阻塞,这些说法都是不完全准确地。

六、最终结论概括

并发和并行都是实现异步编程的思路,只有一个线程的并发,称之为“伪并发”;有多个线程的并发称之为“真并发”,真并发与并行是很接近的。

6.1异步操作的优缺点

因为异步操作无须额外的线程负担(这里指的是单线程交替执行的“伪并发”),并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些 初入,而且难以调试。

6.2 多线程的优缺点

多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。

异步与多线程,从辩证关系上来看,异步和多线程并不时一个同等关系,(因为单线程也是可以实现异步的)异步是目的,多线程只是我们实现异步的一个手段.什么是异步:异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回.实现异步可以采用多线程技术或则交给另外的进程来处理

写了两个demo,第一个 demo1阻塞.exe 是阻塞的,第二个 demo2非阻塞.exe 是非阻塞的。

当运行 demo1阻塞.exe 的时候,点击 开始 按钮 执行睡眠20秒,这个时候 demo1阻塞 是被阻塞的,所以这个时候再点击 结束 按钮,未响应

当运行 demo1非阻塞.exe 的时候,点击 开始 按钮 执行睡眠20秒,这个时候 demo1非阻塞 是不会被阻塞的,所以这个时候再点击 结束 按钮, 弹出弹框 你好

七:阻塞和挂起区别

阻塞和挂起的定义

同步与异步,阻塞和非阻塞1.png

阻塞:正在执行的进程由于发生某时间(如I/O请求、申请缓冲区失败等)暂时无法继续执行。此时引起进程调度,OS把处理机分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种状态称为阻塞状态。

挂起:由于系统和用户的需要引入了挂起的操作,进程被挂起意味着该进程处于静止状态。如果进程正在执行,它将暂停执行,若原本处于就绪状态,则该进程此时暂不接受调度。

共同点:

  1. 进程都暂停执行
  2. 进程都释放CPU,即两个过程都会涉及上下文切换

不同点:

  1. 对系统资源占用不同:虽然都释放了CPU,但阻塞的进程仍处于内存中,而挂起的进程通过“对换”技术被换出到外存(磁盘)中。
  2. 发生时机不同:阻塞一般在进程等待资源(IO资源、信号量等)时发生;而挂起是由于用户和系统的需要,例如,终端用户需要暂停程序研究其执行情况或对其进行修改、OS为了提高内存利用率需要将暂时不能运行的进程(处于就绪或阻塞队列的进程)调出到磁盘
  3. 恢复时机不同:阻塞要在等待的资源得到满足(例如获得了锁)后,才会进入就绪状态,等待被调度而执行;被挂起的进程由将其挂起的对象(如用户、系统)在时机符合时(调试结束、被调度进程选中需要重新执行)将其主动激活。
总结一下:阻塞是进程不得不阻塞,I/O请求需要时间准备吧。而挂起可能是因为电脑的内存不足,就找几个处于阻塞态的程序挂起弄到外存(这里的外存指的是是磁盘缓存,相当于linux下的swap文件)里。

进程因为请求I/O之类的处于阻塞状态,然后操作系统有可能将其挂起。

我在 计算机操作系统第四版(汤小丹) 的 85页 找到了一些内容:

处理机调度 有 高级调度 和 低级调度 还有中级调度。 中级调度又称为内存调度。引入中级调度的主要目的是,提高内存利用率和系统吞吐量。为此,应把那些暂时不能运行的进程,调至外存等待,此时进程的状态称为就绪驻外内存状态(或挂起状态)。

阻塞和挂起的对象

阻塞的对象:进程和线程应该都可以处于阻塞状态。为什么这么说,因为linux下都是进程,父进程可以被阻塞,子进程也可以被阻塞。windows下干活的都是线程,一个进程会主动创建main线程,然后在main线程里面执行代码。

挂起的对象:进程应该可以被挂起,线程不能被挂起。前面说过进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位,所以线程只能操作进程的资源,这么说应该就只有进程能挂起。确切的说 windows只有进程可以挂起,而linux 子进程和父进程应该都能被挂起。

我记得 在windwos环境下,线程也分为内核级线程和用户级线程。当使用用户级线程的时候,一个线程阻塞会导致一个进程阻塞。 当使用内核级线程的时候,一个线程阻塞,windows会调用该线程所属进程下面的其他线程。

八、c语言中的sleep函数

当进程或者线程调用sleep函数的时候,进程或者线程处于阻塞状态。如果电脑内存不够,该进程或者线程就会被挂起。

九、参考

OS中阻塞与挂起的区别&sleep的实现原理
python协程系列(四)——同步/异步、并发/并行、线程/进程