【Linux】线程控制

目录

个人主页:矢望

个人专栏:C++LinuxC语言数据结构Coze-AIMySQL

一、线程创建

1.1 创建线程

参数 类型 是否输出 作用 示例
thread pthread_t * 输出参数 成功创建线程后,存储新线程的ID(用于后续操作如 pthread_joinpthread_cancel pthread_t tid; → &tid
attr const pthread_attr_t * 输入参数 设置线程属性(如栈大小、调度策略、分离状态等);传 NULL 表示使用默认属性 NULL&attr
start_routine void *(*)(void *) 输入参数 线程入口函数指针,线程创建后从此函数开始执行;函数签名固定为 void* func(void*) &thread_func
arg void * 输入参数 传递给入口函数的参数,可以是任意类型指针;无参数时传 NULL &my_dataNULL

代码:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>

void* routine(void* args)
{
    const char* name = static_cast<const char*>(args); // 对 args 进行类型强转

    while(true)
    {
        printf("新线程正在运行, name: %s, pid: %d\n", name, getpid());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void*)"Thread-1");

    while(true)
    {
        printf("主线程正在运行,pid: %d\n", getpid());
        sleep(1);
    }

    return 0;
}

编译运行:

如上,创建出了新线程。

我们创建出新线程之后,顺便也在代码中得到了新线程的tid,这个内容是什么呢?我们打印看一下,是LWP吗?

如上图,我们打印出来发现,这并不是LWP,是一个很大的数字。这在在 Linux 上是用户态线程库内部数据结构的地址 。所以我们可以给主线程和新线程都按照地址打印一下。

pthread_self() 返回当前线程的 pthread_t 标识, 获取的是用户态线程 ID。在 Linux 上本质是用户态线程控制块的堆内存地址,用于线程库内部快速访问线程私有数据

cpp 复制代码
void* routine(void* args)
{
    const char* name = static_cast<const char*>(args); // 对 args 进行类型强转

    while(true)
    {
        printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name, pthread_self(), getpid());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void*)"Thread-1");

    printf("新线程 tid: 0x%lx\n", tid);

    while(true)
    {
        printf("主线程正在运行, tid: 0x%lx, pid: %d\n", pthread_self(), getpid());
        sleep(1);
    }

    return 0;
}

编译运行:

1.2 创建多线程

代码:

cpp 复制代码
void* routine(void* args)
{
    std::string name = static_cast<const char*>(args); // 对 args 进行类型强转

    while(true)
    {
        printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
        sleep(1);
    }
}

int main()
{
    int num = 10;
    for(int i = 1; i <= num; i++)
    {
        pthread_t tid;
        // 构建进程名称
        char thread_name[64];
        snprintf(thread_name, sizeof(thread_name), "Thread-%d", i);
        pthread_create(&tid, nullptr, routine, thread_name);

        // 只有主线程可以执行到这里
        sleep(1);
    }
    
    while(true)
    {
        printf("主线程正在运行, tid: 0x%lx, pid: %d\n", pthread_self(), getpid());
        sleep(1);
    }

    return 0;
}

在上面代码中,只有主线程可以执行创建线程的for循环,其它的线程,一旦被创建就会去执行routine方法。

编译运行:

如上图,我们创建了10个线程。

并且10个线程,这10个执行流都在执行routine函数,多个执行流同时进入一个函数,所以这个函数被重入了!又凸显了线程资源共享(代码共享)的特点

如果我在全局定义一个变量,在新线程中修改变量,而主线程只读取变量,你会发现主线程的变量变化了

cpp 复制代码
int gnum = 100;

void* routine(void* args)
{
    std::string name = static_cast<const char*>(args); // 对 args 进行类型强转

    while(true)
    {
        printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
        sleep(1);
        gnum++;
    }
}

int main()
{
    int num = 1;
    //...
    
    while(true)
    {
        printf("主线程正在运行, tid: 0x%lx, pid: %d, gnum: %d\n", pthread_self(), getpid(), gnum);
        sleep(1);
    }

    return 0;
}

编译运行:

异常问题

  • 如果进程内部的一个线程出异常了,这个进程会怎样?

对代码进行修改,挑选一个线程让它发生异常,例如除0错误。

cpp 复制代码
void* routine(void* args)
{
    std::string name = static_cast<const char*>(args); // 对 args 进行类型强转

    if(name == "Thread-6") 
    {
        printf("Thread-6say#: 要出异常了!\n");
        int a = 10;
        a /= 0;
    }

    while(true)
    {
        printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
        sleep(1);
        gnum++;
    }
}

编译运行:

如上,进程终止了。所以 任意线程异常崩溃,会导致整个进程挂掉!

  • 并发问题

我们的代码中有一个问题,如果将创建线程循环中的sleep去掉就会显现,如下图:

cpp 复制代码
for(int i = 1; i <= num; i++)
{
    pthread_t tid;
    // 构建进程名称
    char thread_name[64];
    snprintf(thread_name, sizeof(thread_name), "Thread-%d", i);
    pthread_create(&tid, nullptr, routine, thread_name);

    // 只有主线程可以执行到这里
    // sleep(1);
}

编译运行:

如上图,通过tid我们可以发现,标注的是三个不同的线程,但是它们的线程名获取的是相同的!

这是因为循环中的thread_name的缓冲区中保存着线程名数据,但是它是所有线程共享的,所以当创建出一个新线程之后,这个线程就会拿到thread_name的地址。可是for循环进行很快,可能这个线程还没来得及进行拷贝数据,这个缓冲区中的数据就被更改了,所以拿到的数据就变化了,导致多个进程拿到了同一份数据,这种问题是由并发导致的数据不一致问题

由于thread_name是线程共享的资源,所以我们不能这么使用,我们要给每一个新线程一份独立的数组

cpp 复制代码
for(int i = 1; i <= num; i++)
{
    pthread_t tid;
    // 构建进程名称
    char* thread_name = new char[64];
    sprintf(thread_name, "Thread-%d", i);
    pthread_create(&tid, nullptr, routine, thread_name);

    // 只有主线程可以执行到这里
    // sleep(1);
}

编译运行:

如上图,这样就不会导致数据不一致问题了。

传参问题

pthread_create中的传递的函数是void*的,它是任意类型,所以它就可以传递很多类型,不止有字符串,例如传递类和结构体对象的地址,所以就可以给线程传递任务了!

现在我们写一个简单的加法类:

cpp 复制代码
#pragma once

#include <string>

class Task
{
public:
    Task(int x, int y)
        :_x(x)
        ,_y(y)
    {}

    void Excute()
    {
        _result = _x + _y;
    }

    std::string Result()
    {
        return std::to_string(_x) + " + " + std::to_string(_y) + " = " + std::to_string(_result);
    }

    ~Task(){}
private:
    int _x;
    int _y;
    int _result;
};

然后,创建新线程时,就传递task任务:

cpp 复制代码
void* routine(void* args)
{
    Task* t = static_cast<Task*>(args); // 对 args 进行类型强转

    t->Excute();
    std::cout << t->Result() << std::endl;

    return nullptr;
}

int main()
{
    int num = 10;
    srand((unsigned int)time(nullptr) ^ getpid());

    for(int i = 1; i <= num; i++)
    {
        pthread_t tid;

        int x = rand() % 50 + 1;
        usleep(123); // 尽量让生成的两个数更随机
        int y = rand() % 66 + 1;
        Task* task = new Task(x, y);

        pthread_create(&tid, nullptr, routine, task); // 传递任务

        // 只有主线程可以执行到这里
        // sleep(1);
    }
    
    while(true)
    {
        printf("主线程正在运行, tid: 0x%lx, pid: %d, gnum: %d\n", pthread_self(), getpid(), gnum);
        sleep(1);
    }

    return 0;
}

上面代码中使用了随机数来生成两个加数。

编译运行:

如上图,这样线程就可以执行我们给的任务了!

二、线程终止

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

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit
  2. 线程可以调用pthread_ exit终止自己
  3. ⼀个线程可以调用pthread_cancel终止同一进程中的另一个线程

注意exit是终止整个进程的函数


pthread_exit():终止调用它的线程

使用示例:

cpp 复制代码
void* routine(void* args)
{
    Task* t = static_cast<Task*>(args); // 对 args 进行类型强转

    t->Excute();
    std::cout << t->Result() << std::endl;

    pthread_exit(nullptr);
    // return nullptr;
}

编译运行:

如上图,最终只剩下主线程了。

注意,如果主线程调用,主线程也会终止,但是进程不会退出

cpp 复制代码
void* routine(void* args)
{
    Task* t = static_cast<Task*>(args); // 对 args 进行类型强转

    t->Excute();
    std::cout << t->Result() << std::endl;

    while(true)
    {
        std::cout << "新线程还活着..." << std::endl;
        sleep(1);
    }
}

int main()
{
    int num = 1; // 创建一个新线程
    // ...
    std::cout << "主线程退出..." << std::endl;
    pthread_exit(nullptr);
    
    while(true)
    {
        printf("主线程正在运行, tid: 0x%lx, pid: %d, gnum: %d\n", pthread_self(), getpid(), gnum);
        sleep(1);
    }

    return 0;
}

编译运行:

如上,主线程终止,新线程继续运行。


pthread_cancel():向指定线程发送取消请求,使其终止

我们可以让主线程收集新线程的tid,然后在后面使用tid终止新线程。

cpp 复制代码
void* routine(void* args)
{
    Task* t = static_cast<Task*>(args); // 对 args 进行类型强转

    t->Excute();
    std::cout << t->Result() << std::endl;

    while(true)
    {
        std::cout << "新线程还活着..." << std::endl;
        sleep(1);
    }
}

int main()
{
    int num = 1;
    srand((unsigned int)time(nullptr) ^ getpid());

    std::vector<pthread_t> tids;
    for(int i = 1; i <= num; i++)
    {
        pthread_t tid;
        // ...
		// 收集 tid
        tids.push_back(tid);
    }
    
    for(auto& e : tids)
    {
        pthread_cancel(e);
    }
    
    while(true)
    {
        printf("主线程正在运行, tid: 0x%lx, pid: %d, gnum: %d\n", pthread_self(), getpid(), gnum);
        sleep(1);
    }

    return 0;
}

如上代码,新线程不会主动退出,如果新线程减少了就是主进程干的。

编译运行:

如上,新创建的线程全部被终止了。

不推荐这个函数终止线程,别的线程可能正在完成它的任务。另外如果新线程内调用pthread_cancel去终止主线程是终止不了的。

三、线程等待

新线程必须被主线程等待,否则会出现类似子进程那里的僵尸问题,主线程可以获取新线程的执行结果

pthread_join 用于等待指定线程终止 ,并将该线程的退出状态(通过 pthread_exitreturn 返回的 void* 值)存储到第二个参数中,同时回收线程资源

如上图,第一个参数是要等待的目标线程id,第二个参数是输出型参数,二级指针,用于接收被等待线程的退出状态(即 pthread_exitreturn 返回的 void* 值);如果为 NULL 则忽略退出状态

返回值:等待成功返回0,失败返回错误码

cpp 复制代码
void* Routine(void* args)
{
    std::string name = static_cast<const char*>(args); // 对 args 进行类型强转

    while(true)
    {
        printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
        sleep(3);
        break;
    }

    return (void*)10;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)"Thread-1");

    sleep(5);

    // 1. 一般而言必须等待新线程退出,如果不等待,导致类似僵尸进程的问题
    // 2. 为了获取新线程执行的结果
    void *retval = nullptr;
    int n = pthread_join(tid, &retval);
    if(n == 0)
    {
        std::cout << "join success! " << (long long)retval << std::endl;
    }

    return 0;
}

如上代码中,我们创建出新线程,然后执行3s之后,让新线程退出,主线程获取它。

编译运行:

如上图,新线程获取之后,主线程等待新线程成功,拿到了新线程的退出码。

代码中,为什么直接获取的就是新线程的退出码呢?那退出信号呢?也就是说不需要进行异常分析吗? 任何线程出异常,进程直接终止!没有机会join成功,所以不需要在这里关心异常!只关心正常情况

返回值类型

Routine的返回值类型可是void*的,谁规定只能返回整形类型的,我可以返回类对象等任意类型!

我们在前面不是实现了一个加法类吗,我就要让它执行加法任务,然后给我返回。

cpp 复制代码
void* Routine(void* args)
{

    Task* t = static_cast<Task*>(args); // 对 args 进行类型强转
    
	std::cout << "新线程执行任务" << std::endl;
    t->Excute(); // 执行任务
    std::cout << "执行完毕!" << std::endl;

    return (void*)t;
}

int main()
{
    Task t(10, 20);

    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)&t);

    sleep(5);
    Task* task;
    int n = pthread_join(tid, (void**)&task);
    if(n == 0)
    {
        // 得到任务的执行结果
        std::cout << "join success! " << task->Result() << std::endl;
    }
    
    return 0;
}

编译运行:

注意:如果thread线程被别的线程调用pthread_cancel异常终掉,再次等待线程成功之后,第二个参数retval所指向的单元里存放的是常数PTHREAD_CANCELED,这个值是-1

四、线程分离

线程内部是不可以直接执行程序替换函数exec*的,这样会导致整个进程的代码和数据都被替换掉,但是可以fork+exec*

主线程进行线程等待是阻塞等待的,如果想让线程全部自动结束掉,线程的执行结果并不关心,主线程不想等待线程,可以将目标线程设置成为分离状态

pthread_detach 用于将指定线程标记为分离状态 ,意味着该线程终止时会自动释放其占用的系统资源(如线程栈、TCB等) ,无需也不能再用 pthread_join 等待和回收。

cpp 复制代码
void* Routine(void* args)
{
    std::string name = static_cast<const char*>(args); // 对 args 进行类型强转

    int cnt = 5;
    while(cnt--)
    {
        printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)"Thread-1");

    std::cout << "主线程分离线程" << std::endl;
    pthread_detach(tid); // 分离线程

    sleep(3);

    int* ret = nullptr;
    int n = pthread_join(tid, (void**)&ret);
    if(n == 0)
    {
        std::cout << "join success! " << n << std::endl;
    }
    else 
    {
        std::cout << "join error! " << n << std::endl;
    }
    
    return 0;
}

如上代码,我们让主线程对新线程进行了分离,之后在进行线程等待的时候,我们就应该等待失败,然后拿到失败的错误码。

编译运行:

等待失败时候,主线程退出,整个进程退出。

扩充:新线程被分离之后,如果在线程内部发生了异常等问题,整个进程依然会被终止

最佳实践:一旦线程要被设置为分离,主线程不能提前退,甚至主线程是个死循环!

五、线程ID及进程地址空间布局

如上图所示,pthread库也是库,所以它就要被映射到当前进程的虚拟地址空间以支持线程控制。

一个进程中的线程可以有多个,所以就线程需要被进程管理起来,例如描述线程的结构体struct Tcb{...}。如下图所示:

上图圈出来的就是线程描述结构体Tcb,之前说线程ID是一个地址,这个地址就是这个描述结构体的起始地址

此外在线程描述结构体中有一个void *result的成员,这个里面存储的就是线程运行结束的返回值,我们调用pthread_join时,就是通过第一个参数找到描述结构体,然后第二个参数会将result中存放的值带出来

在创建线程的时候struct pthread :struct task_struct1:1的,所以也叫做1:1式的用户级线程。前面那个用户级线程结构体是一个摆设,真正在内核中干活的是后面的PCB,当PCB执行完成之后,会将数据在pthread中写一份

所以我们之前才会看到即使没有pthread_join线程,我们在查询的时候,依旧看不到类似僵尸线程的状态,因为task_struct确实被释放了,所以查不到;但是必须对线程进行等待是因为pthread这个描述结构体还在占用着空间,如果不进行等待会造成内存泄漏,这个内存泄漏是在库里面发生的

结构体 所在层级 核心职责 生命周期管理
struct pthread 用户态 (由pthread库在堆/mmap区域管理) 存储线程的用户态上下文:pthread_t ID(即结构体自身地址)、线程栈地址、线程局部存储(TLS)、void *result 返回值等。 pthread_create 创建,必须由 pthread_joinpthread_detach 回收,否则会内存泄漏。
task_struct 内核态 (内核管理) 内核调度实体(LWP),存储内核上下文:进程PID、调度优先级、CPU状态、打开文件表等。 clone 系统调用创建,线程主动退出或被取消时,内核会立即回收此结构体。

Linux线程是"一个用户态的壳(struct pthread)包裹着一个内核态的核(task_struct)"。内核回收"核",而"壳"需要用户通过 pthread_join 显式回收

线程局部存储

cpp 复制代码
__thread int cnt = 0;

void* Routine(void* args)
{
    std::string name = static_cast<const char*>(args); // 对 args 进行类型强转

    while(true)
    {
        printf("new thread cnt: %d\n", cnt);
        printf("new thread cnt: %p\n", &cnt);
        cnt++; // 新线程修改 cnt

        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)"Thread-1");

    while(true)
    {
        printf("main thread cnt: %d\n", cnt);
        printf("main thread cnt: %p\n", &cnt);
        sleep(1);
    }
    
    pthread_join(tid, nullptr);
    
    return 0;
}

如上代码,在全局变量前使用了__thread,它是线程局部存储(TLSThread Local Storage) 关键字,用于修饰全局或静态变量,使得每个线程拥有该变量的独立副本,互不干扰

如果没有这个关键字修饰的话,我们之前的代码演示过,主线程和新线程获取到的cnt是相同的,使用它修饰之后,我们应该看到它们是不同的。

编译运行:

如上图,这两个变量的地址都不相同。

线程局部存储: 只能用来局部存储内置类型,常见的是整形。它可以让不同的线程用同样的变量名,访问不同的内存块,各自访问各自的局部存储!

六、C++ 多线程

C++中也是有多线程的,线程管理在<thread>头文件中。

简单使用代码:

cpp 复制代码
#include <iostream>
#include <thread>
#include <unistd.h>

void Routine(int cnt)
{
    while(true)
    {
        std::cout << "new thread, cnt: " << cnt << std::endl;
        cnt++;
        sleep(1);
    }
}

int main()
{
    std::thread t(Routine, 10); // 创建线程

    while(true)
    {
        std::cout << "main thread" << std::endl;
        sleep(1);
    }

    t.join(); // 等待线程

    return 0;
}

使用g++ -o TestThread TestThread.cc -std=c++11编译:

虽然,编译通过了,但是运行时发生了报错,这是因为当前在Linux平台上跑,但编译时没有链接 pthread 库。

再次编译运行:

如上,链接上pthread库就可以运行了。

所以C++多线程的本质:在Linux系统中本质是C++多线程操作时对pthread库的封装!而在Windows下,它也对Windows上的相关的库进行了封装!
因此C++才有良好的跨平台性!


所以为什么C++的新特性的支持通常很慢,一年为单位呢?因为从技术角度讲,它要把所有平台上的对应的功能都封装一遍!

所有语言都追求跨平台性是因为它们为了吸引更多的用户,这样,这门语言的生命力才会更强!

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
Chirp7 小时前
Windows下借助wsl2读取ext4格式磁盘
linux·windows
IMPYLH7 小时前
Linux 的 whoami 命令
linux·运维·服务器·bash
NashSKY7 小时前
RK3588 Linux SDK 编译、烧录与 MIPI 屏配置流程
linux·rk3588
JAVA社区7 小时前
Java进阶全套教程(七)—— Redis超详细实战详解
java·linux·开发语言·redis·面试·职场和发展
青天喵喵8 小时前
Linux Wi-Fi 实战指南:AP / STA 实战用例(实战篇一)
linux·网络·架构·智能路由器·嵌入式·wi-fi
广州灵眸科技有限公司8 小时前
瑞芯微(EASY EAI)RV1126B ubuntu系统SDK源码获取
linux·运维·ubuntu
Irissgwe8 小时前
二、Socket编程UDP
linux·网络·网络协议·udp·socket·socket编程
无相孤君9 小时前
我用 Docker + JunimoServer 搭了一个星露谷物语无头服,还顺手做了个本地管理面板
linux·游戏·docker·开源
浮生若城10 小时前
Linux基础I/O(2):理解“一切皆文件”与缓冲区
linux·运维·服务器