一个“制作午餐”的故事,帮助你理解并发和并行(上)

2023-11-04

导读

这是一个关于“午餐时间”的小故事,用于阐述 threading、asyncio、multiprocessing、cloud functions 等内容。为了方便阅读并理解文章的内容,全文分上、下两篇,上篇主要讲解并发,下篇重点讲解并行。

e93870ae2c48c23c7f92be6a18a533ae.jpeg

介绍

我们将会讲述一个故事,来解释 python 中并发与并行的不同之处。

在这个故事中,我们将看到一个单人进行多任务处理的场景(类似并发),以及一个多人分别处理自己任务的场景(类似并行);我们会站在餐厅的角度观察这些场景的实际效果,并观察它们如何快速有效地为顾客服务;然后我们将在 Python 中实现这些“餐厅”;最后,我们会比较这两种不同的并发选项,并解释如何择机使用它们。

解释的内容包含:

  • 并发和并行之间有什么区别?

  • 不同的并发选项以及比较它们的方式,包括 threading、asyncio、multiprocessing、cloud functions 等

  • 它们各自的优缺点

  • 使用流程图来介绍选择并发选项的思路

什么是并发和并行?

让我们从定义开始:

如果一个系统可以同时支持两个或多个正在进行中的操作,则称该系统是并发的。

如果一个系统可以支持同时执行两个或多个动作,则称该系统是并行的。

这些定义之间的关键概念和区别在于“进行中”这一短语。— 并发的艺术[1]

如果你被绕晕了,现在让我们直接通过制作午餐的故事来讲解。

在午餐时间,你拐进了一条之前从未注意到的街道。这里有两种可供选择的食物来源:一个叫做 Concurrent Burgers 的市场摊位和一个叫做 Parallel Salads 的商店。

两者看起来都很美味,但都在排长队,所以你想知道哪一个会先为你服务。

Concurrent Burgers 由一位手臂上有蟒蛇纹身的中年女士经营,她在工作时开怀大笑。她正在执行以下任务:

  • 接单

  • 翻转汉堡肉饼

  • 用沙拉、肉饼和调味品填满面包,然后完成订单

她在每个任务之间无缝切换:有一刻,她正在检查烤架上的肉饼并将煮熟的取出,下一刻她正在接受订单,再下一刻如果有任何肉饼已经准备好,她会制作一个汉堡并完成这笔订单。

Parallel Salads 配备了许多相同的人,他们在工作时面带微笑并礼貌地交谈。他们每个人都为一位顾客制作沙拉。他们接受订单,将所有原料加入一个新鲜的碗中,浇上调味汁,尽情地混合,在一个容器中装满一份健康的沙拉,然后丢掉碗。与此同时,另一个克隆人拿起脏碗并清洗它们。

两家的主要区别在于员工数量和执行任务的方式:

  • Concurrent Burgers “同时”执行多个任务,并且仅有一个工作人员在它们之间切换。

  • Parallel Salads 有多个同时进行的任务,并且有多个工人每次只负责该任务中的一部分。

你注意到:两家餐厅都以相同的速度为顾客提供服务。Concurrent Burgers 中的女士同时制作多个汉堡,并且受到她的小烤架输出熟肉饼的速度的限制。

Parallel Salads 雇用多名男子一次制作一份沙拉,并且受到将制作单份沙拉的材料放在一起所需时间长度的限制。

你很快意识到 Concurrent Burgers 受 I/O 限制,而 Parallel Salads 受 CPU 限制:

  • I/O 密集型意味着程序受 I/O 子系统的限制,在计算机术语中意味着从磁盘读取或执行网络请求。在 Concurrent Burgers 中,它指的是肉饼烹饪;

  • CPU 密集型意味着程序受 CPU 速度的限制。如果 CPU 运行得更快,程序就会运行得更快。在 Parallel Salads 中,它是制作沙拉的人的处理速度。

在一个固执己见的朋友打断你并邀请你加入他们的队列之前,你无法做出决定,你可能会在相同的状态下保持五分钟的困惑。

需要注意的是,Parallel Salads 是并发,也是并行的,因为“两个或多个操作同时进行”。并行处理是并发处理的一个子集。

这两个商店为并发和并行任务之间的区别提供了一种直观的视角。下面我们将研究如何在 Python 中实现这两者。

可供使用的选项

Python 有两个可用于并发的选项:

  • threading

  • asyncio

同时它内置了这个库以实现并行性:

  • multiprocessing

在云上运行 Python 程序时,还有另一种并行选项:

  • cloud functions

实践并发

让我们看一下使用 threading 和 asyncio 的 Concurrent Burgers 的两种可能实现。在这两种情况下,都有一个工人接单、做肉饼和做汉堡。

对于 threading 和 asyncio,都只有一个处理器在运行,但它在需要执行的不同任务之间跳转。threading 和 asyncio 之间的区别在于如何切换任务。

  • 在 threading 中,操作系统掌控不同的线程,并且会在任何时候中断它们并切换到不同的任务。程序本身无法控制这一点。这称为抢占式多任务处理,因为操作系统可以抢占您的线程以进行切换。在大多数编程语言中,线程并行运行,但在 Python 中,一次只允许执行一个。

  • 使用 asyncio,则是由程序本身决定何时在任务之间切换。每个任务通过在准备切换时,放弃对当前任务的控制,来与其他任务合作。出于这个原因,它被称为‘协作多任务“:因为当每个任务无法再取得进展时,它必须通过放弃控制来进行合作。

使用 threading 实现 Concurrent Burgers

通过 threading,工作人员可以在执行期间随时切换任务。这名工人正在下订单时突然切换到检查馅饼或制作汉堡,然后又随时切换到其他任务之一。

让我们来看一下使用 threading 实现的 Concurrent Burgers:

from concurrent.futures import ThreadPoolExecutor
import queues


# Note: Some methods and variables are skipped
#       to focus only on the threading details


def run_concurrent_burgers():
    # Create blocking queues
    customers = queue.Queue()
    orders = queue.Queue(maxsize=5)  # Process up to 5 orders at once
    cooked_patties = queue.Queue()

    # The grill is entirely independent of the worker,
    # and turns raw patties into cooked patties.
    # This is like reading from disk or doing a network request
    grill = Grill()

    # Run the three tasks using a thread pool executor
    with ThreadPoolExecutor() as executor:
        executor.submit(take_orders, customers, orders)
        executor.submit(cook_patties, grill, cooked_patties)
        executor.submit(make_burgers, orders, cooked_patties)


def take_orders(customers, orders):
    while True:
        customer = customers.get()
        order = take_order(customer)
        orders.put(order)


def cook_patties(grill, cook_patties):
    for position in range(len(grill)):
        grill[position] = raw_patties.pop()

    while True:
        for position, patty in enumerate(grill):
            if patty.cooked:
                cooked_patties.put(patty)
                grill[position] = raw_patties.pop()

        # Don't check again for another minute
        threading.sleep(60)


def make_burgers(orders, cooked_patties):
    while True:
        patty = cooked_patties.get()
        order = orders.get()
        burger = order.make_burger(patty)
        customer = order.shout_for_customer()
        customer.serve(burger)

接受订单、烹饪肉饼和制作汉堡的每一项任务都是一个无限循环,不断执行其动作。

run_concurrent_burgers 中,我们在单独的线程中启动每个任务。我们可以为每个任务手动创建一个线程,但是有一个更好的接口,称为 ThreadPoolExecutor,它为我们提交给它的每个任务创建一个线程。

当使用多个线程时,我们必须确保一次只有一个线程在读取或写入任何状态。否则我们可能会遇到两个线程拿着同一个馅饼的情况,我们最终会遇到一个相当愤怒的顾客;这个问题被称为线程安全

为了避免这个问题,我们使用 Queues 来传递状态。在单个任务中,调用 get 时 Queues 会阻塞,直到有客户、订单或小馅饼准备好。操作系统不会尝试切换到任何被阻塞的线程,这为我们提供了一种安全切换状态的简单方法。只要将状态放入 Queues 线程不再使用它,那么获取状态的线程就知道它在使用时不会改变。

threading 的优点

  • I/O 不会阻塞其他任务的进行

  • 出色的 Python 版本和库支持——如果它可以单线程运行,它很可能也可以多线程运行

threading 的缺点

  • 由于系统线程之间切换的开销,比 asyncio 慢

  • 非线程安全

  • 对于像制作沙拉这样的 CPU 密集型问题(由于 Python 只允许一个线程同时运行)没有效果 -- 一个工人同时制作多个沙拉不会比他们一个接一个地制作沙拉更快,因为每份沙拉仍然需要同样的时间来制作。

使用 asyncio 实现 Concurrent Burgers

在 asyncio 中有一个事件循环来管理所有任务。任务可以处于多种不同的状态,但最重要的两个状态是就绪或等待。在每个循环中,事件循环都会检查:是否有任何处于等待状态的任务由于另一个任务完成而准备就绪。然后它选择一个就绪任务并运行它,直到任务完成或需要等待另一个任务,这通常是一个 I/O 操作,比如从磁盘读取或发出一个 http 请求。

有两个关键字涵盖了 asyncio 的大部分用途:async 和 await。

  • async 用于标记函数必须作为单独的任务运行。

  • await 创建一个新任务并放弃对事件循环的控制。它将任务置于等待状态,并在新任务完成时再次准备就绪。

让我们来看一下使用 asyncio 实现的 Concurrent Burgers:

import asyncio

# Note: Some methods and variables are skipped
#       to focus only on the asyncio details


def run_concurrent_burgers():
    # These queues give up control
    customers = asyncio.Queue()
    orders = asyncio.Queue(maxsize=5)  # Only process up to five orders at once
    cooked_patties = asyncio.Queue()

    # The grill runs entirely independently to the worker,
    # and turn raw patties into cooked patties
    grill = Grill()

    # Run all tasks using the default asyncio event loop
    asyncio.gather(
        take_orders(customers, orders),
        cook_patties(grill, cooked_patties),
        make_burgers(orders, cooked_patties),
    )


# Declare asyncio tasks with async def
async def take_orders(customers, orders):
    while True:
        # Allow switching to another task here
        # and at all other awaits
        customer = await customers.get()
        order = take_order(customer)
        await orders.put(order)


async def cook_patties(grill, cooked_patties):
    for position in range(len(grill)):
        grill[position] = raw_patties.pop()

    while True:
        for position, patty in enumerate(grill):
            if patty.cooked:
                # put_noawait allows us to add to the queue without
                # creating a new task and giving up control
                cooked_patties.put_noawait(patty)
                grill[position] = raw_patties.pop()

        # Wait 30 seconds before checking again
        await asyncio.sleep(30)


async def make_burgers(orders, cooked_patties):
    while True:
        patty = await cooked_patties.get()
        order = await orders.get()
        burger = order.make_burger(patty)
        customer = await order.shout_for_customer()
        customer.serve(burger)

接受订单、烹饪肉饼和制作汉堡的每一项任务都是用 async def 声明的。在这些任务中,每次调用 await 时,worker 都会切换到一个新任务。会出现以下场景:

  • 接单的时候

    • 当即将与下一位客户交谈时

    • 将订单添加到订单队列时

  • 做馅饼的时候

    • 当所有的馅饼都检查完后

  • 做汉堡的时候

    • 在等待熟肉饼时

    • 等待订单时

    • 当找到顾客给他们汉堡时

最后一个难题是在 run_concurrent_burger 中,它调用 asyncio.gather 来安排所有任务由事件循环运行,在这种情况下,事件循环就是我们的工作人员。

正如我们确切地知道,任务切换时我们实际上不需要担心共享状态。我们可以只使用队列列表来实现这一点,并且知道两个任务不会意外地持有同一个馅饼。然而,强烈推荐使用 asyncio 队列,因为它们允许我们通过提供暂停当前任务的合理点来非常轻松地在任务之间进行协作。

使用 asyncio 的一个有趣方面是 async 关键字改变了函数的接口,因为它不能直接从非异步函数调用。这可以被认为是一件好事或坏事。一方面,你可以说它损害了可组合性,因为你不能混合 asyncio 和普通函数。另一方面,如果 asyncio 只用于 I/O,这会迫使 I/O 和业务逻辑分离,将 asyncio 代码限制在应用程序的边缘,并使代码库更易于理解和测试。显式标记 I/O 是类型函数式语言中相当普遍的做法 - 在 Haskell 中是必需的。

小结

Asyncio 的优点

  • 对于 I/O 密集型任务处理非常快

  • 由于只有一个系统线程,因此开销比线程少

  • 所有最快的 Web 服务器框架都在使用asyncio - 此处有一些benchmarks[2]

  • 线程安全

Asyncio 的缺点

  • 对于 CPU 密集型问题没有加速效果

  • 需要 Python 3.5+

  • 库支持适用于大多数 I/O 任务,但不如不使用 asyncio 完整

好了,这就是上篇的内容。如果大家觉得本文内容有帮助,请点赞转发支持一下。下篇将介绍并行的实践,并且总结该如何从 4 种并行和并发方案中做出选择。请持续关注哦~

参考资料

[1]

并发的艺术: https://www.oreilly.com/library/view/the-art-of/9780596802424/

[2]

benchmarks: https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=fortune&l=zijzen-1r

[3]

参考原文: https://sourcery.ai/blog/concurrency/

- EOF -

outside_default.png

加主页君微信,不仅Python技能+1

outside_default.png2635b7e2cf02b63e98187fcec4de435f.png

主页君日常还会在个人微信分享Python相关工具资源精选技术文章,不定期分享一些有意思的活动岗位内推以及如何用技术做业余项目

62e32a1a2011b0f16282ae53722657dc.jpeg

加个微信,打开一扇窗

推荐阅读  点击标题可跳转

1、一个 print 函数,挺会玩啊?

2、For-else:Python中一个奇怪但有用的特性

3、比默认的 Python shell 好太多,IPython 实用小技巧合集

觉得本文对你有帮助?请分享给更多人

推荐关注「Python开发者」,提升Python技能

点赞和在看就是最大的支持❤️

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一个“制作午餐”的故事,帮助你理解并发和并行(上) 的相关文章

  • 如何通过在 Python 3.x 上按键来启动和中断循环

    我有这段代码 当按下 P 键时会中断循环 但除非我按下非 P 键 否则循环不会工作 def main openGame while True purchase imageGrab if a sum gt 1200 fleaButton ti
  • Java 收集返回顶级项目的映射的嵌套流

    我有以下模型 class Item String name List
  • Java - 返回值是否会中断循环?

    我正在编写一些基本上遵循以下格式的代码 public static boolean isIncluded E element Node
  • Seaborn Pairplot 图例不显示颜色

    我一直在学习如何在Python中使用seaborn和pairplot 这里的一切似乎都工作正常 但由于某种原因 图例不会显示相关的颜色 我无法找到解决方案 因此如果有人有任何建议 请告诉我 x sns pairplot stats2 hue
  • Pandas 根据 diff 列形成簇

    我正在尝试使用 Pandas 根据表示时间 以秒为单位 的列中的差异来消除数据框中的一些接近重复项 例如 import pandas as pd numpy as np df pd DataFrame 1200 1201 1233 1555
  • 在 Pandas 中使用正则表达式的多种模式

    我是Python编程的初学者 我正在探索正则表达式 我正在尝试从 描述 列中提取一个单词 数据库名称 我无法给出多个正则表达式模式 请参阅下面的描述和代码 描述 Summary AD1 Low free DATA space in data
  • Espresso 和 Proguard 的 Java.lang.NoClassDefFoundError

    我对 Espresso 不太有经验 但我终于成功地运行了它 我有一个应用程序需要通过 Proguard 缩小才能处于 56K 方法之下 该应用程序以 3 秒的动画开始 因此我需要等到该动画结束才能继续 这就是我尝试用该方法做的事情waitF
  • Python 将日志滚动到变量

    我有一个使用多线程并在服务器后台运行的应用程序 为了无需登录服务器即可监控应用程序 我决定包括Bottle http bottlepy org为了响应一些HTTP端点并报告状态 执行远程关闭等 我还想添加一种查阅日志文件的方法 我可以使用以
  • 尝试使用等于“是”或“否”的字符串变量重新启动 do-while 循环

    计算行程距离的非常简单的程序 一周前刚刚开始 我有这个循环用于解决真或假问题 但我希望它适用于简单的 是 或 否 我为此分配的字符串是答案 public class Main public static void main String a
  • 如何在 Quartz 调度程序中每 25 秒运行一次?

    我正在使用 Java 的 Quartz Scheduling API 你能帮我使用 cron 表达式每 25 秒运行一次吗 这只是一个延迟 它不必总是从第 0 秒开始 例如 序列如下 0 00 0 25 0 50 1 15 1 40 2 0
  • 使用 PyTorch 分布式 NCCL 连接失败

    我正在尝试使用 torch distributed 将 PyTorch 张量从一台机器发送到另一台机器 dist init process group 函数正常工作 但是 dist broadcast 函数中出现连接失败 这是我在节点 0
  • 挂钩 Eclipse 构建过程吗?

    我希望在 Eclipse 中按下构建按钮时能够运行一个简单的 Java 程序 目前 当我单击 构建 时 它会运行一些 JRebel 日志记录代码 我有一个程序可以解析 JRebel 日志文件并将统计信息存储在数据库中 是否可以编写一个插件或
  • Java的-XX:+UseMembar参数是什么

    我在各种地方 论坛等 看到这个参数 并且常见的答案是它有助于高并发服务器 尽管如此 我还是找不到 sun 的官方文档来解释它的作用 另外 它是Java 6中添加的还是Java 5中存在的 顺便说一句 许多热点虚拟机参数的好地方是这一页 ht
  • 如何为每个屏幕添加自己的 .py 和 .kv 文件?

    我想为每个屏幕都有一个单独的 py 和 kv 文件 应通过 main py main kv 中的 ScreenManager 选择屏幕 设计应从文件 screen X kv 加载 类等应从文件 screen X py 加载 Screens
  • Ubuntu 上的 Python 2.7

    我是 Python 新手 正在 Linux 机器 Ubuntu 10 10 上工作 它正在运行 python 2 6 但我想运行 2 7 因为它有我想使用的功能 有人敦促我不要安装 2 7 并将其设置为我的默认 python 我的问题是 如
  • 限制 django 应用程序模型中的单个记录?

    我想使用模型来保存 django 应用程序的系统设置 因此 我想限制该模型 使其只能有一条记录 极限怎么办 尝试这个 class MyModel models Model onefield models CharField The fiel
  • 具有自定义值的 Django 管理外键下拉列表

    我有 3 个 Django 模型 class Test models Model pass class Page models Model test models ForeignKey Test class Question model M
  • JAXB - 列表<可序列化>?

    我使用 xjc 制作了一些课程 public class MyType XmlElementRefs XmlElementRef name MyInnerType type JAXBElement class required false
  • 启动Java项目时发生类冲突:ClassMetadataReadingVisitor将接口org.springframework.asm.ClassVisitor作为超类

    我正在使用最新的Spring框架版本 3 2 2 RELEASE 开发一个Java Web项目 但是现在项目启动时遇到了问题 详细错误是 java lang IncompleteClassChangeError 类 org springfr
  • Scrapy Spider不存储状态(持久状态)

    您好 有一个基本的蜘蛛 可以运行以获取给定域上的所有链接 我想确保它保持其状态 以便它可以从离开的位置恢复 我已按照给定的网址进行操作http doc scrapy org en latest topics jobs html http d

随机推荐

  • 国际版阿里云/腾讯云:弹性高性能计算E-HPC入门概述

    入门概述 本文介绍E HPC的运用流程 帮助您快速上手运用弹性高性能核算 下文以创立集群 在集群中安装GROMACS软件并运转水分子算例进行高性能核算为例 介绍弹性高性能核算的运用流程 帮助您快速上手运用弹性高性能核算 运用流程如下图所示
  • Jmeter —— 录制脚本

    1 第一步 添加http代理服务器 在测试计划 添加 非测试元件 http代理服务器 2 第二步 添加线程组 这个线程组是用来放录制的脚本 不添加也可以 就直接放在代理服务器下 测试计划 添加 线程 线程组 顺便讲一下线程组执行顺序 set
  • Idea快捷键(快速开发)

    Idea快捷键 快捷键 功能 Alt Enter 快速修复选择 Alt Insert 生成代码 如set get 构造方法等 Alt 切换到左侧视图 Alt 切换到右侧视图 Shift Shift 搜索文件 Ctrl D 复制当前一行 插入
  • CH8-排序

    文章目录 1 基本概念和排序方法概述 1 1 排序方法的分类 1 2 存储结构 顺序表 2 插入排序 2 1 插入排序的种类 直接插入 折半插入 希尔排序 3 交换排序 3 1 冒泡排序 3 2 快速排序 4 选择排序 4 1 直接排序 4
  • 【Spring Security】UserDetails 接口介绍

    文章目录 UserDetails 的作用 UserDetails 接口中各个方法详解 UserDetails 的作用 UserDetails 在 Spring Security 框架中主要担任获取用户信息的接口 通过该接口就能拿到用户的信息
  • Android Studio 优先源码编译的framework.jar(使用系统隐藏的api)

    引言 场景 做系统开发或者想使用隐藏的api时 通常只能使用反射的方式 缺点 需要使用的api或变量太多时不方便使用 解决办法 将需要在编译时使用的jar包参与编译 不编译到产品apk里 使app运行时调用的是系统api 步骤 每一步都必须
  • 【DirectX12】2.示例三角形绘制

    示例三角形绘制 1 效果 下面只贴出关于dx的代码 有时间再详细说明 2 标头 h pragma once include pch h include LVEDebug h include LVESystem h include
  • bootstrap点击删除按钮弹出确认框实现

  • orge工具

    tortoisehg 3 2 1 x64 msi mercurial 3 2 1 x64 msi
  • 微信支付宝大规模补贴抢占刷脸支付入口

    刷脸支付相较于二维码 优势在于去掉了手机这一介质 但介质的缺失 也意味着人脸信息的泄露变得更加容易 刷脸支付的基本原理就是将终端硬件采集到的信息与云端的存储的信息进行比对 看信息是否一致 然后解锁完成人脸支付 如果云端生物数据库发生信息泄露
  • 【华为OD统一考试A卷

    华为OD统一考试A卷 B卷 新题库说明 2023年5月份 华为官方已经将的 2022 0223Q 1 2 3 4 统一修改为OD统一考试 A卷 和OD统一考试 B卷 你收到的链接上面会标注A卷还是B卷 请注意 根据反馈 目前大部分收到的都是
  • 微信小程序设置允许转发分享onShareAppMessage(Object object)

    在需要分享的页面js文件中写 Page onShareAppMessage const promise new Promise resolve gt setTimeout gt resolve title 自定义转发标题 2000 retu
  • 基于概率论的分类方法:朴素贝叶斯

    需要分类器做出分类决策 可以使分类器给出各个类别的概率估计值 然后选择概率最高的作为其的类别 在这里使用到了概率论中的贝叶斯公式 P A B P A P B A P B 其中P A B 是后验概率 P A 是先验概率 P B A P B 为
  • Python数据分析与展示第三课

    Matplotlib是python优秀的数据可视化第三方库 数据可视化 将数据以特定的图形图像的形式展现出来 Matplotlib由各种可视化的类构成的 使用方式 import matplotlib pyplot as plt as plt
  • 论文收录引用证明常见问题汇总

    学术论文在毕业 评职称 保研 考博 留学等方面均有重要意义 因此往往需要开具检索证明 然而 开具检索证明的前提是论文必须被收录 1 什么是论文收录引用证明 论文收录引用证明是用来证明作者在科研领域中的实力和成就 当用户需要查询其论文在指定数
  • 单链表实现多项式相加

    这个小项目用C语言实现 代码中有我的注释 思路 用链表的每个节点存储表达式的每一项 因此每个链表就是一个表达式 链表节点类型的定义 struct Node DataType elem 项的系数 Variate ch 常量和变量的标志 规定如
  • rand()每次产生的随机数都一样

    写了个程序 每次产生的随机数都是一样的 在调用之前已经初始化了随机数种子 用的是当前时间 可是还是得到一样的数 for int i 0 i lt 100000 i srand unsigned time NULL cout lt
  • shiro入门

    1 概述 Apache Shiro 是一个功能强大且易于使用的 Java 安全 权限 框架 借助 Shiro 您可以快速轻松地保护任何应用程序一一从最小的移动应用程序到最大的 Web 和企业应用程序 作用 Shiro可以帮我们完成 认证 授
  • Vue中获取input输入框值

  • 一个“制作午餐”的故事,帮助你理解并发和并行(上)

    导读 这是一个关于 午餐时间 的小故事 用于阐述 threading asyncio multiprocessing cloud functions 等内容 为了方便阅读并理解文章的内容 全文分上 下两篇 上篇主要讲解并发 下篇重点讲解并行