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、消息传递降低共享状态。
- 监控锁争用与上下文切换,结合负载与延迟评估线程数。
练习#
- 修改 /tmp/race.c,将线程数改为 4,观察无锁情况下误差变化。
- 使用读写锁替换互斥锁实现“读多写少”计数器。
- 用
pidstat -w -t记录加锁与不加锁两种场景的上下文切换差异,整理结论。