【编程概念】同步、异步、阻塞、非阻塞

这四个概念是非常容易把人绕蒙的。目前我的工作环境中,已经很少有用阻塞、非阻塞的概念了。经常说的是:这是一个同步接口,这是一个异步接口。

为了说清楚这四个概念,我们先建立一个模型。甲方(调用方)、乙方(工作方)和任务(工作)本身。
甲方需要执行一个任务,他可以自己执行(同步),也可以布置任务给别人(异步)。在程序中,只有乙方对应的概念可能是一个事件循环或者另一个线程,这个由调度器决定。如果这个任务相对来说非常耗时,那么我们可以形容这个任务是阻塞执行或者非阻塞执行。阻塞是说:这个任务耗时也会完成。非阻塞是说,这个任务如果完不成我就立刻返回,并告诉执行者。

在这个模型之下,甲方 和 任务 的对应到程序中的实体都是一个函数。这是造成迷惑的根本原因。

总的来说:
阻塞与非阻塞,是用来描述工作是否特别耗时的问题。
同步与异步,是用来描述甲方如何安排任务执行的。

下面用具体的程序来表示一下。
假设有一个工作,用 read() 来表示

read();

另外有一个需要执行该任务被执行的甲方,用 process 来表示

看,甲方的外在表现形式是函数吧。
为了说清楚同步异步的概念,我们假设它执行在线程A。

void process() { // 执行在线程 A
	// 需要 read 被执行
}

同步意味着,工作甲方会自己做,不再外包了。所以 read 仍然会执行在线程 A ,所以 processtask 是一个顺序的执行流。

  • process 开始 -> task 开始 -> task 结束 -> process 结束
void process() {
	task();
}

异步意味着,该工作我不会立刻做,可能会让别的 线程(乙方) 做,也可能我后续会做(事件循环),这由调度器来决定,这代表 process 和 task 是未知的执行流。我们能知道的是 task 只会在 postTask 之后才会执行。

  • process 开始 -> postTask -> porcess 结束 -> 2. 事件循环调度到 task
    1. 线程 B 立即执行 task
void process_async() {
	schedule.addTask(read);
}

我们可以说使用 scheduleprocess 实现,是一个 异步接口。我相信到现在为止,对异步接口的解释仍然不尽如人意,因为还有回调的问题没有解决。后面会继续深入。

现在,我们加上 阻塞和异步,对这四个概念做一个简单总结。

只有 任务 比较慢时,才会用 阻塞与非阻塞 来形容这个任务。一般是需要 io 甚至需要等很久。

  • 如果 task 说我一定会完成,完不成我不回来,那么它是阻塞的。
  • 如果 task 我不一定会完成,如果我无法完成,我会返回来告诉你,不会让你等很久的。那么它是非阻塞的。

注意:如果是阻塞接口,那么它一定会阻塞某个线程的执行,这无法避免。

总结表格如下:

-阻塞非阻塞
同步当前线程等很久任务可能没完成
异步某一个线程会等很久未来任务可能没完成

我们继续讨论异步接口的回调问题。

如果考虑到回调函数,process 会变成这样

void process_async(callback) {
	schedule.addTask(
		[]() {
			res = read();
			callback(res);
		}
	);
}

process_async([](res) {
	do_something(res);
});

这是一个非常裸的异步框架,也能说明很多问题。比如说: schedule 会把 任务安排在任何一个线程,所以潜在的 callback 应该也是运行在别的线程的,这就有潜在的线程安全问题了。

每一个高级的异步编程模型,都可以用这个裸的模型来解释。所以在进行异步编程时,可以用这个裸的模型来分析潜在的问题。

我们对应 javascript 中的异步编程模型,看看怎么用这个裸的模型来解释。

第一步,使用 promise,优雅的将任务函数和回调函数合二为一,更重要的是,回调函数的相对位置在异步函数之后,这为顺序写异步代码提供了基础。

new Promise(
	(resolve, reject) => { // 原 process_async 函数 
		x = read();
		resolve(x);
	}
).then((res) => { // 原 callback
	do_something(res);
})

你会说,这也没有 schedule 呀?确实没有。事实上如果 read() 是阻塞调用的话那可以说这个异步,异步了个寂寞,还是会阻塞。前面说过,阻塞任务必然会阻塞某一个线程的。由于 js 是单线程模型,那阻塞的当然是本线程啦。

可以观察一下产品中的代码,Promise 中的 resolve 调用一定实在内部函数中的,例如经典的: resolve 函数就放在 setTimeout 中,这是运行时提供的能力。运行时的事件循环提供了300ms 之后再执行 resolve 的能力。如果没有事件循环提供异步能力,可以说 javascript 的 Promise 基本就是个摆设。

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 300);
});

myPromise.then(handleFulfilledA, handleRejectedA)
  .then(handleFulfilledB, handleRejectedB)

javascript 与非阻塞调用是密不可分的。永远不可以在 javascript中使用阻塞接口。因此 node 底层是 libuv 库,提供了事件循环和非阻塞+io 多路复用的 io 模型。

此外,do_somthing 和 handleFulfilledA 依然可以是异步的,那便是 javascript 的链式调用了。
第二步,使用 async / await 顺序写异步代码

async main() {
		let res = await new Promise((resolve, reject) => {
					 setTimeout(() => {
					    resolve("foo");
					  }, 300);
					});
		res = handleFulfiledA(res); // handleRejectedB 可以套异常来实现
		res = handleFulfiledB(res);
}

总的来说,javascript 使用 promise + async/await 语法(糖?) + libuv 运行时的事件循环提供的非阻塞io接口,实现了比较优雅的异步编程模型

如果想了解更多 javascript 的 generator、async、promise 机制,可以参考这篇文章

  • 15
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值