Linux多线程线程控制

发布于:2025-08-01 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

1.线程知识补充

1.1 线程私有资源

1.2 线程共享资源

1.3 原生线程库

2、线程控制接口

2.1 线程创建

2.1.1 一批线程

2.2 线程等待

2.3 线程终止

 2.4 线程实战

2.5 其他接口

2.5.1 关闭线程pthread_cancel

2.5.2 获取线程 ID pthread_self

2.5.3 线pthread_detach

3. 深入理解线程

3.1 理解线程库及线程 ID

3.2 理解线程独立栈

3.3 理解线程局部存储

🌇前言
线程是进程内的基本执行单位,作为 CPU 执行的基本单位,线程的控制与任务执行效率息息相关。合理地进行线程管理,能大大提升程序的执行效率,掌握线程的基本操作至关重要。

🏙️正文

1.线程知识补充

在深入讨论线程控制接口之前,我们首先需要补充一些关于线程的基础知识。Linux 中没有真线程,只有复用 PCB 设计思想的 TCB 结构

1.1 线程私有资源

在 Linux 的多线程实现中,线程本质上是轻量级进程(LWP),即通过复用 PCB 设计的 TCB 结构来模拟线程因此,尽管 Linux 系统中的多个线程共享同一进程的地址空间,但每个线程仍然需要一定的独立性和资源

线程私有资源具体包括:

  • 线程 ID:线程的唯一标识符,由内核管理

  • 寄存器:每个线程的上下文信息,如寄存器,线程切换时需要保存这些信息

  • 独立栈:每个线程都有独立的栈空间,用于存储局部变量和执行上下文

  • 错误码(errno):线程异常退出时,通过错误码反馈信息

  • 信号屏蔽字:各个线程对于信号的屏蔽字设置不同,确保每个线程能根据需要对信号做出响应

  • 调度优先级:线程也需要被调度,调度算法根据优先级来合理分配执行时间

其中,寄存器和独立栈是线程最关键的私有资源,它们保障了线程切换的独立性以及运行时的稳定性。

1.2 线程共享资源

除了线程的私有资源,多线程还会共享进程的部分资源。线程共享资源不需要额外的开销,并能在各个线程间随时访问。

共享的定义不需要太多的额外成本,就可以实现随时访问资源

基于 多线程看到的是同一块进程地址空间,理论上 凡是在进程地址空间中出现的资源,多线程都是可以看到的

但实际上为了确保线程调度、运行时的独立性只能共享部分资源

在 Linux 中,共享资源包括:

  • 共享区、全局数据区、字符常量区、代码区:这些区域是进程中天然支持共享的资源。

  • 文件描述符表:在多线程中进行 I/O 操作时,无需每个线程都重新打开文件,文件描述符表在多个线程间共享。

  • 信号处理方式:所有线程共同构成一个整体,信号处理必须统一。

  • 当前工作目录:所有线程共享进程的工作目录。

  • 用户 ID 和组 ID:进程属于特定的用户和组,线程也继承这些身份。

文件描述符表是多线程共享资源中最重要的部分,它确保了多线程 I/O 操作的高效性和协作性。

1.3 原生线程库

当我们编译多线程相关代码时,通常需要添加 -lpthread 参数确保能够使用 pthread 原生线程库。

这是因为,在 Linux 中并没有真正意义上的线程,而是通过轻量级进程(LWP)来模拟线程的实现。Linux 系统并不会直接提供线程控制接口,而是通过封装轻量级进程相关操作,提供了线程控制的接口。

为了使用户能够方便地操作线程,Linux 提供了 pthread,这是一个标准的线程库,也是个第三方库,被存放在了系统及库路径下。封装了操作系统底层的轻量级进程控制接口。用户只需在编译时添加 -lpthread 参数(告诉库名),即可正常使用线程相关接口。

计算机哲学的体现:通过增加一层软件抽象来简化复杂度,解决操作系统对线程支持的不足。

 在 Linux 中,封装轻量级进程操作相关接口的库称为 pthread 库,即 原生线程库,这个库文件是所有 Linux 系统都必须预载的,用户使用多线程控制相关接口时,只需要指明使用 -lpthread 库,即可正常使用多线程控制相关接口


2、线程控制接口

2.1 线程创建

要想控制线程,得先创建线程。对于原生线程库来说,创建线程使用的是 pthread_create 这个接口。

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

参数详解:

  • 参数1 pthread_t* 线程 ID,用于标识线程,本质上它是一个 unsigned long int 类型。
    注: pthread_t* 表示这是一个输出型参数,用于在创建线程后获取新线程的 ID。

  • 参数2 const pthread_attr_t* 用于设置线程的属性,如优先级、状态、私有栈大小等。通常不需要特别处理,传递 nullptr 使用默认设置即可。

  • 参数3 void *(start_routine) (void ): 这是一个非常重要的参数,类型为返回值为 void*、参数也为 void* 的函数指针。线程启动时会自动回调此函数(类似于 signal 函数中的参数2)。

  • 参数4 void* 显然,这个类型与回调函数中的参数类型相匹配,它是线程运行时传递给回调函数的参数。

返回值:
成功返回 0,失败返回错误号。错误检查:

传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误

pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回

pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

理解了创建线程函数的各个参数后,就可以尝试创建一个线程了:

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void* threadRun(void *arg) {
    while(true) {
        cout << "我是次线程,我正在运行..." << endl;
        sleep(1);
    }
    return nullptr;
}

int main() {
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, nullptr);

    while(true) {
        cout << "我是主线程 " << " 我创建了一个次线程 " << t << endl;
        sleep(1);
    }

    return 0;
}

这段代码非常简单。如果直接编译,可能会引发报错:

错误信息: 未定义 pthread_create 这个函数

原因: 没有指定使用原生线程库,解决方法是在编译时添加 -lpthread 来链接线程库。

验证原生线程库是否存在:
你可以通过 ldd 命令查看已编译程序的库链接情况。例如,使用 ldd mythread 命令来查看是否成功链接到原生线程库。

  • ps -al 命令中的 LWP 是内核中线程的 ID,也可以看作是线程在进程中的唯一标识符。

  • 用户层的 pthread_t是用户空间的线程标识符,表示线程控制块(TCB)的地址它与 LWP ID 是映射的。

程序运行时主线程和次线程的顺序如何?
线程的执行顺序由操作系统的调度器决定。多线程程序中的主线程和次线程执行顺序不确定,具体执行顺序依赖于调度器的调度策略。


2.1.1 一批线程

接下来我们演示如何创建一批线程。

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 5

void* threadRun(void *name) {
    while(true) {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);
    }
    return nullptr;
}

int main() {
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++) {
        // 注册新线程的信息
        char name[64];
        snprintf(name, sizeof(name), "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    while(true) {
        cout << "我是主线程,我正在运行" << endl;
        sleep(1);
    }

    return 0;
}

细节:
在传递 pthread_create 的参数时,可以通过 起始地址+偏移量 的方式进行传递,这样每个线程就能接收到不同的参数信息。

预期结果: 打印出 thread-1thread-2thread-3 等。

实际结果: 五个次线程在运行,但打印出来的都是 thread-5

原因: char name[64] 是主线程栈区中的局部变量,多个线程共享这块空间,最后一次的覆盖导致每个线程都读取到相同的数据。
解决方法: 在堆区动态分配空间,为每个线程分配独立的内存区域,以确保信息的独立性。

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 5

void* threadRun(void *name) {
    while(true) {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);
    }
    delete[] (char*)name;
    return nullptr;
}

int main() {
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++) {
        // 注册新线程的信息
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    while(true) {
        cout << "我是主线程,我正在运行" << endl;
        sleep(1);
    }

    return 0;
}

 通过这种方式,程序运行将符合预期,每个线程都会打印出自己独立的名称。


2.2 线程等待

 线程等待 为什么需要线程等待?

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内

创建新的线程不会复用刚才退出线程的地址空间

主线程需要等待次线程。在原生线程库中,提供 pthread_join 来等待一个线程的运行结束。

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

参数说明:

  • 参数1 pthread_t: 待等待的线程 ID,本质上是一个无符号长整型类型。

  • 参数2 void:这是一个输出型参数,用于获取次线程的退出结果。如果不关心返回值,可以传递 nullptr

返回值: 成功返回 0,失败返回错误号。

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 5

void* threadRun(void *name) {
    while(true) {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);
    }
    delete[] (char*)name;
    return nullptr;
}

int main() {
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++) {
        // 注册新线程的信息
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    // 等待次线程运行结束
    for(int i = 0; i < NUM; i++) {
        int ret = pthread_join(pt[i], nullptr);
        if(ret != 0)
            cerr << "等待线程 " << pt[i] << " 失败!" << endl;
    }

    cout << "所有线程都退出了" << endl;

    return 0;
}

该程序确保了主线程在等待所有次线程结束后才会退出,确保了线程的正常结束。

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。

2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。

3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数 


2.3 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。(主线程return 0 要开始合理使用了)

2. 线程可以调用pthread_ exit终止自己。

3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit函数

功能:线程终止原型

void pthread_exit(void *value_ptr);

参数

value_ptr:value_ptr不要指向一个局部变量。

返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
pthread_join 中的 void **retval 是一个输出型参数,可以把一个 void * 指针的地址传递给 pthread_join 函数,当线程调用 pthread_exit 退出时,可以根据此地址对 retval 赋值,从而起到将退出信息返回给主线程的作用

 

为什么 pthread_join 中的参数2类型为 void**?

因为主线程和次线程此时并不在同一个栈帧中,要想远程修改值就得传地址,类似于 int -> &int,不过这里的 retval 类型是 void*
注意: 直接在 回调方法 中 return 退出信息,主线程中的 retval 也是可以得到信息的,因为类型都是 void*,彼此相互呼应

 

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 5

void* threadRun(void *name)
{
    cout << "我是次线程 " << (char*)name << endl;
    sleep(1);

    delete[] (char*)name;

    pthread_exit((void*)"EXIT");

    // 直接return "EXIT" 也是可以的
    // return (void*)"EXIT";
}

int main()
{
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++)
    {
        // 注册新线程的信息
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    // 等待次线程运行结束
    void *retval = nullptr;
    for(int i = 0; i < NUM; i++)
    {
        int ret = pthread_join(pt[i], &retval);
        if(ret != 0)
            cerr << "等待线程 " << pt[i] << " 失败!" << endl;
        cout << "线程 " << pt[i] << " 等待成功,退出信息是 " << (const char*)retval << endl;
    }

    cout << "所有线程都退出了" << endl;

    return 0;
}

void* 非常之强大,可以指向任意类型的数据,甚至是一个对象

 2.4 线程实战

无论是 pthread_create 还是 pthread_join,它们的参数都有一个共同点:包含了一个 void* 类型的参数。这意味着我们可以通过传递对象指针给线程,并在其中执行某些特定任务处理。

我们首先创建一个线程信息类,用于计算从 0N 的累加和。线程信息包括:

  • 线程名字(包括 ID)

  • 线程编号

  • 线程创建时间

  • 待计算的值 N

  • 计算结果

  • 状态

为了方便访问成员,权限设置public

// 线程信息类的状态
enum class Status
{
    OK = 0,
    ERROR
};

// 线程信息类
class ThreadData
{
public:
    ThreadData(const string &name, int id, int n)
        :_name(name), _id(id), _createTime(time(nullptr)), _n(n), _result(0), _status(Status::OK) {}

public:
    string _name;
    int _id;
    time_t _createTime;
    int _n;
    int _result;
    Status _status;
};

此时就可以编写回调方法中的业务逻辑了:

void* threadRun(void *arg)
{
    ThreadData *td = static_cast<ThreadData*>(arg);

    // 业务处理
    for(int i = 0; i <= td->_n; i++)
        td->_result += i;
    
    // 如果业务处理过程中出现异常,可以设置 _status 为 ERROR
    cout << "线程 " << td->_name << " ID " << td->_id << " CreateTime " << td->_createTime << " 完成..." << endl;

    pthread_exit((void*)td);
}

主线程在创建线程及等待线程时,使用 ThreadData 对象。在后续修改业务逻辑时,只需修改类及回调方法,而不需要更改创建及等待逻辑,这有效地做到了逻辑解耦。

int main()
{
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++)
    {
        // 注册新线程的信息
        char name[64];
        snprintf(name, sizeof(name), "thread-%d", i + 1);

        // 创建对象
        ThreadData *td = new ThreadData(name, i, 100 * (10 + i));
        pthread_create(pt + i, nullptr, threadRun, td);
        sleep(1); // 尽量拉开线程创建时间
    }

    // 等待次线程运行结束
    void *retval = nullptr;
    for(int i = 0; i < NUM; i++)
    {
        int ret = pthread_join(pt[i], &retval);
        if(ret != 0)
            cerr << "等待线程 " << pt[i] << " 失败!" << endl;

        ThreadData *td = static_cast<ThreadData*>(retval);

        if(td->_status == Status::OK)
            cout << "线程 " << pt[i] << " 计算 [0, " << td->_n << "] 的累加和结果为 " << td->_result << endl;
        delete td;
    }

    cout << "所有线程都退出了" << endl;

    return 0;
}

程序运行时,各个线程能够正确计算累加和。此示例展示了线程如何利用传递的对象指针进行任务处理。线程不仅可以用于计算,还可以扩展到其他领域,如网络传输、密集型计算、多路 I/O 等,关键在于修改业务逻辑。

2.5 其他接口

与多线程相关的还有一批简单但重要的接口,我们将一并介绍。

2.5.1 关闭线程pthread_cancel

线程不仅可以被创建,还可以被关闭。我们可以使用 pthread_cancel 来关闭已经创建并正在运行的线程。

#include <pthread.h> int pthread_cancel(pthread_t thread);

参数说明:
pthread_t thread:表示被关闭的线程 ID。
返回值:
成功返回 0,失败返回错误号。

该函数使用成功后,线程会被异常信号杀死,退出码为PTHREAD_CANCELED(-1), pthread_join()函数会等待成功,回收资源一样成功,与pthread_detach.

detach是明确表明推出资源直接交由操作系统直接释放,一种不关心的状态,如果在用pthread_join等待的话,资源已经没有,所以会等待失败。

#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void *threadRun(void *arg)
{
    const char *ps = static_cast<const char*>(arg);

    while(true)
    {
        cout << "线程 " << ps << " 正在运行" << endl;
        sleep(1);
    }

    pthread_exit((void*)10);
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, (void*)"Hello Thread");

    // 3秒后关闭线程
    sleep(3);

    pthread_cancel(t);

    void *retval = nullptr;
    pthread_join(t, &retval);

    cout << "线程 " << t << " 已退出,退出信息为 " << (int64_t)retval << endl;
    return 0;
}

运行结果:
程序运行 3 秒后,可以看到退出信息为 -1,这是因为 pthread_cancel 关闭的线程,其退出信息统一为 PTHREAD_CANCELED-1


2.5.2 获取线程 ID pthread_self

线程 ID 是线程的唯一标识符,我们可以通过 pthread_self 获取当前线程的 ID。

#include <pthread.h> pthread_t pthread_self(void); 
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void *threadRun(void *arg)
{
    cout << "当前次线程的ID为 " << pthread_self() << endl;
    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, nullptr);

    pthread_join(t, nullptr);
    cout << "创建的次线程ID为 " << t << endl;

    return 0;
}

结果:
pthread_self 返回当前线程的 ID,而 t 显示的是主线程创建时的线程 ID。


2.5.3 线pthread_detach

父进程需要阻塞式等待子进程退出,主线程等待次线程时也是阻塞式等待。如果希望避免一直阻塞,我们可以使用线程分离。

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

注意: 如果线程失去了 joinable 属性,就无法被 join,如果 join 就会报错

#include <pthread.h> 
int pthread_detach(pthread_t thread);

参数说明:
pthread_t thread:待分离的线程 ID。
返回值:
成功返回 0,失败返回错误号。

#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void *threadRun(void *arg)
{
    int n = 3;
    while(n)
    {
        cout << "次线程 " << n-- << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, nullptr);

    pthread_detach(t);

    int n = 5;
    while(n)
    {
        cout << "主线程 " << n-- << endl;
        sleep(1);
    }

    return 0;
}

运行结果:
主线程和次线程并发执行,不需要担心次线程的退出会导致主线程阻塞。

 


3. 深入理解线程

3.1 理解线程库及线程 ID

在见识过原生线程库提供的一批便利接口后,我们不禁感叹库的强大。那么,这样一个强大的库究竟是如何工作的呢?

原生线程库本质上是一个存储/lib64 目录下的动态库,想要使用这个库,在编译时必须加上 -lpthread 来指定链接动态库。

程序运行时,原生线程库需要从磁盘加载到内存中,并通过进程地址空间映射到共享区供线程使用。

由于用户并不会直接操作轻量级进程的接口,因此需要借助第三方库进行封装,就像用户可能不了解操作系统提供的文件接口一样,而使用 C 语言封装的 FILE 库。

对于原生线程库来说,线程不仅仅是一个,而是多个。因此,在线程库中创建 TCB(线程控制块)结构,类似于进程的 PCB(进程控制块),其中存储线程的各种信息,例如线程独立栈信息等。

在内存中,整个线程库就像一个“数组”,每一块空间存储了 TCB 信息,每个 TCB 的起始地址就表示当前线程的 ID。由于地址是唯一的,因此线程 ID也是唯一的。LWP ID 是内核为每个线程分配的唯一标识符,线程在内核中的标识。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

using namespace std;

string toHex(pthread_t t)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", t);
    return id;
}

void *threadRun(void *arg)
{
    cout << "我是[次线程],我的ID是 " << toHex(pthread_self()) << endl;

    return (void*)0;
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, nullptr);

    pthread_join(t, nullptr);

    cout << "我是[主线程],我的ID是 " << toHex(pthread_self()) << endl;


    return 0;
}

我们之前打印 pthread_t 类型的线程 ID 时,实际打印的就是地址,不过它是以十进制显示的。我们可以通过一个函数将其转换为十六进制显示:

运行结果:
线程 ID 确实能转换为地址(虚拟进程地址空间上的地址)。

注意: 即便是 C++11 提供的 thread 线程库,在 Linux 平台中运行时,也需要带上 -lpthread 选项,因为它本质上是对原生线程库的封装。


3.2 理解线程独立栈

线程之间存在独立栈,保证它们在执行任务时不会相互干扰。我们可以通过以下代码来验证这一点:

多个线程使用同一个入口函数,并打印其中临时变量的地址:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

using namespace std;

string toHex(pthread_t t)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", t);
    return id;
}

void *threadRun(void *arg)
{
    int tmp = 0;
    cout << "thread " << toHex(pthread_self()) << " &tmp: " << &tmp << endl;

    return (void*)0;
}

int main()
{
    pthread_t t[5];
    for(int i = 0; i < 5; i++)
    {
        pthread_create(t + i, nullptr, threadRun, nullptr);
        sleep(1);
    }

    for(int i = 0; i < 5; i++)
        pthread_join(t[i], nullptr);
    return 0;
}

运行结果:
可以看到,五个线程打印出的临时变量地址不相同,证明每个线程都有独立的栈空间。

为什么 CPU 能够区分这些栈结构呢?

答案是:通过栈顶指针 ebp 和栈底指针 esp 来进行切换。ebpesp 是 CPU 中两个非常重要的寄存器,即使是程序启动时,也需要借助这两个寄存器来为 main 函数开辟对应的栈区。

除了移动 esp 扩大栈区外,还可以同时移动 ebpesp 来更改当前栈区。因此,在多线程中,栈区的切换是通过这两个寄存器来完成的。


3.3 理解线程局部存储

线程之间共享全局变量,操作全局变量时会影响其他线程:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int g_val = 100;

string toHex(pthread_t t)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", t);
    return id;
}

void *threadRun(void *arg)
{
    cout << "thread: " << toHex(pthread_self()) << " g_val: " << ++g_val << " &g_val: " << &g_val << endl;
    return (void*)0;
}

int main()
{
    pthread_t t[3];
    for(int i = 0; i < 3; i++)
    {
        pthread_create(t + i, nullptr, threadRun, nullptr);
        sleep(1);
    }

    for(int i = 0; i < 3; i++)
        pthread_join(t[i], nullptr);
    return 0;
}

运行结果:
在三个线程的影响下,g_val 最终变成了 103

如果想让每个线程看到不同的全局变量可以使用 __thread 修饰符,这样全局变量就不再存储在全局数据区,而是存储到每个线程的局部存储区中

__thread int g_val = 100;

运行结果:
通过 __thread 修饰后,每个线程看到的 g_val 都是不同的,并且地址变大了。

解释:
“全局变量” 的地址变大是因为它不再存储在全局数据区,而是存储在线程的局部存储区中。线程的局部存储区位于共享区,并且共享区的地址天然大于全局数据区


网站公告

今日签到

点亮在社区的每一天
去签到