同步编程与异步编程

0 股票
0
0
0
0

介绍

在软件开发中,选择同步编程模型还是异步编程模型不仅仅是一个技术问题;它会影响应用程序的协同工作方式、任务完成方式以及对用户输入的响应方式。请记住,选择正确的模型可以决定项目的成败,尤其是在比较这两种范式时。本文旨在通过清晰区分同步编程和异步编程,并解释它们的优势、劣势和最佳实践,来消除围绕这些概念的一些混淆。通过深入理解每种策略,开发人员可以做出明智的决策,并根据应用程序的需求调整其方法。.

理解并发编程

什么是并发编程?

在并发编程中,任务是按顺序执行的。就像读书一样,你从头开始,逐字逐句地阅读。并发编程要求每个任务必须完成才能开始下一个任务。控制流程是可预测且简单的。.

如果任务耗时过长,系统可能会卡住或无响应。阻塞行为是并发编程的一个显著特征。.

它是如何运作的?

并发编程模型以线性方式进行操作。该过程可简化如下:

  • 程序是顺序执行的。.
  • 任务按照代码顺序执行。.
  • 从上到下,每一行代码都会被执行。.

如果一项任务耗时过长,例如读取大型文件或等待用户输入,程序将阻塞,直到任务完成。这就是并发编程的阻塞机制。.

并发编程的优势应用场景

并发编程在任务必须按特定顺序执行的场景中尤其有用。例如,如果你想烤蛋糕,就不能在混合所有材料之前就把蛋糕放进烤箱。同样,在应用程序中,你可能需要先从数据库中检索数据才能对其进行处理。.

示例:顺序读取文件

以下示例展示了并发编程在读取文件时的工作原理:

function readFilesSequentially(fileList) {
for each file in fileList {
content = readFile(file) // This is a blocking operation
process(content)
}
}

在这段伪代码中,函数 读取文件(文件) 这是一个同步操作。 处理(内容) 直到 读取文件(文件) 由于文件尚未完全读取,因此不会执行。这清楚地表明了并发编程的顺序性和阻塞性。.

异步编程回顾

什么是异步编程?

异步编程是一种允许任务并发执行而非顺序执行的编程范式。这意味着程序执行无需等待一个任务完成即可继续执行下一个任务。这就像在自助餐厅一样——你无需等待别人吃完才能开始取餐。.

在异步编程中,任务通常先启动,然后暂时搁置,以便其他任务能够运行。主任务完成后,可以从中断的地方继续执行。这种非阻塞特性是异步编程的关键特征之一。.

它是如何运作的?

同时执行: 异步编程的主要优势之一是能够同时执行多个任务。这可以显著提高应用程序的效率和性能,尤其是在任务相互独立或需要等待外部资源(例如网络请求)的情况下。.

非阻塞特性: 异步编程不会阻塞程序的其他部分,因为它无需等待耗时较长的任务(例如 I/O 操作)。在用户界面编程中,这可以提升用户体验和响应速度。.

异步编程的适用场景

依赖 I/O 的任务通常采用异步编程。在 Web 开发中,可以使用异步任务来发送 API 请求、访问数据库以及处理用户输入,而无需中断主线程。.

示例:Web 开发中的 AJAX 请求(伪代码)

异步编程可用于 Web 开发中发送 AJAX 请求。请参见以下示例:

function fetchAndDisplayData(url) {
// This is a non-blocking operation
data = asyncFetch(url);
data.then((response) => {
// This code will run once the data has been fetched
displayData(response);
});
}

在上面的伪代码中,函数 asyncFetch(url) 这是一个异步操作。函数 显示数据(响应) 直到 asyncFetch(url) 该进程尚未完成数据接收,因此尚未执行。与此同时,其他代码可以在后台继续运行,这体现了异步编程的非阻塞特性。.

比较异步编程和同步编程

同步编程和异步编程在性能、程序执行和运行时间方面的区别如下:

程序执行

同时地: 在并发系统中,任务按顺序依次执行。因此,控制流程易于预测和实现。.

异步: 在异步环境中,任务可以并发执行。这意味着软件无需等待一个任务完成即可继续执行下一个任务。.

表现

同时地: 在并发执行的情况下,如果某个任务耗时过长,整个系统可能会冻结或无响应。.

异步: 异步编程的非阻塞特性可以带来更快速、更流畅的用户体验,尤其是在用户界面开发方面。.

活动场合

同时地: 适用于需要按预定顺序执行步骤的情况。.

异步: 当任务依赖于 I/O 而不是 CPU 时,它们就被认为是异步的。.

何时使用异步编程

  • 基于Web的应用程序: 为了防止主线程被中断,可以使用异步任务来执行诸如发出 API 请求之类的操作。.
  • 数据库管理: 数据读写操作可能很耗时,而且并不需要先完成这些操作才能执行其他任务。.
  • 用户界面编程: 异步编程在处理用户输入时能够提供更灵敏、更流畅的用户体验。.
  • 文件 I/O 操作: 一般来说,耗时的文件 I/O 操作不需要在进行下一步之前完成。.

事件循环和调用栈

在 JavaScript 中,要高效地使用异步代码,需要理解事件循环及其调用栈。简单来说,调用栈就是代码按顺序执行的地方。并发任务首先执行,事件循环最后才允许执行任何异步代码语句,例如: 设置超时 或者在同步处理代码后处理 API 请求。.

这就是为什么 JavaScript 虽然技术上是单线程的,但看起来却能同时执行很多操作。在这些异步操作运行的同时,事件循环确保所有数据都能及时处理,而不会阻塞主线程。.

了解事件循环和调用堆栈如何交互有助于我们编写更好的异步代码,并避免诸如 UI 冻结或交互极其缓慢等常见问题。.

使用 Web Worker 进行异步编程

另一个对异步管理任务非常有用的工具是 Web Workers 它们允许我们在后台运行 JavaScript 代码,而不会阻塞主线程,这对于性能优化和执行一些特定任务(例如复杂的计算或检索大量数据)非常有用。Web Workers 提供了真正的并发性,这意味着我们可以将繁重的任务转移到另一个线程,并保持主线程的正常运作。但是,需要注意的是,Workers 无法访问 DOM,因此更适合那些不需要直接更新 UI 的任务。.

以下是一个如何使用 Web Workers 的简单示例:

// In the main script
const worker = new Worker("./worker.js");
worker.postMessage("Start the task");
// In the worker script (worker.js)
onmessage = function (event) {
// Perform long-running task here
postMessage("Task done");
};

何时使用并发编程

  • 按顺序接收和处理数据: 对于某些应用来说,从数据库中检索数据是处理该数据的先决条件。.
  • 编写基本脚本: 在处理小型脚本时,并发编程可能更容易理解和调试。.
  • CPU依赖型任务: 执行依赖 CPU 的繁重操作。对于依赖 CPU 的任务,并发编程可能比依赖 I/O 的任务更高效。.

代码中的实际示例

并发代码示例:按顺序处理任务列表

在并发编程中,任务是按顺序处理的。以下是一个 Python 示例:

import time
def process_userData(task):
# Simulate task processing time
time.sleep(1)
print(f"Task {task} processed")
tasks = ['task1', 'task2', 'task3']
for task in tasks:
process_userData(task)

这种并发方法会按顺序执行任务。 处理用户数据 任务会被逐个处理。如果某个任务耗时过长,后续任务就必须等待,因为这种顺序处理方式会导致延迟。应用程序性能和用户体验可能会因此受到影响。.

异步代码示例:同时从多个数据源接收数据

相比之下,异步编程允许并发处理多个任务。以下是一个使用 Python 库的示例。 异步 已知:

import asyncio
async def retrieve_data(source):
# Simulate time taken to fetch data
await asyncio.sleep(1)
print(f"Data retrieved {source}")
sources = ['source1', 'source2', 'source3']
async def main():
tasks = retrieve_data(source) for source in sources]
await asyncio.gather(*tasks)
asyncio.run(main())

异步方法可以同时启动多个进程,确保应用程序能够无缝地在不同任务间切换。这样做可以提升应用程序的性能和用户体验。然而,任务和回调的管理也使得实现更加复杂。.

console.log("Start"); // First task (synchronous) - goes to call stack
setTimeout(() => {
console.log("Timeout callback"); // This task(aysnchronous) is put into the event loop
}, 1000);
console.log("End"); // Second task (synchronous) - in call stack

调用堆栈:

功能 console.log('开始') 由于它是同步操作,因此会首先执行。该函数处理完毕后会立即从调用栈中移除。.

功能 设置超时() 这是一个异步函数,所以它的回调函数是 console.log('超时回调') 它被延迟并发送到事件循环,在 1 秒(1000 毫秒)后执行,但它本身 设置超时() 它不会阻止代码执行。.

然后 console.log('结束') 因为它是在主线程上执行的并发操作,所以能够执行。.

事件循环:

在并发任务(例如)之后 console.log('开始')console.log('结束')如果执行了这些操作,事件循环会等待 1 秒的延迟,然后执行传递给异步回调函数的操作。 设置超时 流程。.

回调函数准备就绪后,事件循环会将其推入调用栈并执行。 '超时回调' 印刷。.

输出:

Start
End
Timeout callback

此示例展示了 JavaScript 如何先执行同步任务,然后在清除主调用堆栈后使用事件循环处理异步任务。.

有效使用每种编程模型的最佳实践和模式

并发编程
  • 当简洁性至关重要时,请使用: 并发编程简单易懂,因此非常适合简单的任务和脚本。.
  • 避免将其用于依赖 I/O 的任务: 并发编程在等待 I/O 操作(例如网络请求或磁盘读写)时可能会阻塞执行线程。为了避免阻塞,请使用异步编程来处理此类任务。.
异步编程
  • 用于依赖 I/O 的任务: 异步编程在处理依赖 I/O 的任务时非常有效。它允许执行线程在等待 I/O 操作完成的同时,继续执行其他任务。.
  • 注意公共资源: 如果多个任务访问和修改共享资源,异步编程可能会导致竞态条件。为了避免这个问题,请使用同步机制,例如锁或信号量。.

常见设计模式

并发编程

并发编程中最常见的模式是顺序执行模式,其中任务一个接一个地执行。.

异步编程
  • 承诺: Promise 代表一个可能尚未可用的值。它们用于处理异步操作,并提供方法来附加回调函数,这些回调函数会在值可用或发生错误时被调用。.
  • 异步/等待: 这项特性就像是在 Promise 之上添加的一层语法糖,使异步代码看起来与同步代码相似。这使得异步代码更容易编写和理解。.

如何避免常见问题

回调地狱

«回调地狱»指的是调用层层嵌套,导致代码难以阅读和理解。以下是一些避免回调地狱的方法:

  • 将代码模块化: 将你的代码拆分成更小、可重用的函数。.
  • 使用 Promise 或 Async/Await: 这些 JavaScript 特性可以简化你的代码,使其更易读、更易理解。.
  • 错误处理: 回调函数中务必包含错误处理机制。未处理的错误可能会导致不可预知的后果。.
异步编程 – 内存管理

我想分享一些关于如何在使用异步编程时有效管理内存的技巧,因为处理不当会导致内存泄漏等性能问题。.

异步编程中的内存管理

在使用异步代码时,务必注意内存的分配和清理方式。这涉及到长时间运行的任务或未完成的 Promise,如果管理不当,可能会导致内存泄漏。.

垃圾收集

在 JavaScript 中,内存由垃圾回收器管理。垃圾回收器会自动清理程序不再使用的内存。但是,在使用异步编程时,如果我们不注意,可能会占用过多的内存。例如,永远不会解析的 Promise、仍然附加的事件监听器或正在运行的定时器都可能占用大量内存。.

异步代码中内存泄漏的常见原因
  • 未兑现的承诺: 如果一个承诺从未被解决或拒绝,它可能会阻止内存被清理。.
let pendingPromise = new Promise(function (resolve, reject) {
// This promise never resolves
});
  • 事件监听器: 很容易忘记在不再需要时移除事件监听器,这会导致不必要的内存消耗。.
element.addEventListener("click", handleClick);
// Forgetting to remove the listener
// element.removeEventListener('click', handleClick);
  • 计时器: 使用 设置超时 或者 设置间隔 如果不再需要这些内存时不清除它们,可能会导致内存保留时间过长。.
var timer = setInterval(function () {
console.log("Running.");
}, 1000);
// Forgetting to clear the interval
// clearInterval(timer);
防止内存泄漏的最佳实践
  • 承诺,要么兑现,要么拒绝: 承诺必须得到解决或拒绝,以确保在不再需要时释放其内存。.
let myPromise = new Promise((resolve, reject) =>
setTimeout(() => {
resolve("Task complete");
}, 1000),
);
myPromise.then((result) => console.log(result));
  • 移除事件监听器: 事件监听器一旦附加,当不再需要它们时(例如,相应的元素已被删除或其功能不再需要时),就应将其移除。.
element.addEventListener("click", handleClick);
// Proper cleanup when no longer needed
element.removeEventListener("click", handleClick);
  • 清除计时器: 如果来自 设置超时 或者 设置间隔 如果使用它们,请记住用完后要清理干净,以免不必要的内存残留。.
var interval = setInterval(function () {
console.log('Doing something...');
}, 1000);
// Clear the interval when done
clearInterval(interval);

弱引用

另一种高级技术是使用 弱图 或者 弱集 它用于管理那些在代码中不再被引用时会被垃圾回收器自动清理的对象。这些结构允许你引用对象,而不会阻止它们被垃圾回收器清理。.

let myWeakMap = new WeakMap();
let obj = {};
myWeakMap.set(obj, "someValue");
// If obj gets dereferenced somewhere else, it will be garbage-collected.
obj = null;

结果

在结束了对同步和异步编程模型的讨论之后,很明显,它们各自具有优势,适用于不同的场景。由于同步编程是顺序执行且非阻塞的,因此易于理解,非常适合需要线性执行的任务。.

另一方面,异步编程以其非阻塞特性和同时执行多个任务的能力而著称,在需要快速响应和高性能的情况下,尤其是在处理依赖 I/O 的操作时,异步编程效果最佳。具体采用哪种方法取决于您对应用程序需求、性能问题以及所需用户体验的理解。.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

您可能也喜欢