```html
📅 更新日期:2023-10-27 🌐 域名:www.kaoacca.cn

C语言多线程面试题全解析

深入理解线程创建、同步机制、死锁预防及内存一致性模型,掌握并发编程核心考点,助您从容应对大厂技术面试。

一、C语言多线程基础核心

1. 进程与线程的区别

在C语言面试中,这是一个必问的基础题。进程是资源分配的基本单位,拥有独立的内存空间;而线程是CPU调度的基本单位,共享进程的堆内存和全局变量,但拥有独立的栈空间和寄存器状态。线程的创建和切换开销远小于进程。

2. POSIX Threads (pthread)

Linux环境下,C语言多线程编程主要依赖POSIX线程库(pthread)。核心函数包括:pthread_create(创建线程)、pthread_join(等待线程结束)、pthread_exit(终止线程)。面试常考这些函数的返回值及错误处理。

3. 线程的生命周期

线程状态包括:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated)。面试官可能会询问线程在阻塞状态下释放CPU资源的情况,以及pthread_join如何导致调用线程阻塞。

二、同步机制与互斥锁详解

互斥锁 (Mutex)
条件变量 (CondVar)
读写锁 (RWLock)

互斥锁 (pthread_mutex_t)

互斥锁用于保护临界区,确保同一时刻只有一个线程访问共享资源。面试重点在于理解pthread_mutex_lock和pthread_mutex_unlock的原语操作,以及锁的粒度选择。

#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void worker(void arg) {
    pthread_mutex_lock(&mutex); // 加锁
    // 临界区代码
    printf("Thread ID: %lu\n", pthread_self());
    pthread_mutex_unlock(&mutex); // 解锁
    return NULL;
}

面试陷阱:如果忘记解锁,会导致死锁;如果在持有锁时调用阻塞函数(如scanf),会导致其他线程永久阻塞。此外,需了解互斥锁的类型:快速锁、递归锁、检错锁。

条件变量 (pthread_cond_t)

条件变量用于线程间的通信,通常与互斥锁配合使用。它允许线程在满足特定条件时挂起,直到其他线程通知。核心函数是pthread_cond_wait和pthread_cond_signal。

// 等待条件
pthread_mutex_lock(&mutex);
while (condition == false) {
    pthread_cond_wait(&cond, &mutex); // 原子地解锁并等待
}
// 处理数据
condition = false;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

关键点:为什么pthread_cond_wait需要放在while循环中?因为存在“虚假唤醒”(Spurious Wakeup)现象,或者多个线程被唤醒但只有一个能处理数据的情况。必须重新检查条件。

读写锁 (pthread_rwlock_t)

当读操作远多于写操作时,读写锁能显著提高性能。它允许多个读者同时访问,但写者独占访问。面试中常问读写锁与互斥锁的性能对比。

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
// 读操作
pthread_rwlock_rdlock(&rwlock);
// 读取数据
pthread_rwlock_unlock(&rwlock);
// 写操作
pthread_rwlock_wrlock(&rwlock);
// 修改数据
pthread_rwlock_unlock(&rwlock);

注意:读写锁可能导致写者饥饿(Writer Starvation),即持续的读请求导致写者无法获得锁。实现时需考虑公平性策略。

三、死锁的产生与避免

1. 死锁的四个必要条件

面试必背知识点。死锁发生必须同时满足:互斥条件(资源独占)、请求与保持条件(持有资源等待新资源)、不剥夺条件(资源不可强制释放)、循环等待条件(线程形成环路)。

2. 死锁的检测与解除

操作系统层面可通过资源分配图检测死锁。在应用层,通常通过超时机制或定期检查来解除。例如,设置pthread_mutex_trylock,如果获取锁失败,则释放所有已持有锁并重试。

3. 避免死锁的策略

最常用的策略是破坏循环等待条件:规定线程必须按顺序获取锁(如先锁A再锁B,所有线程遵循此顺序)。此外,使用trylock配合退避算法也是一种有效手段。

死锁避免策略 实现方式 优缺点
顺序加锁 全局定义锁的获取顺序 简单有效,但可能降低并发度
超时重试 使用trylock并设置超时 灵活,但可能引发活锁(Livelock)
锁粗化/细化 调整锁的粒度 细粒度提高并发,粗粒度减少开销

四、内存模型与原子操作

1. 内存可见性问题

由于CPU缓存和编译器优化,一个线程对共享变量的修改可能不会立即被其他线程看到。在C11标准之前,C语言缺乏标准的内存模型,依赖编译器扩展。C11引入了_Atomic和内存序(Memory Order)。

2. 原子操作 (Atomic Operations)

原子操作是不可中断的操作。C11提供了atomic_int等类型。面试常考:fetch_add、compare_exchange_weak(CAS操作)的实现原理。CAS是无锁编程的基础。

3. 内存序 (Memory Order)

理解memory_order_relaxed、memory_order_acquire和memory_order_release。Acquire-Release语义保证了 happens-before 关系,是构建高效无锁数据结构的关键。

五、高级实战与并发设计模式

在实际工程项目中,单纯使用锁往往性能不足。以下是几个高频考点:

线程池
无锁编程
线程局部存储

线程池设计 (Thread Pool)

频繁创建和销毁线程开销巨大。线程池维护一组工作线程和一个任务队列。面试常要求手写简易线程池,涉及:生产者-消费者模型、任务队列的线程安全、线程优雅退出机制。

// 伪代码结构
struct ThreadPool {
    pthread_t threads;
    int thread_count;
    struct TaskQueue queue;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int shutdown;
};

无锁编程 (Lock-Free Programming)

利用CAS指令实现数据结构的并发访问,避免锁带来的上下文切换和死锁风险。难点在于ABA问题。解决方案包括使用版本号(Tagged Pointers)或内存回收机制(RCU)。

线程局部存储 (TLS)

使用thread_local关键字(C11)或__thread(GCC扩展)声明变量。每个线程拥有该变量的独立副本,彻底避免竞争,但需注意内存开销和数据一致性成本。

FAQ:高频面试题深度解答

Q1: 为什么pthread_join会导致调用线程阻塞?

A: pthread_join的设计目的是让主线程或其他线程等待目标线程执行完毕,以便回收目标线程的资源(如栈空间、线程描述符等)并获取其退出状态。如果不调用join或detach,线程资源将一直存在,导致内存泄漏。调用线程进入等待状态,直到目标线程终止。

Q2: 自旋锁(Spinlock)和互斥锁(Mutex)的区别是什么?何时使用自旋锁?

A: 互斥锁在获取失败时会使线程进入睡眠状态,涉及上下文切换,开销较大;自旋锁在获取失败时会不断循环检查(忙等待),不释放CPU。自旋锁适用于临界区非常短、竞争不激烈的场景,以避免上下文切换的开销。但在多核CPU上,如果锁竞争严重,自旋锁会浪费大量CPU周期。

Q3: 如何避免“惊群效应”(Thundering Herd)?

A: 惊群效应指多个线程阻塞在同一个资源上(如accept),当资源可用时,所有线程都被唤醒,但只有一个能成功处理,其余继续休眠,造成CPU浪费。解决方法包括:使用互斥锁保护accept调用,或使用epoll的LT/ET模式配合合理的线程分发策略,确保只有一个线程处理新连接。

```
斌知网
蜀ICP备2026017620号-5