4.1.7 线程同步与并发基础

线程同步与并发基础聚焦多线程访问共享资源时的正确性与性能。并发提升吞吐,但若缺乏同步会导致竞态、数据不一致与崩溃。本节在理论基础上补充可执行示例、排错与练习,便于在 Linux 环境落地验证。

并发问题的本质#

  • 竞态条件:多个线程同时读写共享数据,执行顺序不确定。
  • 原子性破坏:复合操作被打断,例如“读-改-写”。
  • 可见性问题:线程对变量的修改未及时被其他线程看到。
  • 有序性问题:CPU/编译器重排导致执行顺序与预期不一致。

原理草图:线程与锁的交互#

文章图片

线程同步的核心手段#

  • 互斥锁(Mutex):同一时刻仅一个线程进入临界区。
  • 读写锁(RWLock):读多写少场景提升并发度。
  • 自旋锁(Spinlock):忙等避免上下文切换,适合极短临界区。
  • 信号量(Semaphore):控制并发数量。
  • 条件变量(Condvar):事件通知与等待。

可执行示例:竞态与互斥锁对比#

以 C/pthreads 演示。无锁时计数器结果不稳定;加锁后稳定。

1) 安装与编译环境

# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y build-essential

# RHEL/CentOS
sudo yum install -y gcc make

2) 编写示例程序

cat > /tmp/race.c <<'EOF'
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define N 1000000
long counter = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

void* worker_no_lock(void* arg){
    for(int i=0;i<N;i++){
        counter++; // 竞态:读-改-写
    }
    return NULL;
}

void* worker_with_lock(void* arg){
    for(int i=0;i<N;i++){
        pthread_mutex_lock(&mtx);
        counter++;
        pthread_mutex_unlock(&mtx);
    }
    return NULL;
}

int main(int argc, char* argv[]){
    int use_lock = (argc > 1);
    pthread_t t1, t2;
    counter = 0;
    if(use_lock){
        pthread_create(&t1, NULL, worker_with_lock, NULL);
        pthread_create(&t2, NULL, worker_with_lock, NULL);
    }else{
        pthread_create(&t1, NULL, worker_no_lock, NULL);
        pthread_create(&t2, NULL, worker_no_lock, NULL);
    }
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("counter=%ld (expected=%d)\n", counter, 2*N);
    return 0;
}
EOF

3) 编译与运行

gcc -O2 -pthread /tmp/race.c -o /tmp/race

# 无锁运行,多次执行结果可能不一致
/tmp/race
/tmp/race

# 加锁运行,结果稳定
/tmp/race lock

预期效果
- 无锁:counter 小于预期且多次变化。
- 加锁:counter 等于 2,000,000。

条件变量示例:生产者-消费者#

cat > /tmp/cond.c <<'EOF'
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int ready = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* producer(void* arg){
    sleep(1);
    pthread_mutex_lock(&mtx);
    ready = 1;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mtx);
    return NULL;
}

void* consumer(void* arg){
    pthread_mutex_lock(&mtx);
    while(!ready){
        pthread_cond_wait(&cond, &mtx);
    }
    printf("consumer got event\n");
    pthread_mutex_unlock(&mtx);
    return NULL;
}

int main(){
    pthread_t p,c;
    pthread_create(&c, NULL, consumer, NULL);
    pthread_create(&p, NULL, producer, NULL);
    pthread_join(p, NULL);
    pthread_join(c, NULL);
    return 0;
}
EOF

gcc -O2 -pthread /tmp/cond.c -o /tmp/cond
/tmp/cond

并发性能与可观测性#

1) 线程与上下文切换监控

# 观察上下文切换与线程数
pidof race
pid=<race_pid>

# /proc 中查看线程数量
ls /proc/$pid/task | wc -l

# 上下文切换统计
cat /proc/$pid/status | egrep 'voluntary|nonvoluntary'

2) 运行态排查

# 观察线程状态与调度
top -H -p $pid

# 采样锁争用与调度
pidstat -w -t -p $pid 1 5

典型并发风险与排错#

  • 死锁:线程互相等待锁。
    排查
    bash # 生成线程栈(需 gdb) gdb -p $pid -ex "thread apply all bt" -ex quit
    观察多线程同时卡在 pthread_mutex_lock,检查锁顺序。

  • 活锁:高 CPU 占用但无进展。
    排查:top 中 CPU 高,线程状态多为 R;检查重试循环是否缺少退避。

  • 饥饿:部分线程长期等待。
    排查:pidstat 观察特定线程长期等待,结合调度策略优化。

  • 优先级反转:低优先级持锁阻塞高优先级。
    排查:检查线程优先级与持锁时间,必要时缩短临界区。

实践建议#

  • 缩小临界区,减少锁持有时间。
  • 统一加锁顺序避免死锁。
  • 读多写少使用读写锁;短临界区可用自旋锁。
  • 通过 TLS、消息传递降低共享状态。
  • 监控锁争用与上下文切换,结合负载与延迟评估线程数。

练习#

  1. 修改 /tmp/race.c,将线程数改为 4,观察无锁情况下误差变化。
  2. 使用读写锁替换互斥锁实现“读多写少”计数器。
  3. pidstat -w -t 记录加锁与不加锁两种场景的上下文切换差异,整理结论。