当前位置:   article > 正文

【Linux取经路】初识线程——线程控制

【Linux取经路】初识线程——线程控制

在这里插入图片描述

一、什么是线程?

线程是进程内的一个执行分支,线程的执行粒度,要比进程细。

1.1 Linux 中线程该如何理解?

地址空间是进程的资源窗口。Linux 中,线程在进程”内部“执行,即线程在进程的地址空间内运行,任何执行流要执行,都要有资源(代码,数据,CPU资源);在 Linux 中,线程的执行粒度要比进程更细,即线程执行进程代码的一部分;在 Linux 中,复用进程数据结构和管理算法来描述和组织线程Linux 中没有真正意义上的线程,即在 Linux 中没有为线程创建独属于自己的 TCB 结构体thread ctrl block) ,而是用”进程“的内核数据结构(PCB)模拟的线 程;CPU 只有执行流的概念,所以从原则上来说,CPU 是不区分进程和线程的,但是 Linux 操作系统要区分进程和线程;我们把 Linux 中的执行流叫做轻量级进程

线程:我们认为线程是操作系统调度的基本单位。

进程:进程是承担分配系统资源(线程(执行流资源)、地址空间、页表、物理内存)的基本实体。

1.2 如何理解把资源分配给线程?

1.2.1 虚拟地址到物理地址的转换

image-20240311181055099

站在地址空间角度,线程分配资源本质就是分派地址空间范围。

1.3 线程 VS 进程

1.3.1 线程为什么比进程更轻量化?

  • 创建释放更加轻量化,创建线程只需要常见 PCB 对象就行。(生死)
  • 切换更加轻量化(运行)。同一个进程内的多个线程在切换的时候,不需要更新 CPU 中的 cache 缓存、进程地址空间、页表等。只需要更新少量的上下文数据。

创建线程不能给该线程重新申请时间片,而是将线程的时间片划分部分给线程。

在这里插入图片描述

cat /proc/cpuinfo:查看 CPU 的信息。

1.3.2 线程的优点

  • 创建一个新线程的代价要比创建一个新进程小的多。

  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。

  • 线程占用的资源要比进程少很多。

  • 能充分利用多处理器的可并行数量。

  • 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务。

  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。

  • I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。

1.3.3 线程缺点

  • 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器,如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的新能损失(切换浪费时间),这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • 健壮性降低:编写多线程需要更全面深入的考虑,在一个线程程序里,因时间分配上的细微偏差或因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说就是线程之间缺乏安全保护。

  • 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响。

  • 编程难度提高:编写与调试一个多线程程序比单线程程序困难的多。

1.3.4 线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。

  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

1.3.5 线程用途

  • 合理的使用多线程,能提高 CPU 密集型程序的执行效率。

  • 合理的使用多线程,能提高 I/O 密集型程序的用户体验(如一边写代码一边下载开发工具,就是多线程运行的一种表现)

1.3.6 线程和进程

  • 进程是资源分配的基本单位。

  • 线程是调度的基本单位。

  • 线程共享进程数据,但也拥有自己的一部分数据:

    • 线程 ID

    • 一组寄存器(线程的上下文)

    • errno

    • 信号屏蔽子

    • 调度优先级

进程的多个线程之间共享同一地址空间,因此代码段、数据段都是共享的,如果定义一个函数,在各线程中都可以的调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各个线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_IGNSIG_DFL 或者自定义的信号处理函数)
  • 当前工作目录
  • 用户 id 和组 id

image-20240311215931222

二、线程控制

Linux 的内核中,没有很明确的线程概念,只有轻量级进程的概念。所以 Linux 操作系统不会给我们直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用。伟大的 Linux 程序员将轻量级进程的接口进行封装,给用户在应用层开发出来了一个 pthread 线程库。几乎所有的 Linux 平台都是默认自带这个库的,Linux 中编写多线程代码,需要使用第三方 pthread 库。

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
  • 2
  • 3
  • thread:输出型参数,返回线程 ID

  • attr:设置线程的属性,为 NULL 表示使用默认属性

  • start_routine:是个函数地址,线程启动后要执行的函数

  • arg:传给启动线程的参数

  • 返回值:创建成功返回0;创建失败返回对应的错误码

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

using namespace std;

void *threadRoutine(void *args)
{
    while(true)
    {
        cout << "new thread, pid: " << getpid() << endl;
        sleep(2);
    }
}

int main()
{
    pthread_t pid;
    pthread_create(&pid, nullptr, threadRoutine, nullptr);
    while(true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

image-20240312165319396

在编译线程代码的时候,需要加上 -lpthread 选项。因为 pthread.h 是第三方库,但是 g++ 编译器仅仅可以找到 pthread.h 和该库的位置,并不会默认帮我们去链接 pthread 库,因此我们要加 -lpthread 选项,告诉 g++ 编译器,我们要链接这个库。

image-20240312171343716

ps -aL:其中 L 表示查看当前操作系统中的所有轻量级线程。

image-20240312171751985

  • LWP:一个轻量级进程的 ID,CPU 是按照 LWP 来进行调度的。

CPU 调度的基本单位是线程,PID == LWP 的线程叫做主线程

2.2 全局变量在线程间是共享的

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

using namespace std;

int g_val = 100;

void *threadRoutine(void *args)
{
    const char *name = (const char*)args;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val);
        sleep(1);
    }
}

int main()
{
    pthread_t pid;
    pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");
    while (true)
    {
        printf("main thread, pid: %d, g_val: %d, &g_val: 0X%p\n", getpid(), g_val, &g_val);
        sleep(1);
        g_val++;
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

image-20240312222311854

2.3 pthread_join——线程等待

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  • 1
  • 2
  • 3
  • thread:要等待的线程 ID
  • retval:输出型参数,获取线程函数的返回值
  • **返回值:**等待成功0被返回;等待失败,错误码被返回

image-20240312230358310

线程等待的目的:

  • 防止新线程内存泄露

  • 主线程获取子线程的执行结果

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

using namespace std;

int g_val = 100;

void *threadRoutine(void *args)
{
    const char *name = (const char*)args;
    int cnt = 5;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if(cnt == 0) break;
    }

    return (void *)100;
}

int main()
{
    pthread_t pid;
    pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");
    // while (true)
    // {
    //     printf("main thread, pid: %d, g_val: %d, &g_val: 0X%p\n", getpid(), g_val, &g_val);
    //     sleep(1);
    //     g_val++;
    // }
    void *ret;
    pthread_join(pid, &ret);
    cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

image-20240313083537658

线程执行完它的函数后就退出了,主线程在等待的时候,默认是阻塞等待。主线程等待子线程,只能获取到子线程执行函数的返回值,不考虑子线程出异常,因为子线程一旦出异常,主线程也会跟着遭殃。

exit 是用来终止进程的,不能用来直接终止线程。任何一个子线程在任何地方调用 exit 都表示整个进程退出。

2.4 pthread_exit——终止一个线程

pthread_exit:终止调用该函数的线程

#include <pthread.h>

void pthread_exit(void *retval);
  • 1
  • 2
  • 3
  • retval :线程函数的返回值
#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int g_val = 100;

void *threadRoutine(void *args)
{
    const char *name = (const char*)args;
    int cnt = 5;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if(cnt == 0) break;
    }

    pthread_exit((void *)200);
    return (void *)100;
}

int main()
{
    pthread_t pid;
    pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");
    void *ret;
    pthread_join(pid, &ret);
    cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

image-20240313084734928

注意:pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

2.5 pthread_cancel——取消一个线程

pthread_cancel:取消一个已存在的目标线程。

#include <pthread.h>

int pthread_cancel(pthread_t thread);
  • 1
  • 2
  • 3
  • thread:要取消的进程 ID

  • 返回值:取消成功返回0;取消失败,对应的错误码被返回

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

using namespace std;

int g_val = 100;

void *threadRoutine(void *args)
{
    const char *name = (const char*)args;
    int cnt = 5;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if(cnt == 0) break;
    }

    pthread_exit((void *)200);
    return (void *)100;
}

int main()
{
    pthread_t pid;
    pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");

    sleep(1);
    pthread_cancel(pid);
    void *ret;
    pthread_join(pid, &ret);
    cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

image-20240313085650274

一个进程被取消,它的返回值是 PTHREAD_CANCELED 一个宏:

image-20240313085905095

2.6 全局函数可以被多个线程同时调用

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

using namespace std;

int g_val = 100;

void Print(const string &name)
{
    printf("%s is running, pid: %d, g_val: %d, &g_val: 0X%p\n", name.c_str(), getpid(), g_val, &g_val);
}

void *threadRoutine(void *args)
{
    const char *name = (const char*)args;
    int cnt = 5;
    while (true)
    {
        Print(name);// 调用全局函数
        sleep(1);
        cnt--;
        if(cnt == 0) break;
    }

    pthread_exit((void *)200);
    return (void *)100;
}

int main()
{
    pthread_t pid;
    pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");

    int cnt = 0;
    while (true)
    {
        Print("main thread");// 调用全局函数
        sleep(1);
        g_val++;
        cnt++;
        if(cnt == 10) break;
    }

    void *ret;
    pthread_join(pid, &ret);
    cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

image-20240313090718302

这说明代码区对所有的线程来说是共享的。

2.7 线程函数的参数和返回值可以传递对象

一个求和任务

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

using namespace std;

class Request
{
public:
    Request(int start, int end, const string &threadname)
    :start_(start)
    ,end_(end)
    ,threadname_(threadname)
    {}

    int sum()
    {
        int ret = 0;
        for(int i = start_; i <= end_; i++)
        {
            cout << threadname_ << " is running..." << endl;
            ret += i;
            usleep(10000);
        }
        return ret;
    }

public:
    int start_; // 起始数
    int end_; // 终止数
    string threadname_; // 线程的名字
};

class Response
{
public:
    Response(int result, int exitcod)
    :result_(result)
    ,exitcode_(exitcod)
    {}
public:
    int result_; // 计算结果
    int exitcode_; // 标记结果的可靠性
};

void *SumCount(void *args)
{
    Request *rq = static_cast<Request *>(args);
    Response *rp = new Response(rq->sum(), 0);

    delete rq;
    return rp;
}

int main()
{
    pthread_t tid;
    // 创建一个线程
    Request *rq = new Request(1, 100, "Thread 1");
    pthread_create(&tid, nullptr, SumCount, rq);

    void *ret;
    pthread_join(tid, &ret); // 线程等待,获取线程的返回值
    Response *rp = static_cast<Response *>(ret);

    cout << "result: " << rp->result_ << ", exitcode: " << rp->exitcode_ << endl;
    delete(rp);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

image-20240313095804127

该实例证明了堆空间也是共享的。

2.8 C++11 的线程库

pthread.h 是原生线程库。C++11 的线程库本质上是封装了原生线程库。在 Linux 下,C++11 的线程库底层封装的是 Linux 的系统调用,在 Windows 下,C++11 底层封装的是 Windows 的系统调用。这也是 C++ 语言具有跨平台性的体现。如果代码中直接使用系统调用,那么就不具有跨平台性。

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

using namespace std;

void threadrun()
{
    while(true)
    {
        cout << "I am a new thread for C++" << endl;
        sleep(1);
    }
}

int main()
{
    thread th(threadrun);// 创建一个线程

    th.join();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

image-20240313101642708

2.9 pthread_self——获取线程ID

返回调用该函数的线程 ID。

#include <pthread.h>

pthread_t pthread_self(void);
  • 1
  • 2
  • 3
  • 返回值:该函数始终会调用成功,返回调用该函数线程的 ID
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <thread>
#include <string>

using namespace std;

string toHex(int num) // 转十六进制接口
{
    char ret[64];

    snprintf(ret, sizeof(ret), "%p", num);

    return ret;
}

void *threadroutine(void *args)
{
    while(true)
    {
        sleep(2);
        cout << "thread id: " << toHex(pthread_self()) << endl;
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadroutine, nullptr);

    while(true)
    {
        cout << "creat a new thread, id: " << toHex(tid) << endl;
        sleep(1);
    }

    pthread_join(tid, nullptr);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

image-20240313103852295

2.10 再来理解 pthread 线程库

Linux 内核中没有很明确线程的概念,只有轻量级进程的概念,clone 接口就是用来创建一个轻量级进程。pthread_creat 底层就是封装了 clone

#include <sched.h>

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, .../* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
  • 1
  • 2
  • 3

image-20240313105433864

线程的概念是 pthread 线程库给我们提供的,我们在使用原生线程库的时候,g++ 默认使用动态链接,所以该库是会被加载到内存中的,然后通过页表映射到进程的共享区中,线程肯定不止一个,所以线程库一定要把当前操作系统中创建的所有线程管理起来,管理的方式就是通过先描述再组织,因此在线程库中一定存在一个描述线程的结构体,将该结构体称作 TCB,一个线程的 tid (上文中提到的线程 ID)就是其在线程库中的 TCB 对象的起始地址(改地址是一个虚拟地址)。这个 tid 是用户层面的,给用户来使用的,LWP 是内核中的概念,因为 CPU 调度的最小单位是线程(也就是轻量级进程),所以操作系统需要有一个编号来唯一标识一个线程。在Linux 中,我们所说的线程是用户级线程,因为在 Linux 中,线程的概念是 pthread 为我们提供的,在 Windows 中的线程是内核级线程,因为它是由操作系统直接提供的。在 Linux 中一个用户级线程对应一个内核级线程。

在这里插入图片描述

每个线程在被创建出来之后,都要有自己独立的栈结构,因为每个线程都有自己的调用链,执行流的本质就是调用链,该栈空间会保存一个执行流在运行过程中产生的临时变量,函数调用进行的入栈操作。主线程直接使用地址空间中为我们提供的栈结构即可,其他子线程的独立栈,都在共享区,具体来说是在 pthread 库中,tid 指向的 TCB 中维护了该线程的独立栈。

2.11 创建一批线程

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

using namespace std;

#define NUM 10

class ThreaInfo
{
public:
    ThreaInfo(const string &threadname)
    :threadname_(threadname)
    {}

public:
    string threadname_;
};

string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void *threadroutine(void *args)
{
    int i = 0;
    ThreaInfo *ti = static_cast<ThreaInfo*>(args);
    while(i < 10)
    {
        cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << endl;
        i++;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    for(int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));
        pthread_create(&tid, nullptr, threadroutine, ti);

        tids.push_back(tid);
        sleep(1);
    }

    for(auto tid:tids)
    {
        pthread_join(tid, nullptr);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

创建一批线程

2.12 验证——每个线程都有自己独立的栈结构

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

using namespace std;

#define NUM 3

class ThreaInfo
{
public:
    ThreaInfo(const string &threadname)
    :threadname_(threadname)
    {}

public:
    string threadname_;
};

string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void *threadroutine(void *args)
{
    int i = 0;
    int num = 0;
    ThreaInfo *ti = static_cast<ThreaInfo*>(args);
    while(i < 10)
    {
        cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid()  << ", num: " << num << ", &num: " << toHex((pthread_t)&num) << endl;
        i++;
        num++;
        usleep(10000);
    }

    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    for(int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));
        pthread_create(&tid, nullptr, threadroutine, ti);

        tids.push_back(tid);
        // sleep(1);
        usleep(1000);

    }

    for(auto tid:tids)
    {
        pthread_join(tid, nullptr);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

image-20240313141716510

每个线程都去调用了 threadroutine 函数,但是每个线程都有自己的 num,都是从0开始,并且 num 的地址都不同。这正是因为每个线程都有自己独立的栈结构,每个线程在调用该函数时,都将该函数中的局部变量压入自己所在的栈空间。

2.13 线程之间没有秘密

虽然每一个线程都有自己独立的栈结构,但是对于同一个进程创建的多个线程来说,它们都是在该进程的地址空间中,所以只要你想,一个进程是可以拿到另一个线程栈中的数据。

定义一个全局的指针变量,让其指向线程1栈空间中的一个变量,这样就能在主线程中去获取子线程栈空间的数据

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

using namespace std;

#define NUM 3

int *p = nullptr;

class ThreaInfo
{
public:
    ThreaInfo(const string &threadname)
    :threadname_(threadname)
    {}

public:
    string threadname_;
};

string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void *threadroutine(void *args)
{
    int i = 0;
    int num = 0;
    ThreaInfo *ti = static_cast<ThreaInfo*>(args);
    if(ti->threadname_ == "Thread-1") p = &num; // 将线程1中的 num 变量的地址存到 p 指针里面
    while(i < 10)
    {
        cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid()  << ", num: " << num << ", &num: " << &num << endl;
        i++;
        num++;
        usleep(10000);
    }

    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    for(int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));
        pthread_create(&tid, nullptr, threadroutine, ti);

        tids.push_back(tid);
        // sleep(1);
        usleep(1000);

    }

    cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl;

    for(auto tid:tids)
    {
        pthread_join(tid, nullptr);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

image-20240313143018887

虽然这样做可以,但是我们在代码中是禁止这样做的。

2.14 线程的局部存储

定义的普通全局变量是被所有线程所共享的,如果想让该全局变量在每个线程内部,各自私有一份,可以在定义全局变量的前面加上 __thread ,这并不是语言给我们提供的,而是编译器给我们提供。并且 __thread 只能用来修饰内置类型,不能用来修饰自定义类型。

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

using namespace std;

#define NUM 3

int *p = nullptr;

__thread int val = 100;

class ThreaInfo
{
public:
    ThreaInfo(const string &threadname)
    :threadname_(threadname)
    {}

public:
    string threadname_;
};

string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void *threadroutine(void *args)
{
    int i = 0;
    // int num = 0;
    ThreaInfo *ti = static_cast<ThreaInfo*>(args);
    // if(ti->threadname_ == "Thread-1") p = &num; // 将线程1中的 num 变量的地址存到 p 指针里面
    while(i < 10)
    {
        cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid()  << ", val: " << val << ", &num: " << &val << endl;
        i++;
        // num++;
        val++;
        usleep(10000);
    }

    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    for(int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));
        pthread_create(&tid, nullptr, threadroutine, ti);

        tids.push_back(tid);
        // sleep(1);
        usleep(1000);

    }

    // cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl;

    for(auto tid:tids)
    {
        pthread_join(tid, nullptr);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

image-20240313151304588

此时 val 虽然定义在全局,但实际上在每一个进程的独立栈中间中都会为 val 开辟一块空间,来进行存储。

2.15 pthread_detach——线程分离

  • 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成资源泄露。
  • 如果主线程不关心子线程的返回值,join 是一种负担,这个时候,我们可以告诉操作系统,当线程退出时,自动释放线程资源。
#include <pthread.h>

int pthread_detach(pthread_t thread);
  • 1
  • 2
  • 3
  • thread:要分离的线程 ID

  • 返回值:分离成功返回0;失败错误码被返回

  • 小Tips:该函数可以由主线程来调用,也可以由子线程来调用。

#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>

using namespace std;

#define NUM 3

int *p = nullptr;

// __thread int val = 100;

class ThreaInfo
{
public:
    ThreaInfo(const string &threadname)
    :threadname_(threadname)
    {}

public:
    string threadname_;
};

string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void *threadroutine(void *args)
{
    int i = 0;
    // int num = 0;
    ThreaInfo *ti = static_cast<ThreaInfo*>(args);
    // if(ti->threadname_ == "Thread-1") p = &num; // 将线程1中的 num 变量的地址存到 p 指针里面
    while(i < 10)
    {
        cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << endl;
        i++;
        // num++;
        // val++;
        usleep(10000);
    }

    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    for(int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));
        pthread_create(&tid, nullptr, threadroutine, ti);

        tids.push_back(tid);
        // sleep(1);
        usleep(1000);

    }

    // cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl;
    for(auto tid:tids)
    {
        pthread_detach(tid); // 线程分离
    }

    for(auto tid:tids)
    {
        int ret = pthread_join(tid, nullptr);
        printf("%p: ret: %d, messg: %s\n", tid, ret, strerror(ret));
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

image-20240313151716109

子线程各自只执行了一次,是因为,子线程在被创建出来之后,主线程立即将所有的子线程进行了分离,然后,主线程又去进行 join,此时因为所有的子线程已经被分离了,主线程去 join 就不会阻塞等待了,而是直接出错返回,最后主线程执行完毕就直接退出了,主线程退出,也就意味着进程退出,进程退出所有的资源就要被释放,所有子线程赖以生存的环境也没了,所以子线程也就跟着没了。因此,线程分离后,要保证主线程最后退出,常见的情况是主线程跑死循环,一直不退出。

线程分离本质上是线程 TCB 中的一个属性,pthread_detach 本质上就是去修改改属性。

三、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/630898
推荐阅读
相关标签
  

闽ICP备14008679号