深入理解并发/并行,阻塞/非阻塞,同步/异步 同步与阻塞,异步与非阻塞的区别

同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。

阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。

阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果。

深入理解并发/并行,阻塞/非阻塞,同步/异步

1. 阻塞,非阻塞

首先,阻塞这个词来自操作系统的线程/进程的状态模型中,如下图:

 
进程状态

一个线程/进程经历的5个状态,创建,就绪,运行,阻塞,终止。各个状态的转换条件如上图,其中有个阻塞状态,就是说当线程中调用某个函数,需要IO请求,或者暂时得不到竞争资源的,操作系统会把该线程阻塞起来,避免浪费CPU资源,等到得到了资源,再变成就绪状态,等待CPU调度运行。

定义:
阻塞调用是指调用结果返回之前,调用者会进入阻塞状态等待。只有在得到结果之后才会返回。
非阻塞调用是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

阻塞调用:比如 socket 的 recv(),调用这个函数的线程如果没有数据返回,它会一直阻塞着,也就是 recv() 后面的代码都不会执行了,程序就停在 recv() 这里等待,所以一般把 recv() 放在单独的线程里调用。

非阻塞调用:比如非阻塞socket 的 send(),调用这个函数,它只是把待发送的数据复制到TCP输出缓冲区中,就立刻返回了,线程并不会阻塞,数据有没有发出去 send() 是不知道的,不会等待它发出去才返回的。

拓展

如果线程始终阻塞着,永远得不到资源,于是就发生了死锁
比如A线程要X,Y资源才能继续运行,B线程也要X,Y资源才能运行,但X,Y同时只能给一个线程用(即互斥条件)用的时候其他线程又不能抢夺。
A 有 X,等待 Y。
B 有 Y,等待 X。
于是A,B发生了循环等待,造成死锁。给用户的感觉就是程序卡着不动了。
在写代码的时候要特别注意共享资源的使用,用信号量控制好,避免造成死锁。死锁的解除有个著名的银行家算法

阻塞和挂起:阻塞是被动的,比如抢不到资源。挂起是主动的,线程自己调用 suspend() 把自己退出运行态了,某些时候调用 resume() 又恢复运行。

线程执行完就会被销毁,如果不想线程被频繁的创建,销毁,怎么办?可以给线程里面写个死循环,或者让线程有任务的时候执行,没任务的时候挂起,就像iOS中的 runloop 机制一样。线程就不会随便的终止了。


2. 同步,异步

定义
同步:在发出一个同步调用时,在没有得到结果之前,该调用就不返回。
异步:在发出一个异步调用后,调用者不会立刻得到结果,该调用就返回了。

同步例子

int n = func();
next();
// func() 的结果没有返回,next() 就不会执行,直到 func() 运行完。

异步例子

func(callback);
next();
...

void callback(int n) // func 结果回调 { int k = n; } // func() 执行后,还没得出结果就立即返回,然后执行 next() 了 // 等到结果出来,func() 回调 callback() 通知调用者结果。 

同步的定义看起来跟阻塞很像,但是同步跟阻塞是两个概念,同步调用的时候,线程不一定阻塞,调用虽然没返回,但它还是在运行状态中的,CPU很可能还在执行这段代码,而阻塞的话,它就肯定不在CPU中跑这个代码了。这就是同步和阻塞的区别。同步是肯定可以在,阻塞是肯定不在。

异步和非阻塞的定义比较像,两者的区别是异步是说调用的时候结果不会马上返回,线程可能被阻塞起来,也可能不阻塞,两者没关系。非阻塞是说调用的时候,线程肯定不会进入阻塞状态。

上面两组概念,就有4种组合。
同步阻塞调用:得不到结果不返回,线程进入阻塞态等待。
同步非阻塞调用:得不到结果不返回,线程不阻塞一直在CPU运行。
异步阻塞调用:去到别的线程,让别的线程阻塞起来等待结果,自己不阻塞。
异步非阻塞调用:去到别的线程,别的线程一直在运行,直到得出结果。


3. 并发,并行

先从定义说起,定义经过我通俗化了,原定义有点难理解。

并发是指一个时间段内,有几个程序都在同一个CPU上运行,但任意一个时刻点上只有一个程序在处理机上运行。

并行是指一个时间段内,有几个程序都在几个CPU上运行,任意一个时刻点上,有多个程序在同时运行,并且多道程序之间互不干扰。

两者区别如下图

 
 
 
 

并行是多个程序在多个CPU上同时运行,任意一个时刻可以有很多个程序同时运行,互不干扰。

并发是多个程序在一个CPU上运行,CPU在多个程序之间快速切换,微观上不是同时运行,任意一个时刻只有一个程序在运行,但宏观上看起来就像多个程序同时运行一样,因为CPU切换速度非常快,时间片是64ms(每64ms切换一次,不同的操作系统有不同的时间),人类的反应速度是100ms,你还没反应过来,CPU已经切换了好几个程序了。

切换耗时:线程用完了时间片,释放CPU控制权,引发系统中断,调度程序根据相关策略选取下一个线程来运行,这里需要一点耗时。

举个例子吧,并行就是,多个人,有人在扫地,有人在做饭,有人在洗衣服,扫地,做饭,洗衣服都是同时进行的。
并发就是,有一个人,这个人一会儿扫地,一会儿做饭,一会儿洗衣服,他在这3件事中来回做,同一时刻只做一件事,不是同时做的,但最后3件事都可以做完。

  • 时间片大小的选取

时间片取的小,假设是20ms,切换耗时假设是 10ms。
那么用户感觉不到多个程序之间会卡,响应很快,因为切换太快了,但是CPU的利用率就低了,20 / (20 + 10) = 66% 只有这么多,33%都浪费了。

时间片取的大,假设是200ms,切换耗时是 10ms
那么用户会觉得程序卡,响应慢,因为要200ms后才轮到我的程序运行,但是CPU利用率就高了,200 / (200 + 10) = 95% 有这么多被利用的。

所以时间片取太大或者太小都不好,一般在 10 - 100 ms 之间。

  • CPU调度策略

在并发运行中,CPU需要在多个程序之间来回切换,那么如何切换就有一些策略

3.1 先来先服务 - 时间片轮转调度
这个很简单,就是谁先来,就给谁分配时间片运行,缺点是有些紧急的任务要很久才能得到运行。

3.2 优先级调度
每个线程有一个优先级,CPU每次去拿优先级高的运行,优先级低的等等,为了避免优先级低的等太久,每等一定时间,就给优先级低的线程提高一个级别

3.3 最短作业优先
把线程任务量排序,每次拿处理时间短的线程运行,就像我去银行办业务一样,我的事情很快就处理完了,所以让我插队先办完,后面时间长的人先等等,时间长的人就很难得到响应了。

3.4 最高响应比优先
用线程的等待时间除以服务时间,得到响应比,响应比小的优先运行。这样不会造成某些任务一直得不到响应。

3.5 多级反馈队列调度
有多个优先级不同的队列,每个队列里面有多个等待线程。
CPU每次从优先级高的遍历到低的,取队首的线程运行,运行完了放回队尾,优先级越高,时间片越短,即响应越快,时间片大小就不是固定的了。
每个队列的内部还是用先来先服务的策略。

原文出处:
作者:ck2016
链接:https://www.jianshu.com/p/2116fff869b6
來源:简书

处理大并发之一 对异步非阻塞的理解

    在研究nginx和node.js的时候常会遇到异步、非阻塞等,之前自己也经常使用epoll,对其同步与阻塞,异步与非阻塞有了一定的认识,现对参考资料总结下。

    首先讨论下使用事件驱动,异步编程的优点:

    充分利用了系统资源,执行代码无须阻塞等待某种操作完成,有限的资源可以用于其他的任务。其非常适合于后端的网络服务编程。

    在服务器开发中,并发的请求处理是个大问题,阻塞式的函数会导致资源浪费和时间延迟。通过事件注册、异步函数,开发人员可以提高资源的利用率,性能也会改善。其nginx和node.js处理并发都是采用的事件驱动异步非阻塞模式。其中nginx中处理并发用的是epoll,poll,queue等方式,node.js使用的是libev,它们对大规模的HTTP请求处理的都很好。

阻塞

    《node.js开发指南》是这样定义的:线程在执行中如果遇到(I/O 操作)如磁盘读写或网络通信,通常要耗费较长的时间,这时操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为 阻塞。当 I/O 操作完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种 I/O 模式就是通常的同步式 I/O(Synchronous I/O)或阻塞式 I/O(Blocking I/O)。

非阻塞

    非阻塞是这样定义的,当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个事件。

对比阻塞与非阻塞

    阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。

    非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的 CPU 核心利用率永远是 100%,I/O 以事件的方式通知。

    在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可以让 CPU 资源不被阻塞中的线程浪费。

    而在非阻塞模式下,线程不会被 I/O 阻塞,永远在利用 CPU。多线程带来的好处仅仅是在多核 CPU 的情况下利用更多的核。

    来看看《深入浅出Node.js》对异步I/O的解释,在操作系统中,程序运行的空间分为内核空间和用户空间。我们常常提起的异步I/O,其实质是用户空间中的程序不用依赖内核空间中的I/O操作实际完成,即可进行后续任务。

    I/O的阻塞与非阻塞的解释

    阻塞模式的I/O会造成应用程序等待,直到I/O完成。同时操作系统也支持将I/O操作设置为非阻塞模式,这时应用程序的调用将可能在没有拿到真正数据时就立即返回了,为此应用程序需要多次调用才能确认I/O操作完全完成。

    I/O的同步与异步I/O的同步与异步出现在应用程序中。如果做阻塞I/O调用,应用程序等待调用的完成的过程就是一种同步状况。相反,I/O为非阻塞模式时,应用程序则是异步的。

    参照《node.js入门经典》中对同步的解释,同步的代码意味着每一次执行一个操作,在一个操作完成之前,代码的执行会被阻塞,无法移到下一个操作上。也就是说代码的执行会在函数返回前停止。直到函数返回后,代码才会继续执行。

相反,异步就意味着函数的执行无需等待某个操作的结果就可以继续执行,其操作的结果会在事件发生时由回调来处理。

异步I/O优缺点

    使用同步IO,它的优点是可以使程序调试方便,但是它的缺点也是明显的,程序的执行过程中如果入到一些耗时的IO操作,程序的执行都要等待该IO的完成,在这个等待的过程中,程序无法充分利用CPU,导致了CPU的闲置,为了充分利用CPU,和IO并行操作,常用的方法有2中:

    (1)多线程单进程

    多线程的设计之处就是为了在共享的程序空间中,实现并行处理任务,从而达到充分利用CPU的效果。

    多线程缺点:

    其一、执行时(线程切换)上下文交换的开销较大,一个线程大约需要2M的内存空间,占用资源较大。

    其二、状态同步(锁)的问题,它也使得程序的编写和调用复杂化。

    (2)单线程多进程

    为了避免多线程造成的使用不便问题,有的语言选择了单线程保持调用简单化,采用启动多进程的方式来达到充分利用CPU和提升总体的并行处理能力。它的缺点在于业务逻辑复杂时(涉及多个I/O调用),因为业务逻辑不能分布到多个进程之间,事务处理时长要远远大于多线程模式。

异步I/O与轮询技术

    当进行非阻塞I/O调用时,要读到完整的数据,应用程序需要进行多次轮询,才能确保读取数据完成,以进行下一步的操作。轮询技术的缺点在于应用程序要主动调用,会造成占用较多CPU时间片,性能较为低下。现存的轮询技术有以下这些: read、select、poll、epoll、pselect、kqueue 

    read是性能最低的一种,它通过重复调用来检查I/O的状态来完成完整数据读取。

    select是一种改进方案,通过对文件描述符上的事件状态来进行判断。

    操作系统还提供了poll、epoll等多路复用技术来提高性能。

    轮询技术满足了异步I/O确保获取完整数据的保证。但是对于应用程序而言,它仍然只能算时一种同步,因为应用程序仍然需要主动去判断I/O的状态,依旧花费了很多CPU时间来等待。上一种方法重复调用read进行轮询直到最终成功,用户程序会占用较多CPU,性能较为低下。而实际上操作系统提供了select方法来代替这种重复read轮询进行状态判断。select内部通过检查文件描述符上的事件状态来进行判断数据是否完全读取。但是对于应用程序而言它仍然只能算是一种同步,因为应用程序仍然需要主动去判断I/O的状态,依旧花费了很多CPU时间等待,select也是一种轮询。

理想的异步I/O模型

    理想的异步I/O应该是应用程序发起异步调用,而不需要进行轮询,进而处理下一个任务,只需在I/O完成后通过信号或是回调将数据传递给应用程序即可。

    暂时就整理这么多吧,感觉好多看过的东西都忘记了,回头会写一篇关于epoll使用的详细例子,该例子支持2W并发是通过的。哎,今天状态不好,写的不好,本打算自己多加点什么的,结果都是参考别人的,如有错误请大家指正,谢谢。

参考资料:

《node.js入门经典》 George Ornbo 著 傅强 陈宗赋 译 人民邮电出版社

《深入浅出node.js》

《node.js开发指南》 BYVoid 人民邮电出版社

原文出处:

晓月天城同步与阻塞,异步与非阻塞的区别https://www.cnblogs.com/-900401/p/4015048.htm

猜你喜欢

转载自www.cnblogs.com/ryelqy/p/10138010.html