深入理解并发/并行,阻塞/非阻塞,同步/异步
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发生了循环等待,造成死锁。给用户的感觉就是程序卡着不动了。
在写代码的时候要特别注意共享资源的使用,用信号量控制好,避免造成死锁。死锁的解除有个著名的银行家算法(https://baike.baidu.com/item/%E9%93%B6%E8%A1%8C%E5%AE%B6%E7%AE%97%E6%B3%95)
阻塞和挂起:阻塞是被动的,比如抢不到资源。挂起是主动的,线程自己调用 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运行。
异步阻塞调用:去到别的线程,让别的线程阻塞起来等待结果,自己不阻塞。