一、为什么要多线程?
1.通过多线程并发提升处理能力,比如统计一批文件某个单词出现次数;
2.通过多线程实现异步非阻塞操作。
二、pthread_create 、多线程的内存结构和线程私有数据
1.pthread_create
头文件:
#include<pthread.h>
函数原型:
int pthread_create(pthread_t*restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);
各个参数说明:
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
编译链接参数:
-pthread (更建议)
返回值:
若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。
example:
pthread_mutex_init(&lock, NULL); // 初始化锁
a = 0; // 重置
for (int i = 0; i < THREAD_COUNT; ++i)
pthread_create(&threads[i], NULL, safe_increment, NULL);
for (int i = 0; i < THREAD_COUNT; ++i)
pthread_join(threads[i], NULL);
pthread_mutex_destroy(&lock);
pthread_join 调用该函数的线程将挂起等待,直到id为thread的线程终止
对于第三个参数,每个线程调用这一函数都是调用的同一个入口地址,具体看内存结构。
2.内存结构
┌───────────────┐
│ 代码段 │ ← 所有线程共享,函数入口地址都指向这里
├───────────────┤
│ 全局变量区 │ ← 所有线程共享,会产生竞争!(⚠️)
├───────────────┤
│ 堆(malloc) │ ← 所有线程共享,但谁申请谁用
├───────────────┤
│ TLS(线程私有数据)│ ← 每个线程独有(比如 __thread)
├───────────────┤
│ 栈(每线程一块) │ ← 每个线程都有自己的独立栈
│ ┌─────────┐ │
│ │ 局部变量 │ │
│ └─────────┘ │
└───────────────┘
3.线程私有化数据
__thread char error_buf[128]; // 每个线程一份,全局可见
__thread 变量是每个线程持久可用的“全局副本”,适合跨函数维护线程状态(比如错误消息、日志上下文、会话信息)。
4.原子操作
原子操作指的是:
a.一个操作或一组操作要么全部完成、要么全部不做,中间不会被任何线程中断或干扰。
b.在多线程环境下,原子操作是 不可分割、不可被打断 的。
例如,“我买东西” 若定义为原子操作,
动作分别有:我给出钱,钱给到商家,我拿到商品 。
每一个动作都不会被别人所干扰、打断,不会被自己再分割了。
如何实现原子操作?答案是 加速操作或资源(变量)原子化
以自增为例
方法一:加锁保护(互斥量)
pthread_mutex_t lock;
void* thread_func(void* arg) {
pthread_mutex_lock(&lock);
a++;
pthread_mutex_unlock(&lock);
}
✅ 效果:每次只有一个线程能进入临界区,从而保证 a++ 是“线程安全”的。
方法二:使用原子变量(C11 提供)
#include <stdatomic.h>
atomic_int a = 0;
void* thread_func(void* arg) {
atomic_fetch_add(&a, 1); // 原子加法,相当于 a++
}
✅ 效果:atomic_fetch_add 是底层由 CPU 原子指令实现的,不需要加锁,效率更高。
三、互斥锁、自旋锁
在这个加锁版本的 safe_increment 函数中:
pthread_mutex_lock(&lock);
a++;
pthread_mutex_unlock(&lock);
如果有多个线程要访问 a,而某个线程已经加锁了,那么其他线程在干什么?是在空等?让出 CPU?不断尝试加锁?
答:其他线程会阻塞在 pthread_mutex_lock() 上,直到锁可用。
它们不会执行 a++,也不会“忙等”,而是进入阻塞状态(sleep),被内核挂起,等待唤醒。
加打印试试:
void* safe_increment(void* arg) {
for (int i = 0; i < LOOP_COUNT; ++i) {
printf("线程 %ld 想加锁...\n", pthread_self());
pthread_mutex_lock(&lock);
printf("线程 %ld 获得锁,执行 a++\n", pthread_self());
a++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
会看到:
线程 1001 想加锁...
线程 1001 获得锁,执行 a++
线程 1002 想加锁...
线程 1003 想加锁...
线程 1002 获得锁,执行 a++
那我们有没有别的锁呢??还有自旋锁,等待机制不一样
自旋锁(spinlock):如果拿不到锁,线程不会睡眠,而是持续尝试获取锁(忙等) ,pthread_spinlock_t
互斥锁(阻塞锁):拿不到锁的线程会阻塞,进入睡眠,等待唤醒 pthread_mutex_t
行为对比
特性 自旋锁(spinlock) 互斥锁(pthread_mutex)
拿不到锁时 一直循环尝试(消耗CPU) 挂起等待(不占CPU)
上锁代价 低(用户态实现) 高(可能涉及内核调度)
临界区时间短 ✅ 非常合适 ❌ 调度代价反而浪费性能
临界区时间长 ❌ 会浪费 CPU ✅ 更合适
多核场景 ✅ 多线程可能很快拿到锁 ⚠️ 唤醒延迟高一点
tips:如果临界区非常短(比如 counter++),spinlock 更快;
如果临界区比较长(例如 I/O 操作、sleep、复杂运算),用 mutex 更好;
在单核 CPU 上,spinlock 通常是灾难,因为线程不能释放锁还在占着 CPU,其他线程永远等不到。
编译:使用 pthread 的多线程程序必须链接 pthread 库,所以你需要加上 -pthread(或 -lpthread)。假设你的文件名是 lock_test.c,那么编译命令如下1:
gcc -o lock_test lock_test.c -pthread