操作系统面试题 — 你知道的线程同步的方式有哪些?

228 阅读4分钟

Author : Cyan_RA9
Source : 【卡码笔记】网站
Question : 你知道的线程同步的方式有哪些?

【简要回答】

  1. 内核级线程(KLT)的同步方式
    • 互斥锁(Mutex):确保同一时间只有一个线程进入临界区。
    • 信号量(Semaphore):控制资源访问数量,支持计数和二进制模式。
    • 条件变量(Condition Variable):用于线程间状态通知,基于特定条件阻塞或唤醒线程,需搭配互斥锁使用。
    • 读写锁(Read-Write Lock):允许多读单写,提高读多写少场景的性能。
    • 自旋锁(Spinlock):忙等待锁,适用于短临界区。
  2. 用户级线程(ULT)的同步方式
    • 原子操作(Atomic):通过原子指令如CAS(Compare-And-Swap),避免锁开销。
    • 协程同步原语:通过调度器协作式切换,如Go的channel,通过消息传递替代共享内存。
    • 轻量级锁:用户态实现的互斥锁(如Java的偏向锁)。
    • 非阻塞数据结构:如无锁队列,依赖硬件指令实现并发安全。

【详细回答】

  1. 内核级线程(KLT)的同步方式
    • 互斥锁(Mutex)
      实现:通过系统调用(如pthread_mutex_lock)获取锁,竞争失败的线程被阻塞并移出调度队列。
      优化:可配置为自适应自旋(如Linux的futex),减少上下文切换。
      适用场景:长临界区(如文件操作),避免忙等待浪费CPU。
    • 信号量(Semaphore)
      计数信号量:管理资源池(如数据库连接池)。
      二进制信号量:等价于互斥锁,但支持跨进程(System V信号量)。
    • 条件变量(Condition Variable)
      实现:需与互斥锁配合,wait()释放锁并阻塞,signal()唤醒等待线程。
      典型场景:生产者-消费者模型中的缓冲区空/满通知。
    • 读写锁(Read-Write Lock)
      实现:读锁共享(允许多线程同时读),写锁独占(仅允许单线程写)。
      适用场景:配置文件读取(多读)与更新(单写)。
    • 自旋锁(Spinlock)
      实现:线程循环检测锁状态(如while(lock == BUSY);),不主动让出CPU。
      适用场景:多核短临界区(如Linux内核中断处理)。
      风险:长时间自旋浪费CPU,需配合抢占式调度使用。
  2. 用户级线程(ULT)的同步方式
    • 原子操作(Atomic)
      实现:通过CPU指令(如CASLL/SC)直接操作内存,无需锁。
      适用场景:无锁计数器(如atomic.AddInt32)、标志位更新。
      局限性:仅适用于简单操作,复杂逻辑仍需锁。
    • 协程同步原语
      实现:如Go的channel,通过通信传递数据所有权,避免共享内存。
      适用场景:高并发I/O密集型任务(如网络服务器)。
      优势:无锁竞争,依赖调度器协作切换(非抢占式)。
    • 轻量级锁
      实现:先尝试用户态自旋,失败后升级为内核锁(如Java的锁膨胀)。
      优势:减少内核切换次数,平衡性能与公平性。
    • 非阻塞数据结构
      无锁队列基于原子操作设计无锁队列、栈等数据结构(如Java的ConcurrentLinkedQueue)。
    • 协作式调度同步
      示例:用户态调度器主动让出CPU(如协程yield),通过事件循环协调任务,避免竞态条件。
      缺点:依赖开发者手动控制切换时机,易引发死锁。

【知识图解】

  • 自旋锁(Spinlock)的工作示意图,如下所示:
    spinlock_workchart.jpg

  • 自旋与非自旋的流程图,如下图所示:
    spinlock_flowchart.jpg


【知识拓展】

  • 面试官可能的追问1 — 用户级线程(如Go的Goroutine)如何避免内核级同步的开销?
    • Go通过channel通信替代锁,由调度器在用户态管理协程切换,仅在必要时(如系统调用)才进入内核态。
  • 面试官可能的追问2 — 自旋锁在什么场景下会失效?如何优化?
    • 失效场景:单核环境(忙等待浪费CPU)、长临界区(自旋时间过长)。
    • 优化:设置最大自旋次数后转为阻塞锁,或结合自适应自旋(动态预测等待时间)。
  • 面试官可能的追问3:自旋锁在内核线程和用户线程中的使用有何差异?
    • 内核线程:可禁用中断确保原子性,防止死锁;支持多核并发自旋。
    • 用户线程:依赖原子指令或futex;单核环境需主动让出CPU(如调用sched_yield)。