【Linux】线程控制

目录

🐼回顾

🐼多线程共享同一虚拟地址空间

🐼各个接口的注意事项

🐼如何理解tid,以及传参问题

🐼封装线程库


🐼回顾

Linux不需要有线程的概念,只存在轻量化进程的概念!lwp(Light Weight Process)。但是在用户视角,只认进程和线程,为了方便用户使用,因此Linux在轻量化进程系统调用clone等的基础上,封装了一个原生线程库:pthread库。提供线程接口给用户使用!


🐼多线程共享同一虚拟地址空间

我们上节课说过**,线程是进程内的一个执行流,共用进程的虚拟地址空间,所以如果变量定义在全局,所有线程都能看到, 现在会发现以前的进程间通信,它们让两个进程看到同一份代码和资源的方法都很复杂,但是线程天生就具有看到同一份资源的能力,因为能看到同一份虚拟地址空间,因此,线程间通信十分方便。**

🔹 全局变量共享

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

int gval = 10;
void* thread_routine(void* args)
{
    std::string name = static_cast<const char*>(args);
      while (true)
    {
        gval++;
        printf("new thread create success,name:%s gavl:%d &gval:%p\n",name.c_str(),gval,&gval);
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");

    while (true)
    {
        printf("main thread create success gavl:%d &gval:%p\n",gval,&gval);
        sleep(1);
    }
    return 0;
}

现象:


🔹全局函数共享

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

int gval = 10;
std::string func()
{
    return "我是另一个函数";
}

void* thread_routine(void* args)
{
    std::string name = static_cast<const char*>(args);
      while (true)
    {
        gval++;
        printf("new thread create success,name:%s gavl:%d &gval:%p func():%s &func():%p\n",name.c_str(),gval,&gval,func().c_str(),func);
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");

    while (true)
    {
        printf("main thread create success gavl:%d &gval:%p func():%s &func():%p\n",gval,&gval,func().c_str(),func);
        sleep(1);
    }
    return 0;
}

🔹堆共享,逻辑上是可以共享

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

int gval = 10;
int *data;
std::string func()
{
    return "我是另一个函数";
}

void* thread_routine(void* args)
{
    data = new int(10);
    std::string name = static_cast<const char*>(args);
      while (true)
    {
        gval++;
        // printf("new thread create success,name:%s gavl:%d &gval:%p func():%s &func():%p\n",name.c_str(),gval,&gval,func().c_str(),func);
        printf("new thread... data:%d &data:%p\n",*data,data);
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    (void)n;//避免警告

    sleep(2);//保证了新线程先向要堆空间,主线程再出发
    while (true)
    {
        // printf("main thread create success gavl:%d &gval:%p func():%s &func():%p\n",gval,&gval,func().c_str(),func);
        printf("main thread... data:%d &data:%p\n",*data,data);
        sleep(1);
    }
    return 0;
}

我们之前可以创建多进程,可以创建多线程吗?可以!

创建多少最佳呢?

计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。(最佳是CPU个数*CPU核数)

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

🔹创建一批线程

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

int gval = 10;
int *data;
std::string func()
{
    return "我是另一个函数";
}

void *thread_routine(void *args)
{
    // 堆空间,就是该线程申请的, 往往只
    //     // 需要让对应的线程知道这部分堆空间起始虚拟地址,就可以叫做这个对是该线程拥有的
    // int *data = new int(10);
    std::string name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt--)
    {
        // gval++;
        // printf("new thread create success,name:%s gavl:%d &gval:%p func():%s &func():%p\n",name.c_str(),gval,&gval,func().c_str(),func);
        // printf("new thread... data:%d &data:%p\n", *data, data);
        printf("new thread is runing name is:%s\n", name.c_str());
        sleep(1);
    }
    return nullptr; // 表示线程退出
}
int main()
{
    std::vector<pthread_t> tids;
    for (int i = 0; i < gval; i++)
    {
        pthread_t tid;
        // 格式化线程名字
        // char buff[64];//bug// 这样对线程初始化名字问题是因为线程的运行顺序不确定,buff是一个共享资源,导致错乱
        char *buff = new char[64]; 
        snprintf(buff, 64, "thread-%d", i);

        int n = pthread_create(&tid, nullptr, thread_routine, (void *)buff);
        (void)n; // 避免警告
        tids.emplace_back(tid);
        // printf("main thread create success:%s\n",buff);
    }

    sleep(1);
    // sleep(2); // 保证了新线程先向要堆空间,主线程再出发
    for (auto &tid : tids)
        printf("main create a new thread, new thread id is : 0x%lx\n", tid);

    for (auto &tid : tids)
        pthread_join(tid, nullptr);

    printf("main thread end...\n");
    // while (true);
    // while (true)
    // {
    //     // printf("main thread create success gavl:%d &gval:%p func():%s &func():%p\n",gval,&gval,func().c_str(),func);
    //     printf("main thread... data:%d &data:%p\n",*data,data);
    //     sleep(1);
    // }
    // pthread_join(tid,nullptr);
    return 0; // 主线程退出了,不管其他线程有没有运行完,都要退出了,因为要释放资源了!
}

一个线程死了,其他线程都崩溃了。因为整个进程崩溃

🔹给多线程添加任务、主线程等待子线程,并且获取退出信息

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

void *thread_routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt--)
    {
        printf("new thread is runing name is:%s,id is: 0x%lx\n", name.c_str(), pthread_self());
        sleep(1);
    }
    //线程退出
    // exit(0);//表示进程直接退出
    // return nullptr; // 新线程退出方式1
    // pthread_exit(0); // 新线程退出方式2
    //    pthread_cancel(pthread_self());//可以这么退出,但是实际不这么用
    //    return (void*)10;
    // 方法3
    // pthread_exit((void*)0);
    // pthread_exit((void*)1);
    // pthread_exit((void*)2);
    // pthread_exit((void*)3);  
    pthread_exit((void*)4);
    // return (void*)10; // 字面值,int,4 , "hello world!", char *str = "hello";
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
    (void)n; // 避免警告
    printf("main thread create success , main thread id is: 0x%lx,new thread id is: 0x%lx\n", pthread_self(), tid);

    sleep(2);
    // printf("cancel new thread...\n");
    // pthread_cancel(tid);// 方法三本来就是main取消其他线程的---最佳实践

    // 得知新线程执行的结果:代码跑完,结果对还是不对! 退出信息 = 信号 + 退出码,难道,多线程这里,不考虑所谓的异常吗??
    // 不用考虑,因为没机会考虑!!!!
    void *ret;
    int m = pthread_join(tid, &ret); //  int pthread_join(pthread_t thread, void **retval);
    if (m == 0)
    {
        printf("pthread_join success,m: %d,ret:%lld\n", m, (long long)ret);
    }

    sleep(3);
    printf("main thread end...\n");
    return 0;
}

🐼各个接口的注意事项

首先,如果想使用pthread库,在编译时链接-lpthread库和你的程序进行编译。因为pthread库是Linux提供的第三方库。

pthread_create的最后一个参数是void*类型的,这就给我们很大的拓展性和灵活性,所以最后一个参数谁说只能传内置类型,传对象也可以,比如:

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

class Task
{
public:
    Task() {}

    Task(int x, int y) : _x(x), _y(y) {}
    void Div()
    {
        if (_y == 0)
        {
            _code = 1;
            return;
        }

        _result = _x / _y;
    }

    void Print()
    {
        std::cout << "result: " << _result << "[" << _code << "]" << std::endl;
    }
    ~Task() {}

private:
    int _x;
    int _y;
    int _code;
    int _result;
};
void *thread_routine(void *args)
{
    sleep(1);
    // std::string name = static_cast<const char *>(args);
    // std::cout << "我是新线程:" << name << std::endl;

    Task *t = static_cast<Task *>(args);

    t->Div();
    // pthread_detach(pthread_self());
    return t;
}

int main()
{
    pthread_t tid;
    Task *t = new Task(10, 0);
    pthread_create(&tid, nullptr, thread_routine, t);

    std::cout << "我是主线程" << std::endl;
    sleep(2);
    // pthread_detach(tid);//线程分离不需要join!!!
    // while (true)
    //     ;
    void *ret;
    int m = pthread_join(tid, &ret);

    // Task *result = (Task *)ret;
    // result->Print();
    std::cout << m << std::endl;
    if (m == 0)
    {
        Task *result = static_cast<Task *>(ret);
        result->Print();
        std::cout << "join success" << " " << (long long int)ret << std::endl;
    }
    else
    {
        std::cout << "m:" << strerror(m) << std::endl;
    }
    return 0;
}

**✅**我们可以使用pthread_self得到当前线程的ID,pthread 库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯⼀的"ID"来唯⼀标识这个线程。

我们可以使用ps -aL | head -1查看线程信息

LWP(Light Weight Process,轻量级进程)是 Linux 内核中的线程实现方式,它得到的是真正的线程ID。之前使用 pthread_self得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID、线程栈、寄存器等属性。

在 ps -aL得到的线程ID中,有一个线程ID和进程ID相同,这个线程就是主线程。主线程的栈在虚拟地址空间的栈上,而其他线程的栈是在共享区(堆栈之间)因为 pthread系列函数都是 pthread 库提供给我们的,而 pthread 库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区


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

从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。

线程可以调用pthread_ exit终⽌自已(pthread_exit(pthread_self())。

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

✅默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。 如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,我们可以使用pthread_detach()来分离某个线程,表示该线程如果无异常运行完会自动退出,不需要主线程再join,和SIGCHLD原理有点类似 。joinable和分离是冲突的,一个线程不能既是joinable又是分离的。分离线程,主线程如果detach指定线程,表示对他不关心:

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

class Task
{
public:
    Task() {}

    Task(int x, int y) : _x(x), _y(y) {}
    void Div()
    {
        if (_y == 0)
        {
            _code = 1;
            return;
        }

        _result = _x / _y;
    }

    void Print()
    {
        std::cout << "result: " << _result << "[" << _code << "]" << std::endl;
    }
    ~Task() {}

private:
    int _x;
    int _y;
    int _code;
    int _result;
};
void *thread_routine(void *args)
{
    sleep(1);
    // std::string name = static_cast<const char *>(args);
    // std::cout << "我是新线程:" << name << std::endl;

    Task *t = static_cast<Task *>(args);

    t->Div();
    // pthread_detach(pthread_self());
    return t;
}

int main()
{
    pthread_t tid;
    Task *t = new Task(10, 0);
    pthread_create(&tid, nullptr, thread_routine, t);

    std::cout << "我是主线程" << std::endl;
    sleep(2);
    pthread_detach(tid);//线程分离不需要join!!!
    while(true);
    void *ret;
    //     int m = pthread_join(tid, &ret);

    //     // Task *result = (Task *)ret;
    //     // result->Print();
    //     std::cout<<m<<std::endl;
    //    if (m == 0)
    //     {
    //         Task *result = static_cast<Task *>(ret);
    //         result->Print();
    //         std::cout << "join success" << " " << (long long int)ret << std::endl;
    //     }
    //     else
    //     {
    //         std::cout << "m:" << strerror(m) << std::endl;
    //     }
    return 0;
}

🐼如何理解tid,以及传参问题

下面我们谈谈pthread_t究竟是什么类型,为什么pthread_create第一个参数就是tid。pthread_join是如何获取到线程的返回值的。

✅首先我们要很清楚,既然线程有不同的工作状态,这么多线程,os一定要有一个struct tcb{}来管理这些线程,由于pthread库是共享库,并且每一个线程都会维护自已的栈,上下文,而我们创建线程,本质上是在共享库中malloc,在共享库内部,帮助我们维护了一个线程对象

pthread_create 调用后,内核会分配一个 LWP(轻量级进程,即线程),pthread 库在用户态维护一个线程控制块(TCB),存储线程的栈、寄存器、状态等信息。而反应给用户的就是一个地址,为了让用户找到这块线程,所以才有了pthread_t,所以pthread_t,本质就是⼀个进程地址空间上的⼀个地址.创建线程pthread库源码部分重要字段:

cpp 复制代码
// Linux传说的phread库源码
// 重点2:传说中的原⽣线程库中的⽤来描述线程的tcb
struct pthread *pd = NULL;


// 把pd(就是线程控制块地址)作为ID,传递出去,所以上层拿到的就是⼀个虚拟地址
*newthread = (pthread_t)pd

/* Thread ID - which is also a 'is this thread descriptor (and
therefore stack) used' flag. */
pid_t tid;
/* Process ID - thread group ID in kernel speak. */
pid_t pid;


// 线程⾃⼰的栈和⼤⼩
void *stackblock;
size_t stackblock_size;


syscall // 陷⼊内核(x86_32是int 80),要求内核创建轻量级进程

在创建线程时,就是在共享库帮我们维护了一个线程对象,我们将tid等创建信息写入到了struct tcb,而库让我们可以通过tid来找到这块地址,所以tid是一个输出型参数。

理解了这点,那如果主线程关心新线程的返回值,pthread_join()第二个参数是如何获取到新线程的返回值的?

**首先,新线程返回值,一定是向维护的那块虚拟地址的struct tcb的某个字段中写入,我们通过二级指针,指向这个字段,自然就能获取到了!**pthread库源码字段:

cpp 复制代码
/* The result of the thread function. */
// 线程运⾏完毕,返回值就是void*, 最后的返回值就放在tcb中的该变量⾥⾯
// 所以我们⽤pthread_join获取线程退出信息的时候,就是读取该结构体
// 另外,要能理解线程执⾏流可以退出,但是tcb可以暂时保留,这句话
void *result;

所以pthread库让我们用户可以通过pthread_create的第一个参数找到库维护的那块线程对象所占的虚拟地址。如图所示:

最后再总结一下,pthread_t 的值通常是 TCB 的地址(用户态视角),而非内核的 LWP ID。

在 Linux 中,pthread_t 是用户态线程标识符,通过 pthread_self()获取,本质是线程库管理的 TCB 地址;而 LWP(TID)是内核态线程 ID,通过 syscall(SYS_gettid) 获取,代表内核调度的真实线程 ID。两者分别用于用户态和内核态的线程管理。


🐼封装线程库

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <functional>

#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>

// 1. 后面加上拷贝构造和移动构造函数
// 2. _is_running 设置为临界资源
// 3. Debug 策略换成日志
namespace lsg
{
    using func_t = std::function<void()>;
#define GET_LWPID() syscall(SYS_gettid)
    class Thread
    {
    public:
        Thread(const std::string &name, func_t func)
        {
            std::cout << _name << ": create success" << std::endl;
        }

        Thread(const Thread &) = delete;
        Thread &operator=(const Thread &) = delete;

        static void *routine(void *args)
        {
            Thread *self = static_cast<Thread *>(args);
            self->_is_running = true;
            self->lwpid = GET_LWPID();
            self->_func();
            pthread_exit((void *)0);
        }

        bool Start()
        {
            // 安全检查
            if (!_func)
            {
                std::cerr << "Thread function is empty!" << std::endl;
                return false;
            }
            int n = pthread_create(&_tid, nullptr, routine, this);
            if (n == 0)
            {
                std::cout << _name << ": run success" << std::endl;
                return true;
            }
            else
            {
                std::cerr << "pthread_create error: " << strerror(errno) << std::endl;
                return false;
            }
        }

        bool Join()
        {
            if (!_is_running)
                return false;

            int n = pthread_join(_tid, nullptr);
            _is_running = false;
            if (n == 0)
            {
                std::cout << _name << ": join success" << std::endl;
                return true;
            }
            else
            {
                std::cerr << "join error: " << strerror(errno) << std::endl;
                return false;
            }
        }

        bool Die()
        {
            if (!_is_running)
                return false;
            int n = pthread_cancel(_tid);
            if (n == 0)
            {
                std::cout << _name << ": cancel success" << std::endl;
                return true;
            }
            else
            {
                std::cerr << "cancel error: " << strerror(errno) << std::endl;
                return false;
            }
        }

        ~Thread()
        {
            if (_is_running)
            {
                std::cerr << "WARNING: Thread " << _name << " is still running!" << std::endl;
                pthread_detach(_tid);
            }
        }

    private:
        bool _is_running; // 临界资源。主线程和子线程
        std::string _name;
        pthread_t _tid;
        pid_t lwpid;
        func_t _func;
    };
}

需要注意的一点就是,由于pthread_create的回调函数参数是void*args,但是作为类的成员函数,隐藏this指针,所以我们将其实现为类的静态成员方法,为了访问其私有成员属性,我们将this作为参数传递进去.

相关推荐
小糖学代码3 小时前
LLM系列:1.python入门:3.布尔型对象
linux·开发语言·python
shizhan_cloud3 小时前
Shell 函数的知识与实践
linux·运维
Deng8723473483 小时前
代码语法检查工具
linux·服务器·windows
霍夫曼5 小时前
UTC时间与本地时间转换问题
java·linux·服务器·前端·javascript
月熊6 小时前
在root无法通过登录界面进去时,通过原本的普通用户qiujian如何把它修改为自己指定的用户名
linux·运维·服务器
大江东去浪淘尽千古风流人物6 小时前
【DSP】向量化操作的误差来源分析及其经典解决方案
linux·运维·人工智能·算法·vr·dsp开发·mr
赖small强7 小时前
【Linux驱动开发】NOR Flash 技术原理与 Linux 系统应用全解析
linux·驱动开发·nor flash·芯片内执行
IT运维爱好者8 小时前
【Linux】LVM理论介绍、实战操作
linux·磁盘扩容·lvm
LEEE@FPGA8 小时前
ZYNQ MPSOC linux hello world
linux·运维·服务器
郝学胜-神的一滴8 小时前
Linux定时器编程:深入理解setitimer函数
linux·服务器·开发语言·c++·程序人生