线程栈与TLS和线程互斥

作者:keyipatience日期:2026/7/25

线程栈

主线程(进程 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
2// 参数1 线程tid,参数2 字符串名字
3int pthread_setname_np(pthread_t thread, const char *name);

获取线程名:

1char buf[16];
2pthread_getname_np(pthread_t tid, buf, sizeof(buf));

pthread.hpp:

1#pragma once
2#include <iostream>
3#include <pthread.h>
4#include <unistd.h>
5#include <functional>
6#include <string>
7#include <errno.h>
8#include <cstring>
9#include<cstdint>
10using namespace std;
11namespace ThreadModlue
12{
13    static uint32_t number = 1;//bug多个线程同时构造时,number++存在数据竞争,会出现重名线程
14    //定义在 <cstdint>不用int是两点:1. 线程编号只能是正数
15    //2. uint32_t固定 32 位,跨平台不会溢出
16    using func_t = function<void()>;
17
18    class Thread
19    {
20    private:
21        void EnableRunning()
22        {
23            cout<<"_isrunning=true"<<endl;
24            _isrunning = true;
25        }
26        void EnableDetach()
27        {
28            cout << "_isdetach=true" << endl;
29            _isdetach = true;
30        }
31          static void *Routine(void *args)
32        {
33            Thread *self = static_cast<Thread *>(args);
34            self->EnableRunning();
35            self->Detach();
36            pthread_setname_np(self->_tid, self->_name.c_str());
37            self->_func(); // 回调处理
38            return nullptr;
39        }
40        //C++ 普通成员函数隐藏第一个参数 this,函数真实原型,所以Routine真实原型
41        //void Routine(Thread* this, void* args);与pthread_create 对线程入口函数的强制要求
42        //void* (*start_routine)(void*);只接收单个 void * 参数,不能多参数。有冲突所以加static,
43        //static成员函数没有隐含this指针
44
45    public:
46        Thread(func_t func)
47            : _isdetach(false), _isrunning(false), _tid(0), _func(func)
48        {
49            _name = "thread-" + std::to_string(number++);
50        }
51        void Detach()
52        {
53            if (_isrunning && _isdetach)
54            {
55                cout<<"调用了phread_detach,线程分离了"<<endl;
56                pthread_detach(_tid);
57            }
58            else if (_isdetach == false)
59                EnableDetach();
60        }
61      
62        bool Start()
63        {
64            if (_isrunning)
65                return false;
66            int n = pthread_create(&_tid, nullptr, Routine, this);
67            if (n != 0)
68            {
69                std::cerr << "create thread error: " << strerror(n) << std::endl;
70                return false;
71            }
72            else
73            {
74                std::cout << _name << " create success" << std::endl;
75                return true;
76            }
77        }
78        bool Stop()
79        {
80            if (_isrunning)
81            {
82                int n = pthread_cancel(_tid);
83                if (n != 0)
84                {
85                    std::cerr << "stop thread error: " << strerror(n) << std::endl;
86                    return false;
87                }
88                else
89                {
90                    _isrunning = false;
91                    std::cout << _name << " stop" << std::endl;
92                    return true;
93                }
94            }
95            return false;
96        }
97        bool Join()
98        {
99            if (_isdetach)
100            {
101                return false;
102            }
103            int n = pthread_join(_tid, nullptr);
104            if (n != 0)
105            {
106                std::cerr << "join thread error: " << strerror(n) << std::endl;
107                return false;
108            }
109            else
110            {
111                std::cout << _name << " join" << std::endl;
112                return true;
113            }
114        }
115        ~Thread()
116        {
117        }
118
119    private:
120        pthread_t _tid;
121        bool _isrunning;
122        bool _isdetach;
123        func_t _func;
124        string _name;
125    };
126
127};

main.cc:

1#include "pthread.hpp"
2#include <unistd.h>
3#include <vector>
4using namespace ThreadModlue;
5void func()
6{
7    while (1)
8    {
9        char name[128];
10        pthread_getname_np(pthread_self(), name, sizeof(name));
11        std::cout << "我是一个新线程: " << name << std::endl; // 我的线程的名字是什么呀?debug
12        sleep(1);
13    }
14}
15int main()
16{
17    Thread t(func);
18  
19    t.Detach();
20    t.Start();
21    sleep(5);
22
23    t.Stop();
24
25    sleep(5);
26
27    t.Join();
28
29    return 0;
30}

运行结果:

线程局部存储(TLS)

1#include <iostream>
2#include <pthread.h>
3#include <unistd.h>
4#include <cstring>
5#include <stdio.h>
6using namespace std;
7static int count = 0;
8string Addr(int &c)
9{
10    char addr[64];
11    snprintf(addr, sizeof(addr), "%p", &c);
12    return addr;
13}
14void *routine1(void *args)
15{
16    (void)args;
17    while (1)
18    {
19        cout << "thread-1,count=" << count << "[我来修改count],"
20             << "&count: " << Addr(count) << endl;
21        count++;
22        sleep(1);
23    }
24}
25void *routine2(void *args)
26{
27    (void)args;
28    while (1)
29    {
30        cout << "thread-2,count=" << count
31             << "&count: " << Addr(count) << endl;
32        count++;
33        sleep(1);
34    }
35}
36int main()
37{
38    pthread_t tid1, tid2;
39    pthread_create(&tid1, nullptr, routine1, nullptr);
40    pthread_create(&tid2, nullptr, routine2, nullptr);
41
42    pthread_join(tid1, nullptr);
43    pthread_join(tid2, nullptr);
44
45    return 0;
46}

运行结果:

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

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

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

线程互斥

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

• 共享资源

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

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

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

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

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

1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h>
5#include <pthread.h>
6int ticket = 100;
7void *route(void *arg)
8{
9    char *id = (char *)arg;
10    while (1)
11    {
12        if (ticket > 0)
13        {
14            usleep(1000);
15            printf("%s sells ticket:%d\n", id, ticket);
16            ticket--;
17        }
18        else
19        {
20            break;
21        }
22    }
23    return nullptr;
24}
25int main()
26{
27    pthread_t t1, t2, t3, t4;
28    pthread_create(&t1, NULL, route, (void *)"thread 1");
29    pthread_create(&t2, NULL, route, (void *)"thread 2");
30    pthread_create(&t3, NULL, route, (void *)"thread 3");
31    pthread_create(&t4, NULL, route, (void *)"thread 4");
32    pthread_join(t1, NULL);
33    pthread_join(t2, NULL);
34    pthread_join(t3, NULL);
35    pthread_join(t4, NULL);
36}
37

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

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

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

10xFF00  mov ebx, ticket    // 1. 从内存把ticket载入CPU寄存器ebx
20xFF02  sub ebx, 1         // 2. CPU运算:寄存器内数值-1
30xFF04  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);

  • attr 传 NULL 使用默认锁属性;
  • 使用完毕必须调用 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:全局锁

1int ticket = 100;
2pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 方法1静态全局初始化
3
4void *route(void *arg)
5{
6    char *id = (char *)arg;
7    while (1)
8    {
9       pthread_mutex_lock(&mutex);
10        if (ticket > 0)
11        {
12            usleep(1000);
13            printf("%s sells ticket:%d\n", id, ticket);
14            ticket--;
15            pthread_mutex_unlock(&mutex);
16        }
17        else
18        {
19            pthread_mutex_unlock(&mutex);
20            break;
21        }
22    }
23    return nullptr;
24}

方式2:局部锁

1// 全局共享票数
2int ticket = 100;
3
4void *route(void *arg)
5{
6    char *id = (char*)arg;
7    // 1. 定义局部互斥锁(栈上局部变量)
8    pthread_mutex_t mutex;
9    // 2. 动态初始化锁,attr传NULL使用默认属性
10    pthread_mutex_init(&mutex, NULL);
11
12    while (1) {
13        // 加锁进入临界区
14        pthread_mutex_lock(&mutex);
15
16        if (ticket > 0) {
17            usleep(1000);
18            printf("%s sells ticket:%d\n", id, ticket);
19            ticket--;
20            // 分支内解锁
21            pthread_mutex_unlock(&mutex);
22        } else {
23            // 无票也要先解锁再退出循环
24            pthread_mutex_unlock(&mutex);
25            break;
26        }
27    }
28
29    // 3. 销毁局部锁,释放内核资源
30    pthread_mutex_destroy(&mutex);
31    return NULL;
32}

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

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

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

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

怎么解决呢?

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

1void *route(void *arg)
2{
3    char *id = (char *)arg;
4    while (1)
5    {
6        pthread_mutex_lock(&mutex);
7        if (ticket > 0)
8        {
9            usleep(1000);
10            printf("%s sells ticket:%d\n", id, ticket);
11            ticket--;
12            pthread_mutex_unlock(&mutex);
13        }
14        else
15        {
16            pthread_mutex_unlock(&mutex);
17            break;
18        }
19        // 新增:解锁后休眠,给其他线程调度时间
20         usleep(1000);
21     
22    }
23    return nullptr;
24}

这次的运行结果:

c++锁

c++自己也有锁

1#include<mutex>
2int ticket = 100;
3mutex cpp_lock;
4void *route(void *arg)
5{
6    char *id = (char *)arg;
7    while (1)
8    {
9        cpp_lock.lock();
10        if (ticket > 0)
11        {
12            usleep(1000);
13            printf("%s sells ticket:%d\n", id, ticket);
14
15            ticket--;
16          cpp_lock.unlock();
17        }
18        else
19        {
20            cpp_lock.unlock();
21            break;
22        }
23    }
24    return nullptr;
25}
26

基于RAII风格的互斥锁

test.cc:

1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h>
5#include <pthread.h>
6#include "Mutex.hpp"
7using namespace std;
8using namespace MutexModule;
9int ticket = 100;
10class ThreadData
11{
12public:
13    ThreadData(const string &name, Mutex &lock)
14        : _name(name), _lockp(lock)
15    {
16    }
17    ~ThreadData()
18    {
19    }
20
21
22    string _name;
23    Mutex &_lockp;//为什么是引用?
24};
25//函数形参写 &:只是接收外部对象时不拷贝;
26//类内部成员没写 &:初始化列表会执行拷贝,生成独立副本;
27//想要全程共用同一把锁,构造参数 和 类成员 必须同时是引用 / 指针,只改一边没用。
28
29void *route(void *arg)
30{
31    ThreadData *td = static_cast<ThreadData *>(arg);
32    while (1)
33    {
34      { LockGuard guard(td->_lockp);
35        if (ticket > 0)
36        {
37            usleep(1000);
38            printf("%s sells ticket:%d\n", td->_name.c_str(), ticket);
39            ticket--;
40         
41        }
42        else
43        {
44           
45            break;
46        }
47      }
48        usleep(1000);
49    }
50    return nullptr;
51}
52int main()
53{
54
55    Mutex lock;
56    pthread_t t1, t2, t3, t4;
57    ThreadData *td1 = new ThreadData("thread 1", lock);
58    pthread_create(&t1, NULL, route, td1);
59
60    ThreadData *td2 = new ThreadData("thread 2", lock);
61    pthread_create(&t2, NULL, route, td2);
62
63    ThreadData *td3 = new ThreadData("thread 3", lock);
64    pthread_create(&t3, NULL, route, td3);
65
66    ThreadData *td4 = new ThreadData("thread 4", lock);
67    pthread_create(&t4, NULL, route, td4);
68
69    pthread_join(t1, NULL);
70    pthread_join(t2, NULL);
71    pthread_join(t3, NULL);
72    pthread_join(t4, NULL);
73
74    delete td1,td2,td3,td4;
75
76
77    return 0;
78}
79

Mutex.hpp:

1#pragma once
2#include <iostream>
3#include <pthread.h>
4namespace MutexModule
5{
6    //第一层 Mutex 类:封装底层 C 锁 API
7    class Mutex
8    {
9    public:
10        Mutex()
11        {
12            pthread_mutex_init(&_mutex, nullptr);
13        }
14        void Lock()
15        {
16            int n = pthread_mutex_lock(&_mutex);
17            (void)n;
18        }
19        void Unlock()
20        {
21            int n = pthread_mutex_unlock(&_mutex);
22            (void)n;
23        }
24        ~Mutex()
25        {
26            pthread_mutex_destroy(&_mutex);
27        }
28
29    private:
30        pthread_mutex_t _mutex;
31    };
32    class LockGuard
33    {
34        public:
35        LockGuard(Mutex &mutex):_mutex(mutex)//这里&但是!这只作用在函数内部临时变量 mutex,不影响类成员_mutex
36        //比如mutex++,等等确实是引用,会影响外面传入的锁,但是_mutex(mutex)如果参数没有引用,就会发生值拷贝
37        {
38            _mutex.Lock();
39        }
40         ~LockGuard()
41        {
42            _mutex.Unlock();
43        }
44
45
46        private:
47        Mutex &_mutex;//为什么是引用?
48
49    };
50
51
52}

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,谁就占有锁资源。


《线程栈与TLS和线程互斥》 是转载文章,点击查看原文。


相关推荐


如何用 AI 协助解决陌生技术问题:拆解-分析-熟悉-解决四步法
dozenyaoyida2026/7/17

你接过陌生项目吗?那种打开 IDE,几千个文件铺开,光看目录名就头大,盯着屏幕两小时一行代码没写的感觉。 或者更常见的,线上突然报了个错,涉及一个你从没读过的模块,老板在群里 @ 你,你点开文件,密密麻麻的调用链,不知道从哪开始查。 我做了十年开发,最近两年重度用 AI 辅助。最大的体会是,面对陌生问题,卡住你的从来不是"难",是"乱"。你不知道从哪下手,不知道自己不知道什么,于是在原地打转。 下面这套方法我用了几十次,核心就四个字:拆解、分析、熟悉、解决。AI 在每个阶段扮演的角色不一样,你介


【从零开始大模型开发与微调:基于PyTorch与ChatGLM】(基于PyTorch卷积层的MNIST分类实战:从卷积直觉到高效卷积设计)
承渊政道2026/7/9

🔥承渊政道:个人主页 ❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》 ✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介: 前面使用多层感知机完成了MNIST分类实战的演示.多层感知机是一种对目标数据进行整体分类的计算方法.虽然从演示效果来看,多层感知机可以较好地完成项目


开源「仓颉.Skill」2.0,你现在可以蒸馏任何视频!
AI袋鼠帝2026/7/1

大家好,我是袋鼠帝。 没想到cangjie-skill在4月开源,中间没怎么推,两个月还慢慢涨到了1.3K Star,有点出乎我的意料。 而且现在每天都还在增涨,感谢大家支持~ github.com/kangarookin… 说明大家对蒸馏书是有需求的(可以理解为人工智能拆书)。 也并不是像评论区一些人说的:“所有书AI都学过了,你这个是脱了裤子放屁。”那样不堪。 对一些大众非常熟悉的书,可能不太需要这个方式来蒸馏。但是有很多比较小众的书,AI不一定记得清楚,甚至还有很多新书是AI没有训练的。


AI Agent(六)- Dify 自定义工具实战 - 基于百度天气 API 搭建天气查询 Agent(天气智查助手)
BigDataMagician2026/6/22

文章目录 一、前言二、整体实现思路三、申请百度地图开放平台 AK1. 注册百度地图开放平台2. 登录百度地图开放平台3. 创建应用并获取AK4. 查看国内天气查询接口开发文档5. 接口测试 四、创建自定义工具1. OpenAPI 规范配置内容及说明1.1 OpenAPI 规范配置内容1.2 OpenAPI 规范配置说明 2. 配置OpenAPI 规范3. 工具测试 六、搭建天气智查助手Agent1. 创建Agent2. System Prompt(系统提示词)3. 调用工具4.


MyBatis魔法堂:结果集映射
独泪了无痕2026/6/14

一、ResultMap 的定义   在当今的软件开发领域,MyBatis 作为一款优秀的持久层框架,以其简洁的配置和强大的功能,深受广大开发者的喜爱。然而,在实际的项目开发中,我们常常会遇到数据模型与数据库表结构不一致的情况,这时就需要 MyBatis 的 resultMap 功能来帮助我们实现复杂的映射关系。想象一下,一个典型的业务场景:一个电商系统中的订单表,其字段包括订单ID、用户ID、商品ID、订单金额等。然而,在业务逻辑层,我们可能需要将订单信息与对应的用户信息和商品信息结合起来,以便


不用 Mac 也可以 Windows下管理iOS描述文件的非Xcode完整指南
程序员不说人话2026/6/7

很多开发者第一次接触 iOS 描述文件(Provisioning Profile)时,看到的教程基本都围绕 Xcode 和钥匙串。 但实际开发里,有一类项目并不是在 Mac 上完成的、uni-app、Flutter、React Native、HBuilderX 云打包、Windows 开发环境,这时问题会变成.mobileprovision 文件到底怎么管理? 尤其项目一多之后,开发者会开始遇到 描述文件和证书不匹配、Bundle ID 混乱、测试设备漏加、文件过期后无法安装、不同电脑之间无法同


栗子前端技术周刊第 131 期 - pnpm 11.3、npm 11.16.0、Astro 6.4...
晓得迷路了2026/6/1

🌰栗子前端技术周刊第 131 期 (2026.05.25 - 2026.05.31):浏览前端一周最新消息,学习国内外优秀文章,让我们保持对前端的好奇心。 📰 技术资讯 pnpm 11.3:pnpm 11.3 版本更新,新增阶段性发布命令 pnpm stage、用于管控信任策略生效规则的 trustLockfile 配置,同时原生支持 pkg、repo、set-script 等命令,以及多项其他功能。 npm 11.16.0:npm 11.16.0 已正式发布,该版本初步支持可自主选


MySQL视图
Halvmån2026/5/10

我们上一篇博客也讲到了视图,但是我们今天要学的这个视图并不是上篇博客的视图。 在日常数据库开发中,我们经常遇到这样的需求:多个业务模块需要查询同一份数据,但每个模块关注的字段不同;或者某些敏感字段需要隐藏,不能让所有用户都看到。这时候,视图(View) 就成了一个非常优雅的解决方案。 很多人刚开始接触视图时,会觉得它像一个“虚拟表”或者“保存好的查询语句”。本文将从实际开发的角度,带你全面掌握 MySQL 视图的使用。 一、什么是视图? 视图是一个虚拟表,它不存储实际数据,而是存储一条 


Linux 线程同步与互斥(六) 线程安全与重入问题,死锁,线程done
codeacac2026/4/30

目录 一、线程安全与重入问题 概念 线程安全 重入 多线程重入函数 信号导致的重入 可重入与线程安全的联系 可重入与线程安全的区别 二、死锁 概念 造成死锁的4个必要条件 避免死锁的做法: 三、STL, 智能指针和线程安全 四、总结 一、线程安全与重入问题 概念 线程安全 线程安全就是当多个线程同时访问同一块资源(如全局变量、任务队列、打印终端)时,最终结果能符合预期,不会出现数据错乱、逻辑错误,这就是线程安全。 我们可以结合上一篇线程池的代码


把 Git 提交历史变成一条流动的河——Project River
仿生狮子2026/4/22

是什么 你有没有好奇过一个开源项目十年的贡献者活动长什么样?谁一直在写代码?谁是后来加入的?版本大升级时社区发生了什么变化? 我做了 Project River,一个 Git 历史可视化工具——输入一个 Git 仓库,能把每位贡献者的提交活动渲染成随时间流动的河流图(Streamgraph)。 项目地址:github.com/Lionad-Moro… 在线体验:lionad-morotar.github.io/project-riv… 直接看效果: 河流越宽,说明当天的提交越多。每条色带

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读