多线程、线程同步、互斥锁、自旋锁

一、为什么要多线程?

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值