背景

互斥

  • Peterson 算法
  • 原子指令和自旋锁

并发编程

  • 数据竞争
  • 并发数据结构

大家已经掌握了越来越多编写并发程序的工具了!

本次课内容与目标

使用操作系统 (和硬件机制) 实现非自旋的锁

  • yield 锁
  • 互斥锁
  • futex: 自旋 + 等待

复习:自旋锁

使用原子指令实现

int table = KEY;
void lock() {
retry:
  int got = xchg(&table, NOTE);
  if (got != KEY)
    goto retry;
  assert(got == KEY);
}
void unlock() {
  xchg(&table, KEY)
}
typedef struct { int locked; } spinlock_t;
#define LOCK_INIT() ((spinlock_t) { .locked = 0 })
void spin_lock(spinlock_t *lk)   { while (xchg(&lk->locked, 1)); }
void spin_unlock(spinlock_t *lk) { xchg(&lk->locked, 0); }

自旋锁:应用

保护并发代码安全、消除数据竞争

  • deposit/withdraw

并发数据结构

  • malloc/free (Lab1)

自旋锁:浪费问题

多个处理器上的线程争相打印

  • 只有一个能获得锁
  • 那其他 CPU 上的线程就只能围观了?

void thread() {
  lock(&lk);
  // 单个 printf 保证线程安全
  printf("LOG: ");
  printf(very_long_string);
  unlock(&lk);
}
$ ./a.out > /mnt/sdcard/a.txt  # printf 将非常耗时

自旋锁:无法容忍的浪费问题

别忘了操作系统还管理其他进程/线程……

  • 持有锁的线程切出,“全核围观”,CPU 利用率为零

void thread() {
  while (1) {
    lock(&lk);
    // ------------ (中断;线程切换)
    do_something();
    unlock(&lk);
  }
}

int main() {
  for (int i = 0; i < 1000; i++) create(thread); // 线程 >> CPU
}

改进自旋锁的性能

长临界区:分析

持有锁的线程

  • 可能发生系统调用或中断
  • 期间 CPU 可能会切换到其他线程执行
    • 锁应该保持持有状态 (例如阻止并发的 printf)

等待锁的线程

  • 能否不必占用处理器自旋
    • 如果系统中还有不需要这把锁的线程,可能让他们先执行

一个想法

Simply yield!


用户进程的事,操作系统完全说了算

  • “syscall” 指令能做任何事

与自旋锁完全一样,除了在自旋失败后增加一次系统调用:

void yield_lock(spinlock_t *lk) {
  while (xchg(&lk->locked, 1)) {
    syscall(SYS_yield); // yield() on AbstractMachine
  }
}
void yield_unlock(spinlock_t *lk) {
  xchg(&lk->locked, 0);
}

一个小优化

如果等待锁的线程非常非常多,我们需要 “空转一轮” 才能让真正需要运行的线程执行。

  • 能否干脆暂停这个线程,等有人解锁以后再被调度执行?

中断/系统调用后运行哪个线程完全由操作系统决定

  • “Virtualization”: 操作系统其实是 “状态机的管理者”
  • 在操作系统上加一些 hacking
    • 操作系统可以知道锁的状态
      • spin-wait 的线程,即便调度执行,也只是 spin + yield
    • 在线程调度时,如果满足
      t->registers.PC $\in$ yield_lock(lk) $\land$ lk->locked == 0
      就不调度这个线程

这个 Hacking 可以用系统调用实现

提供两个系统调用:

  • mutex_lock(&locked)
    • 如果 xchg(&locked, 1) == 1,就不再调度当前线程
      • 为当前线程标记上 “block on locked
  • mutex_unlock(&locked)
    • xchg(&locked, 0);
    • 如果有 “block on locked” 的线程,修改其状态为可调度

操作系统内核中的互斥锁

void mutex_lock(mutexlock_t *lk) {
  if (lk->locked != 0) {
    append(lk->wait_list, current); // 睡眠
    current->status = BLOCKED;
    yield(); // 操作系统控制了中断,有调度权
  } else {
    lk->locked = 1;
  }
}
void mutex_unlock(mutexlock_t *lk) {
  if (!is_empty(lk->wait_list)) {
    pop_front(lk->wait_list)->status = RUNNABLE; // 唤醒
  } else {
    lk->locked = 0;
  }
}

互斥锁:游泳馆的更衣室协议 (在 L2 中实现)

管理员 (操作系统代码) 有一个手环

  • 管理员用自旋锁保护原子性、消除数据竞争
  • 之后我们将看到不止一个手环的设计

任何人想进入更衣室都需要通过管理员

  • 先到的人 (线程)
    • 成功获得手环 → 进入游泳馆 (线程可被调度执行)
  • 后到的人 (线程)
    • 不能进入游泳馆,排队等待 (线程进入队列,不被调度执行)
  • 洗完澡出来的人 (线程)
    • 交还手环给管理员 → 管理员交给排队的人进入游泳馆 (等待的线程被唤醒)

Futex

关于互斥的一些分析

自旋锁 (线程直接共享 locked)

  • 好处:更快的 fast path
    • xchg 成功 → 立即进入临界区,开销很小
  • 坏处:更慢的 slow path
    • xchg 失败 → 浪费 CPU 自旋等待

睡眠锁 (locked 保存在操作系统内核,通过系统调用访问)

  • 好处:更快的 slow path
    • 上锁失败线程不再占用 CPU
  • 坏处:更慢的 fast path
    • 即便上锁成功也需要进出内核 (syscall)

Futex: Fast Userspace muTexes

小孩子才做选择,我全都要

  • fast path: 一条原子指令,上锁成功立即返回
  • slow path: 上锁失败,执行系统调用睡眠

pthreads 库中的互斥锁 (pthread_mutex)

  • sum-mutex.c
    • 观察系统调用
    • gdb 调试
      • set scheduler-locking on
      • info threads; thread X

Futex: Fast Userspace muTexes (cont'd)

Futexes are very basic and lend themselves well for building higher-level locking abstractions such as mutexes, condition variables, read-write locks, barriers, and semaphores. —— futex (7)


操作系统会设计怎样的接口?

  • 站在 “状态机视角” 考虑这个问题

futex 系统调用

int futex(int *uaddr, int futex_op, int val, ...);

RTFM


任何体系结构上 uaddr 都是 4B

  • futex_wait (futex_op == FUTEX_WAIT)
    • 如果 *uaddr == val 就睡眠等待 futex_wake (原子完成)
    • 否则返回 -EAGAIN
  • futex_wake (futex_op == FUTEX_WAKE)
    • 唤醒至多 val 个正在睡眠的线程

Futex + 自旋

void futex_lock() {
  while (1) {
    int r = xchg(&locked, 1);
    if (r == 0) break;
    syscall(SYS_futex, &locked, FUTEX_WAIT, 1);
  }
}

void futex_unlock() {
  xchg(&locked, 0);
  syscall(SYS_futex, &locked, FUTEX_WAKE, 1);
}

仍然不是我们想要的 (正确性:futex.yaml)

总结

总结

本次课内容与目标

  • 实现非自旋的锁 (睡眠 & futex)

Take-away messages

  • 操作系统有非常大的权力
    • 操作系统是状态机的管理者
    • syscall 指令可以做 “任何事”