线程栈与TLS和线程互斥

线程栈

主线程(进程 main 栈)特性:

(1)来源:fork 复制父进程栈

(2)可动态自动扩容

(3)缺页容错特殊:允许访问未映射页、不一定直接段错误的栈

线程栈

复制代码
mem = mmap(NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
  • 标志MAP_STACK:专门标记这块内存用作线程栈;
  • 默认固定 8MB 大小(一般够用),不支持动态扩容,空间用完直接栈溢出崩溃;
  • 属于进程虚拟地址里一块独立匿名映射区域

pthread_create → allocate_stack(mmap 分配栈)→ 调用sys_clone(clone封装了sys_clone)系统调用 ,clone 时把 mmap 得到的栈顶(栈向下增长)newsp传给内核,内核新建 task_struct (LWP) 绑定该栈。

复制代码
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);

线程之间虽说是"私有"的线程栈,但线程之间也可以访问

子线程的a变量虽然定义在其"私有"的线程栈上,但因为线程之间的虚拟地址空间是共享的所以其它线程当然也可以通过地址访问到a变量。

即只要拿到栈内变量地址,线程语法上能访问,但不推荐跨线程读写对方栈:

  • 子线程退出时 mmap 栈会被 munmap 释放,指针变成野指针;
  • 属于 C/C++ 未定义行为,不同系统 / 内核表现不一致(有时正常、有时段错误)。

C++封装线程

先再认识2个接口

设置线程名:

复制代码
// 参数1 线程tid,参数2 字符串名字
int pthread_setname_np(pthread_t thread, const char *name);

获取线程名:

复制代码
char buf[16];
pthread_getname_np(pthread_t tid, buf, sizeof(buf));

pthread.hpp:

复制代码
#pragma once
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <string>
#include <errno.h>
#include <cstring>
#include<cstdint>
using namespace std;
namespace ThreadModlue
{
    static uint32_t number = 1;//bug多个线程同时构造时,number++存在数据竞争,会出现重名线程
    //定义在 <cstdint>不用int是两点:1. 线程编号只能是正数
    //2. uint32_t固定 32 位,跨平台不会溢出
    using func_t = function<void()>;

    class Thread
    {
    private:
        void EnableRunning()
        {
            cout<<"_isrunning=true"<<endl;
            _isrunning = true;
        }
        void EnableDetach()
        {
            cout << "_isdetach=true" << endl;
            _isdetach = true;
        }
          static void *Routine(void *args)
        {
            Thread *self = static_cast<Thread *>(args);
            self->EnableRunning();
            self->Detach();
            pthread_setname_np(self->_tid, self->_name.c_str());
            self->_func(); // 回调处理
            return nullptr;
        }
        //C++ 普通成员函数隐藏第一个参数 this,函数真实原型,所以Routine真实原型
        //void Routine(Thread* this, void* args);与pthread_create 对线程入口函数的强制要求
        //void* (*start_routine)(void*);只接收单个 void * 参数,不能多参数。有冲突所以加static,
        //static成员函数没有隐含this指针

    public:
        Thread(func_t func)
            : _isdetach(false), _isrunning(false), _tid(0), _func(func)
        {
            _name = "thread-" + std::to_string(number++);
        }
        void Detach()
        {
            if (_isrunning && _isdetach)
            {
                cout<<"调用了phread_detach,线程分离了"<<endl;
                pthread_detach(_tid);
            }
            else if (_isdetach == false)
                EnableDetach();
        }
      
        bool Start()
        {
            if (_isrunning)
                return false;
            int n = pthread_create(&_tid, nullptr, Routine, this);
            if (n != 0)
            {
                std::cerr << "create thread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                std::cout << _name << " create success" << std::endl;
                return true;
            }
        }
        bool Stop()
        {
            if (_isrunning)
            {
                int n = pthread_cancel(_tid);
                if (n != 0)
                {
                    std::cerr << "stop thread error: " << strerror(n) << std::endl;
                    return false;
                }
                else
                {
                    _isrunning = false;
                    std::cout << _name << " stop" << std::endl;
                    return true;
                }
            }
            return false;
        }
        bool Join()
        {
            if (_isdetach)
            {
                return false;
            }
            int n = pthread_join(_tid, nullptr);
            if (n != 0)
            {
                std::cerr << "join thread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                std::cout << _name << " join" << std::endl;
                return true;
            }
        }
        ~Thread()
        {
        }

    private:
        pthread_t _tid;
        bool _isrunning;
        bool _isdetach;
        func_t _func;
        string _name;
    };

};

main.cc:

复制代码
#include "pthread.hpp"
#include <unistd.h>
#include <vector>
using namespace ThreadModlue;
void func()
{
    while (1)
    {
        char name[128];
        pthread_getname_np(pthread_self(), name, sizeof(name));
        std::cout << "我是一个新线程: " << name << std::endl; // 我的线程的名字是什么呀?debug
        sleep(1);
    }
}
int main()
{
    Thread t(func);
  
    t.Detach();
    t.Start();
    sleep(5);

    t.Stop();

    sleep(5);

    t.Join();

    return 0;
}

运行结果:

线程局部存储(TLS)

复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
#include <stdio.h>
using namespace std;
static int count = 0;
string Addr(int &c)
{
    char addr[64];
    snprintf(addr, sizeof(addr), "%p", &c);
    return addr;
}
void *routine1(void *args)
{
    (void)args;
    while (1)
    {
        cout << "thread-1,count=" << count << "[我来修改count],"
             << "&count: " << Addr(count) << endl;
        count++;
        sleep(1);
    }
}
void *routine2(void *args)
{
    (void)args;
    while (1)
    {
        cout << "thread-2,count=" << count
             << "&count: " << Addr(count) << endl;
        count++;
        sleep(1);
    }
}
int main()
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, nullptr, routine1, nullptr);
    pthread_create(&tid2, nullptr, routine2, nullptr);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

运行结果:

我们可以从运行结果看出count的地址并没有发生变化,再换成__thread int count=0后:

发现两个线程的count不一样了,互不影响

线程局部存储原理:操作系统为每个线程单独分配 TLS 内存段,线程访问时自动切换自身副本,地址各不相同,互不干扰,完全隔离。

线程互斥

进程线程间的互斥相关背景概念

• 共享资源

• 临界资源:多线程执行流被保护的共享的资源就叫做临界资源

• 临界区:每个线程内部,访问临界资源的代码,就叫做临界区

• 互斥:任何时刻,互斥保证有且只有⼀个执行流进入临界区,访问临界资源,通常对临界资源起 保护作用

• 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成

下面我们看一段模拟抢票过程的代码

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

运行结果(截取一部分):

为什么会有数据不一致问题:

核心原因:ticket--不是原子操作,3条汇编

复制代码
0xFF00  mov ebx, ticket    // 1. 从内存把ticket载入CPU寄存器ebx
0xFF02  sub ebx, 1         // 2. CPU运算:寄存器内数值-1
0xFF04  mov ticket, ebx    // 3. 把计算结果写回内存ticket

这三步是分开执行的,线程切换可以发生在任意两步中间,所以数据就会错乱。

eg:

  • 内存开始ticket=100,线程 A 执行到0xFF04时,发生线程切换,此时保留线程的硬件上下文,pc指针为0XFF04,ebx为99
  • 切换到线程 B,假如完整并且多次执行了三次汇编,将ticket修改为 1
  • 切回线程 A,从 PC=0xFF04 继续执行:直接把 ebx=99 写回内存
  • 最终内存ticket=99,线程 B 的修改直接丢失,数据完全错乱

为什么会出现负数的情况:

当几个线程的ticket都为1时

  • 线程 A:if(ticket>0) 判断通过,进入usleep主动放弃 CPU
  • 线程 B:同样if(ticket>0)判断通过,进入usleep
  • 两个线程同时唤醒,先后执行:打印 ticket=1 → ticket-- 最终内存 ticket = -1,出现超卖负数。

usleep的作用:

人为拉长 "判断票数" 和 "修改票数" 之间的间隔(因为进入usleep后当前线程休眠了,cpu就有就会去调度其它线程了),强制触发线程切换,容易看到bug

线程切换触发时机

线程切走时机(就是陷入内核时机):

  1. 时间片耗尽
  2. 阻塞式系统调用:sleep/IO 读写等等
  3. 中断

线程切回时机(选择新的线程)

系统调用完成,从内核态切回用户态时,操作系统检查是否需要换线程

pthread_mutex_t 互斥锁

依赖头文件

复制代码
#include <pthread.h>

两种初始化方法

静态初始化(全局锁)---无需手动销毁,程序结束自动释放;

复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态初始化(局部锁 )

复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • attrNULL 使用默认锁属性;
  • 使用完毕必须调用 pthread_mutex_destroy(&mutex) 释放资源。

销毁接口

复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

核心操作函数(全原子执行)

函数 作用 阻塞特性
pthread_mutex_lock(pthread_mutex_t *mutex) 申请锁 阻塞版:锁被占用时,线程挂起等待
pthread_mutex_trylock(pthread_mutex_t *mutex) 尝试申请锁 非阻塞版:拿不到锁直接返回错误,不挂起线程
. pthread_mutex_unlock(pthread_mutex_t *mutex) 释放锁 无阻塞,归还锁资源

加锁:尽量加锁的范围粒度要比较细,尽可能的不要包含太多的非临界区代码

方式1:全局锁

复制代码
int ticket = 100;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 方法1静态全局初始化

void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
       pthread_mutex_lock(&mutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

方式2:局部锁

复制代码
// 全局共享票数
int ticket = 100;

void *route(void *arg)
{
    char *id = (char*)arg;
    // 1. 定义局部互斥锁(栈上局部变量)
    pthread_mutex_t mutex;
    // 2. 动态初始化锁,attr传NULL使用默认属性
    pthread_mutex_init(&mutex, NULL);

    while (1) {
        // 加锁进入临界区
        pthread_mutex_lock(&mutex);

        if (ticket > 0) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            // 分支内解锁
            pthread_mutex_unlock(&mutex);
        } else {
            // 无票也要先解锁再退出循环
            pthread_mutex_unlock(&mutex);
            break;
        }
    }

    // 3. 销毁局部锁,释放内核资源
    pthread_mutex_destroy(&mutex);
    return NULL;
}

2种方式的运行结果(截取一部分):

发现确实这一次的结果正确了,但是为什么绝大部分都是线程2在抢票呢?甚至线程3和4根本就没有机会抢票

我们不妨分析一下整个过程:

  • thread 2 拿到锁,锁内 sleep 1ms 模拟卖票;
  • 打印、ticket--、执行unlock释放锁;
  • 代码无任何延时,直接跳回while(1)顶部,立刻执行lock
  • 此时 thread1、thread4和thread3 还在内核就绪队列排队,CPU 优先调度刚跑完的 thread2,再次抢占锁;
  • 循环往复,就出现同一个线程连续打印十几张票的现象。

怎么解决呢?

循环末尾加 usleep,下一次循环抢锁前,先延时一会,让出 CPU 给其他线程

复制代码
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        pthread_mutex_lock(&mutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
        // 新增:解锁后休眠,给其他线程调度时间
         usleep(1000);
     
    }
    return nullptr;
}

这次的运行结果:

c++锁

c++自己也有锁

复制代码
#include<mutex>
int ticket = 100;
mutex cpp_lock;
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        cpp_lock.lock();
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);

            ticket--;
          cpp_lock.unlock();
        }
        else
        {
            cpp_lock.unlock();
            break;
        }
    }
    return nullptr;
}

基于RAII风格的互斥锁

test.cc:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
using namespace std;
using namespace MutexModule;
int ticket = 100;
class ThreadData
{
public:
    ThreadData(const string &name, Mutex &lock)
        : _name(name), _lockp(lock)
    {
    }
    ~ThreadData()
    {
    }


    string _name;
    Mutex &_lockp;//为什么是引用?
};
//函数形参写 &:只是接收外部对象时不拷贝;
//类内部成员没写 &:初始化列表会执行拷贝,生成独立副本;
//想要全程共用同一把锁,构造参数 和 类成员 必须同时是引用 / 指针,只改一边没用。

void *route(void *arg)
{
    ThreadData *td = static_cast<ThreadData *>(arg);
    while (1)
    {
      { LockGuard guard(td->_lockp);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", td->_name.c_str(), ticket);
            ticket--;
         
        }
        else
        {
           
            break;
        }
      }
        usleep(1000);
    }
    return nullptr;
}
int main()
{

    Mutex lock;
    pthread_t t1, t2, t3, t4;
    ThreadData *td1 = new ThreadData("thread 1", lock);
    pthread_create(&t1, NULL, route, td1);

    ThreadData *td2 = new ThreadData("thread 2", lock);
    pthread_create(&t2, NULL, route, td2);

    ThreadData *td3 = new ThreadData("thread 3", lock);
    pthread_create(&t3, NULL, route, td3);

    ThreadData *td4 = new ThreadData("thread 4", lock);
    pthread_create(&t4, NULL, route, td4);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    delete td1,td2,td3,td4;


    return 0;
}

Mutex.hpp:

复制代码
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
    //第一层 Mutex 类:封装底层 C 锁 API
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex;
    };
    class LockGuard
    {
        public:
        LockGuard(Mutex &mutex):_mutex(mutex)//这里&但是!这只作用在函数内部临时变量 mutex,不影响类成员_mutex
        //比如mutex++,等等确实是引用,会影响外面传入的锁,但是_mutex(mutex)如果参数没有引用,就会发生值拷贝
        {
            _mutex.Lock();
        }
         ~LockGuard()
        {
            _mutex.Unlock();
        }


        private:
        Mutex &_mutex;//为什么是引用?

    };


}

3层封装关系:

Mutex(最底层,基础封装):直接封装 C 原生 pthread_mutex_t,负责锁的初始化、加锁、解锁、销毁

LockGuard(中层,RAII 自动管理层):

  • 构造函数:创建 Guard 瞬间调用 _mutex.Lock() 拿锁
  • 析构函数:Guard 离开作用域自动 _mutex.Unlock() 释放锁 作用:消除手动 unlock,杜绝漏解锁死锁,实现 RAII 自动资源管理

ThreadData(最上层):封装线程名字和锁

特别说明:内层{}

内层{}作用:LockGuard 是局部栈对象,离开包裹它的 {} 代码块时,自动调用析构函数解锁 。 如果不加这对大括号,guard 的生命周期会覆盖整个 while(1) 循环体,锁会持有整整一轮循环。

这样的话后面的usleep(1000)才能发挥无锁休眠,其他线程可以抢锁的效果,如果没有内层{}到了usleep(1000)仍然是持有锁,其它线程不能抢占锁,导致一个线程长期霸占锁。

没有内层{}: 有:

几个结论:

1.锁本身也是临界资源 :多线程同时竞争lock时,申请锁的底层操作必须是 CPU 原子指令,避免锁自身发生竞争错乱;

2.互斥锁本质能力

  1. 并行转串行 :临界区代码同一时刻只允许一个线程执行,消除数据竞争
  2. 变相原子性: 临界区代码在执行期间,不会被其它线程打断

3.对临界资源的保护本质:就是用锁对临界区代码的保护

核心问题:临界区内部(加了锁后)允许线程切换吗?切换后会发生什么?

先说结论

操作系统完全允许在线程持有锁时切换线程,锁不会阻止内核的调度切换,只会阻塞其他线程进入临界区

切换后的行为

当前线程拿着锁被切走、让出 CPU,其他线程尝试执行pthread_mutex_lock时,发现锁已被占用,不能进入临界区,等该线程重新被调度回来后,继续执行剩余临界区代码,直到主动unlock释放锁,其他线程才能竞争后抢到锁再进入。

临界区对外具备原子性

对没有持有锁的外部线程来说,只关心持有锁的线程要么完整走完临界区并释放锁,要么完全不进入,不关心临界区的中间执行状态

理解锁

互斥锁两种底层实现方式

硬件级方案:关闭时钟中断(单 CPU 老式实现思路,多cpu性能损耗极大,不再使用)

软件级方案(现代 CPU 通用):原子交换指令 xchg(CPU 提供swap/xchg交换指令,单条硬件指令、不可分割,天然原子)

伪代码(lock和unlock):

锁状态约定

  • mutex=1:锁空闲,可抢占
  • mutex=0:锁已被其他线程持有

线程 A、B 同时执行xchgb %al, mutex场景演示:

  • 初始内存mutex=1,寄存器%al都会置为0
  • 线程 A 交换:al=1、内存mutex=0 → 判断al>0,成功拿锁
  • 线程 B 随后交换:al=0、内存mutex=0 → 判断失败,循环重试等待

关键结论

交换是数据互换,不是拷贝 ,全局内存只有一份 mutex 变量,谁交换到1,谁就占有锁资源。

相关推荐
我的xiaodoujiao5 小时前
快速学习Python基础知识详细图文教程9--函数进阶
开发语言·python·学习·测试工具
j7~5 小时前
【Linux】二十二.《Linux 信号机制完全指南:表示、捕捉与处理》
linux·信号处理·volatile·可入重函数·保存信号
weixin_408099675 小时前
2026 图片去水印 API 接口完全指南:一键去除图片水印(附 Python/Java/PHP/C# 示例)
java·python·php·图片处理·api调用·图片去水印·石榴智能
爱喝水的鱼丶5 小时前
SAP-ABAP:ALV通用封装实践——搭建可复用的ALV开发工具类,减少80%重复代码
开发语言·性能优化·sap·abap·erp·alv
钱六两5 小时前
#3、SpringAI 接入deepSeek大模型(喂饭版)
java·spring boot·ai编程
脱胎换骨-军哥6 小时前
C++/Rust无缝互操作:混合系统新常态
开发语言·c++·rust
观山岳五楼6 小时前
Ubuntu 24 怎么使用Ubuntu 20 的镜像源
linux·运维·ubuntu
星夜夏空996 小时前
网络编程(1)
服务器·网络
川石课堂软件测试6 小时前
安全测试|常见SQL注入攻击方式、实例及预防
服务器·数据库·sql·功能测试·测试工具·安全·单元测试