【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作为参数传递进去.

相关推荐
海琴烟Sunshine3 小时前
leetcode 28. 找出字符串中第一个匹配项的下标 python
linux·python·leetcode
東雪蓮☆3 小时前
Docker 资源限制与性能优化(CPU / 内存 / IO 管控实战)
linux·运维·docker
阑梦清川4 小时前
深入理解动态链接和静态链接
linux
孞㐑¥4 小时前
Linux网络部分—网络层
linux·c++·经验分享·笔记
ms72wx4 小时前
拥抱终端:Linux 新手命令行入门指南
linux·运维·服务器
東雪蓮☆5 小时前
容器生命周期与管理策略
linux·运维·docker
---学无止境---6 小时前
Linux 内核等待队列(Wait Queue)机制深度分析
linux
wheeldown6 小时前
【Linux】Linux管道与进程池深度解析:从原理到实战
linux·运维·服务器
_extraordinary_6 小时前
Java Linux --- 基本命令,部署Java web程序到线上访问
java·linux·前端