第五章

并行:并发异步与函数式

第八层 + 第九层 · 并发/异步/函数式思想 秒杀上线,库存被抢成了负数

一、秒杀第一夜:库存变成了 -3

云间书店的小程序上线了秒杀活动:《三体》限量 100 本,20:00 开抢。

20:00:00,服务器日志里 1000 个请求同时涌进来。三分钟后,小陈盯着数据库,手都在抖:

《三体》 库存:-3

"负库存?卖了 103 本?"小陈崩溃了,"我们明明写了 if n > stock: raise 啊!"

老周很平静,打开代码:"来,我们演一遍。假设库存只剩 1 本,同时来了两个请求——线程 A 和线程 B。看仔细:"

# 两个线程同时执行这段"扣库存"
def sell(stock, n):        # stock 是共享数据
    if n > stock:          # ① 线程A检查:1 > 1 吗?不,够卖
        raise ...
    stock -= n             # ② 线程A执行到一半,线程B也开始检查:stock 还是 1!够卖!
                           # ③ 线程A:stock = 0
                           # ④ 线程B:stock = -1   ← 两个人各买走了最后一本

"看明白了吗?问题不在检查逻辑,在于检查(①)和扣减(②)之间不是原子的——A 检查完还没扣,B 就看到旧的库存。这叫竞态条件(race condition)多个线程同时读写共享数据,就是一场车祸现场。"


二、第八层:并发与异步

2.1 多线程:CPU 多核,真正并行

"先说清楚什么是多线程(multithreading)。"老周说,"以前一个程序是一个'人'在干活。多线程就是程序里同时有好几个'人'(线程),各干各的。现代 CPU 有 8 核 16 核,真正的并行就是每个核上跑一个线程,同一时刻真的在同时计算。"

"但多个线程同时摸同一份数据(比如 stock),就会撞车——就像两个人同时抢最后一个包子。"

2.2 锁 / 互斥 / 通道:让共享数据安全

"撞车了怎么办?第一种办法:锁(lock)。谁要动共享数据,先拿锁,用完再还——拿锁期间,别人只能排队等着。"

import threading

lock = threading.Lock()     # 一把全局锁
stock = 100                 # 共享库存

def sell(n):
    global stock
    with lock:              # 拿锁:下面的代码一次只允许一个线程进来
        if n > stock:
            raise Exception("库存不足")
        stock -= n
        # 离开 with 自动还锁

"with lock: 包起来的区域叫临界区(critical section),同一时刻只有一个线程能进。检查+扣减现在是一个整体,谁也插不进来。"

"锁的问题:死锁(deadlock)——A 拿了锁 1 等锁 2,B 拿了锁 2 等锁 1,两个人都卡死等对方。所以还有另一种哲学:别共享,用通道(channel)传消息——把数据交给一个专门的'管家线程',其他人只给它发消息,让管家一个人改数据:"

// Go 语言:channel —— 线程之间不共享数据,只传递消息
func main() {
    stock := make(chan int, 1)      // 库存的"保险箱",容量 1
    stock <- 100                    // 初始 100

    go func() {                     // 卖家协程:从通道读当前库存
        s := <-stock
        stock <- s - 1              // 扣 1 再放回去 —— 同一时刻只有一个人在动它
    }()
    // ……多个卖家协程都走"读-扣-放回"这一个口子,天然串行,不会超卖
}

"消息传递,避免共享状态——这是更温和的并发哲学。"

2.3 异步 / async / await:等 IO 时不阻塞

"秒杀解决了,但还有个大头:慢操作。"老周说,"结算的时候,系统要调支付接口——一次网络请求要 500ms。如果收银线程傻等着这 500ms,这个线程就废了,期间它什么也干不了。"

"异步(asynchronous)解决的就是:等 IO(网络、磁盘、数据库)的时候,不傻等,先把线程让出来干别的,等结果回来了再继续。"

import asyncio

async def checkout(order):
    await pay_api(order)          # 发起支付请求,不阻塞 —— 线程继续去接待别的顾客
    await notify_customer(order)  # 支付回来后,再通知顾客
    print("订单完成")

# 事件循环:同一线程里"转圈"接待所有协程
asyncio.run(checkout(order))

"await 的意思:'我先挂起,你去忙别的,我好了叫我。' 一个线程可以同时'挂'着成百上千个这样的等待,谁先好谁先醒——这叫事件循环(event loop)。协程(coroutine)就是这些能被挂起和恢复的小函数。"

"注意区分:多线程是'人多',异步是'一个人高效地来回切换'。IO 密集的任务用异步,CPU 密集的任务才需要多线程。"

2.4 Actor 模型:Erlang 的极致

"最后提一嘴Actor 模型——Erlang 语言用它扛起了整个电信行业。"老周说,"它的哲学比通道更彻底:每个 Actor 都是一个独立的小世界,有自己的状态,外界只能给它发消息,它自己处理、自己回信。没有共享内存,所以没有锁、没有竞态。 聊天软件、游戏服务器这种海量并发场景常用它。"

# Elixir(Erlang 系):每个"库存管理员"都是一个 Actor
defmodule StockActor do
  def loop(stock) do
    receive do
      {:sell, n, from} ->
        new_stock = stock - n
        send(from, {:ok, new_stock})
        loop(new_stock)          # 继续处理下一条消息
    end
  end
end

"总结第八层:多线程=人多;锁/通道=防止人抢数据;异步=一个人高效切换;Actor=每个人都待在自己的隔间里,靠传纸条交流。"


三、第九层:函数式思想

秒杀平稳度过后,王姐又来了:"周哥,我要看一份报表:统计这个月所有订单——总金额、客单价、最畅销的 5 本书、以及'买了 3 本以上'的回头客名单。"

小陈二话不说开始写 for 循环,写了 60 行,绕得自己都头晕。老周拦住他:"停。把报表的需求翻译成人话:'从订单里,筛出符合条件的人,映射成我要的信息,最后归约成总数'。这三件事——筛选、映射、归约——正是函数式思想的三大件。"

3.1 一等函数与高阶函数:map / filter / reduce

"函数式思想的第一块基石:函数是一等公民(first-class function)——函数可以像数字一样被传来传去:当参数、当返回值、存进变量。"

# 函数能当参数!
def apply_twice(f, x):
    return f(f(x))

def add_one(n):
    return n + 1

print(apply_twice(add_one, 5))    # 7 —— 把 add_one 当参数传进去,执行两次

"基于'函数是值',就有了高阶函数(higher-order function)——接收函数或返回函数的函数。最著名的三个:"

orders = [
    {"book": "三体", "price": 29.9, "qty": 2, "customer": "张三"},
    {"book": "活着", "price": 25.0, "qty": 5, "customer": "李四"},
    {"book": "三体", "price": 29.9, "qty": 1, "customer": "王五"},
    {"book": "百年孤独", "price": 42.0, "qty": 3, "customer": "张三"},
]

# filter:筛 —— 只留买 3 本以上的订单
big_orders = list(filter(lambda o: o["qty"] >= 3, orders))

# map:映射 —— 把每个订单变成"金额"
amounts = list(map(lambda o: o["price"] * o["qty"], orders))

# reduce:归约 —— 把所有金额累加成总营业额
from functools import reduce
total = reduce(lambda acc, amt: acc + amt, amounts, 0)

print("总营业额:", total)          # 288.8
print("回头客名单:", [o["customer"] for o in big_orders])  # 李四、张三

"看,filter 筛、map 变、reduce 汇总——把'循环'抽象成了'操作'。你不再关心'怎么循环',只关心'做什么'。代码从'怎么做(how)'变成了'做什么(what)',这就是抽象层级又上一层。"

3.2 闭包:函数记住出生时的环境

"还有闭包(closure):函数不光是一段代码,它还记住了定义它的那一刻的环境。"

def make_discounter(rate):
    # discount 这个函数"记住"了 rate —— 即使 make_discounter 已经返回了
    def discount(price):
        return price * rate
    return discount

vip_88 = make_discounter(0.88)      # 一个"永远打 88 折"的函数
member_95 = make_discounter(0.95)   # 一个"永远打 95 折"的函数

print(vip_88(100))      # 88.0
print(member_95(100))   # 95.0

"discount 被返回出去后,rate 本该'消失'(函数调用结束了),但它没消失——discountrate 牢牢记在心里。这就是闭包:函数 + 它出生时的环境。回调函数、事件处理器、装饰器,到处是它的身影。"

3.3 不可变数据:没有副作用,并发更安全

"函数式的第三个理念:不可变数据(immutability)——数据一旦创建,永不修改,要变就造个新的。"

# 命令式:改来改去
stock = 100
stock = stock - 1          # 变量被"改"了

# 函数式:不修改,只返回新值
def sell(stock, n):
    return stock - n       # 原 stock 保持不变,返回一个"新库存"

new_stock = sell(100, 1)   # new_stock = 99,stock 还是 100

"好处:没有副作用(side effect),并发更安全——既然没人能改我的数据,那大家随便读,永远不会数据竞争!还记得上一章的超卖吗?不可变数据在根源上消灭了一类竞态。"

3.4 模式匹配:比 if/else 更强的分支

"最后是模式匹配(pattern matching)——我们第一章讲过 match,但函数式语言把它玩到了极致:不只匹配值,还能解构数据。"

enum OrderStatus {
    Pending,          // 待支付
    Paid,             // 已支付
    Shipped(String),  // 已发货,带快递单号
    Cancelled,        // 已取消
}

fn describe(status: OrderStatus) -> String {
    match status {
        OrderStatus::Pending        => "等待顾客支付".to_string(),
        OrderStatus::Paid           => "已支付,准备发货".to_string(),
        OrderStatus::Shipped(track) => format!("已发货,单号:{track}"),  // 直接把数据解出来!
        OrderStatus::Cancelled      => "订单已取消".to_string(),
    }
}

"注意 Shipped(track) —— 匹配的同时把快递单号解构出来了,不需要先匹配再取值。而且编译器会检查 match 是否覆盖了所有分支——漏写 Cancelled?编译不过。比 if/else 更强大、更安全的分支。"


四、章末:老周的"第八、九层"总结

白板补上:

第八层:并发与异步(同时做多件事)
纯文本
执行
├── 多线程 → CPU 多核,真正并行
├── 锁 / 互斥 / 通道 → 线程间共享数据要同步(锁防撞车,通道改传消息)
├── 异步 / async / await → IO 等待时不阻塞(事件循环、协程)
└── Actor 模型 → 消息传递,避免共享状态(Erlang)
第九层:函数式思想(把计算当数学)
纯文本
函数
├── 一等函数 → 函数可当参数、返回值、存变量
├── 闭包 → 函数记住定义时的环境
├── 不可变数据 → 没副作用,并发更安全
├── 高阶函数(map/filter/reduce)→ 把循环抽象成操作
└── 模式匹配 → 比 if/else 更强(解构数据)

"但是小陈,你有没有发现,最近每次王姐提需求,我们都要改一堆代码?"老周忽然问。

"是……上周她说报表要加一列,我改了半天。"

"问题就在这。人写的代码,总在被需求追赶。 有没有办法让'代码自己写代码'?让王姐自己就能配置规则,不用每次找我们?"

小陈:"代码……自己写自己?"

"对,那叫元编程——下一章,我们让系统自己长出腿来。"

✌ 语言