您的位置:首页 > 科技 > 能源 > 【编程概念】同步、异步、阻塞、非阻塞

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

2024/12/26 18:48:47 来源:https://blog.csdn.net/weixin_43233774/article/details/140486788  浏览:    关键词:【编程概念】同步、异步、阻塞、非阻塞

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

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

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

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

下面用具体的程序来表示一下。
假设有一个工作,用 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) => { // 原 callbackdo_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 机制,可以参考这篇文章

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com