【Linux庖丁解牛】— 线程控制!

1. 验证之前的理论

我们之前说在Linux下,线程本质就是轻量级进程,我们创建线程本质就是在进程中划分地址空间,贡献进程资源。那我们先用用代码来验证一下,下面的代码不用管,直接看结果即可。

test_thread.cc

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

void *thread_run(void *args)
{
    std::string name = (char *)args;
    while (true)
    {
        sleep(1);
        std::cout << "我是" << name << " pid->" << getpid() << std::endl;
    }
    return nullptr;
}

int main()
{
    // 创建新线程
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_run, (void *)"新线程1");

    // 主线程
    while (true)
    {
        std::cout << "我是主线程" << " pid->" << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}

makefile:

复制代码
test:test_thread.cc
	g++ -o $@ $^ -std=c++11 -l pthread 

PHNOY:clean 
clean:
	rm -rf test

线程果然是属于同一个进程的!

不过,既然一个进程可以有多个线程,我们怎么看到这些线程呢??

不废话:ps -aL 命令查看:

pid果然一样,LWP【light weight process】就是轻量级进程!!所以每个线程都有自己的lwp。

2. 引入pthread线程库

我们在以上代码的编译的时候,使用了**-l pthread 选项**,该选项是用来帮助编译器找到pthread库的。如果我们不引入这个库,那么编译器就无法识别pthread_create接口了。

那么,为什么会有这个东西,为什么会有这个库呢??操作系统直接提供对应的系统调用不就好了吗??

Linux并不存在实际意义上的线程,所谓线程是使用轻量级线程模拟的!!但Linux操作系统只有轻量级进程,所谓用用轻量级线程模拟线程只是我们的说法!!

Linux操作系统也确实给我们提供了对应创建轻量级进程的接口:

这些接口我们不必了解,我们知道上层用户在学操作系统时只认线程,你Linux根我说没有线程,只有轻量级进程那就太不厚道了 !!因此,Linux工程师们就将底层的创建轻量级进程的接口封装称为一个库->pthread库,给用户提供一批创建线程的接口

我们之前在学C++的时候就接触过C++的线程,那C++的线程方案为了保证自己语言的可移植性,把各平台下实现线程的方案都打包起来形成库,使用条件编译 。当程序在不同平台下运行时,程序就会使用对应平台的实现方案。

3. Linux线程控制的接口

> pthread_creat【创建线程】

> pthread_join【回收线程】

新建线程也需要被主线程等待回收,不然会造成像僵尸进程的后果,比如内存泄漏。pthread_join就可以等待回收对应的线程。

> pthread_self【获取线程id】

> demon代码

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

std::string format(pthread_t tid)
{
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}

void *thread_run(void *args)
{
    std::string name = (char *)args;
    int cnt = 5;
    while (cnt)
    {
        sleep(1);
        std::cout << "我是" << name << " pid->" << getpid() << " tid->" << format(pthread_self()) << std::endl;
        cnt--;
    }
    return (void *)10;
}

int main()
{
    // 创建新线程
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_run, (void *)"新线程1");

    // 主线程
    int cnt = 3;
    while (cnt--)
    {
        std::cout << "我是主线程" << " pid->" << getpid() << " tid->" << format(pthread_self()) << std::endl;
        sleep(1);
    }

    // 回收线程
    void *ret;
    int n = pthread_join(tid, &ret);
    (void)n;

    std::cout << "新线程退出,退出码:" << (long long)ret << std::endl;

    delete t;
    delete ret;

    return 0;
}

> 传参和返回值问题

在我们上面所学的接口中,在我们创建线程【pthread_create】时,我们传给线程的类型是任意的,比如我们可以传递一个任务对象让子线程完成任务,然后返回一个结果对象。这个结果对象我们可以用pthread_join接收。

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

std::string format(pthread_t tid)
{
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}

class task
{
public:
    task(int a, int b) : _a(a), _b(b) {}
    ~task() {}
    int Excute() { return _a + _b; };

private:
    int _a;
    int _b;
};

class result
{
public:
    result(int ret) : _ret(ret) {}
    ~result() {};
    long long get_ret() { return _ret; }

private:
    long long _ret;
};


void *thread_run(void *args)
{
    task *t = static_cast<task *>(args);
    result *ret = new result(t->Excute());
    return ret;
}

int main()
{
    // 创建新线程
    pthread_t tid;
    task *t = new task(10, 20);
    pthread_create(&tid, nullptr, thread_run, (void *)t);

    // 主线程
    int cnt = 3;
    while (cnt--)
    {
        std::cout << "我是主线程" << " pid->" << getpid() << " tid->" << format(pthread_self()) << std::endl;
        sleep(1);
    }

    // 回收线程
    void *ret;
    int n = pthread_join(tid, &ret);
    (void)n;

    std::cout << "新线程退出,退出码:" << ((result *)ret)->get_ret() << std::endl;
    
    delete t;
    delete ret;

    return 0;
}

> 线程终止

前面所说线程从入口函数执行完后return就是说线程终止的一种方式,注意线程并不能用exit终止,如果这样做,整个进程就结束了。所以这里还有一个接口可以让线程终止->pthread_exit();

void*的值和我们前面说的return返回的值是一个道理。

还有一个接口pthread_cancel,这个接口是给主线程取消一个新线程用的!!线程一旦被取消,其退出结果则为-1。

> 线程分离

进程等待子进程退出的方式有很多种,比如说阻塞等待,非阻塞等待,还可以让子进程退出的时候发信号,父进程在自定义捕捉信号处等待子进程。

那主线程是否只能阻塞等待新线程呢??线程被创建后,默认是joinable的,是一定需要被主线程join的,否则就会造成内存泄漏等问题。有没有其他方法可以让主线程不必等待新线程呢??有的,我们可以采用线程分离 的方式。一旦一个新线程和主线程分离,那么主线程就再也不关心新线程了!!新线程退出的时候会自动释放资源,并且不会给主线程返回退出码

我们采用接口pthread_detach来完成线程分离【线程分离不仅可以让主线程把新线程分离,还可以让新线程自己从主线程中分离出来】:

4. 创建多线程实操demon

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

const int num = 10;

void *routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    int cnt = 5;

    std::cout << "我是" << name << std::endl;

    return nullptr;
}

int main()
{
    // 记录线程id
    std::vector<pthread_t> tids;
    for (int i = 0; i < num; i++)
    {
        // 注意使用共享地址空间时数据的一致性
        char *buffer = new char[64];
        snprintf(buffer, 64, "线程%d", i);
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, routine, (void *)buffer);
        if (n == 0)
            tids.push_back(tid);
        sleep(1);
    }

    for (auto e : tids)
    {
        int n = pthread_join(e, nullptr);
        if (n == 0)
        {
            std::cout << "等待成功\n";
        }
    }
    return 0;
}

5. 线程id&进程地址空间布局

我们之前看到,打印出来的线程id是一个很大的数字,并且他并不等于lwp【轻量级进程】。那它到底是什么呢??

我们知道,使用线程相关的接口都需要链接动态库pthread。所以,当我们的程序加载到内存的时候,对应的pthread库也加载到内存了并且和进程地址空间中的共享区建立了映射

所以,我们的程序在代码区执行到线程相关的接口时,都会跳转到库中执行对应的方法。所以,我们线程的概念是在库中维护 的!!一个进程在库中可能会创建多个进程,因此,库中势必会通过先描述,再组织的方式维护管理这些线程

struct pthread就是tcb ,而线程局部存储和线程栈我们先不说 。在tcb中有一个(void*)ret,这个变量会记录线程退出时的退出信息,因此我们join时就是从tcb中拷贝一份该数据就可以了 ,只是,线程退出后只是该函数结束了而已为对应线程创建的内存空间并没有释放!!所以,我们需要join来获得管理线程的控制块的地址并将其释放,而这个地址就是tid(线程id)

不过,这里还有一个问题:我们在代码区pthread_create的时候,系统不仅要在库中创建管理线程的控制块 ,还要在内核中创建对应的轻量级进程 【本质就是使用clone系统调用完成的】。而这个轻量级进程需要线程的入口函数地址线程栈的地址 ,未来该线程产生的局部数据都将写入到自己独立的栈中 。到这里,我们会发现,库中管理线程的控制块就只负责记录一些线程的基本属性【线程id,线程状态,线程栈,栈的大小......】。但是,轻量级进程中维护的LWP、时间片、线程入口函数地址、线程独立栈地址......等属性才真正被cpu识别然后进行线程调度。因此,我们将Linux中的线程叫做用户级线程,而用户级线程和轻量级进程之间的关系就好像一个记录任务信息一个真正执行任务。

6. 局部存储

在库中的线程描述控制块中,线程局部存储我们还没有说。其实也非常简单,当一个线程需要定义一个全局变量。但是这个线程希望这个全局变量不被其他线程看到,那么我们就在变量前加__pthread修饰即可 。其底层原理就是在每个线程局部存储中都记录这个变量 ,但是虽然它们的变量名相同,但变量的虚拟地址空间 不同。**当线程修改自己的全局变量时,系统就会拿着这个线程的tid找到对应全局变量地址到内存中修改,**这样就可以做到每个线程都有自己的全局变量。

注意:线程局部存储,只能存储内置类型和部分指针。

7. 线程封装

pthread.hpp

复制代码
#ifndef _PTHREAD_H_
#define _PTHREAD_H_

#include <iostream>
#include <pthread.h>
#include <string>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <vector>
// 命名空间 线程模块
namespace pthread_module
{
    static uint32_t num = 1; 

    class pthread
    {
        using func_t = std::function<void()>;

    private:
        static void *routine(void *args)
        {
            pthread *self = static_cast<pthread *>(args);
            self->_isrunning = true;

            // 将_name 写入线程局部存储区
            pthread_setname_np(self->_tid, self->_name.c_str());
            // 回调用户传入的方法
            self->_func();
            return nullptr;
        }

    public:
        pthread(func_t func)
            : _tid(0), _res(nullptr), _isdetach(false), _isrunning(false), _func(func)
        {
            _name = "线程--" + std::to_string(num++);
        }

        // 启动线程
        bool start()
        {
            int n = pthread_create(&_tid, nullptr, routine, this);
            if (n == 0)
            {
                std::cout << _name << "创建成功 " << std::endl;
                return true;
            }
            else
            {
                std::cerr << _name << "创建失败 " << std::endl;
                return false;
            }
            return false;
        }

        // 终止线程
        bool stop()
        {
            if (_isrunning)
            {
                int n = pthread_cancel(_tid);
                if (n == 0)
                {
                    _isrunning = false;
                    std::cout << _name << "终止成功" << std::endl;
                    return true;
                }
                else
                {
                    std::cerr << _name << "终止失败" << std::endl;
                    return false;
                }
                return false;
            }
            return false;
        }

        // 分离线程
        void detach()
        {
            if (_isdetach)
                return;
            _isdetach = true;
        }

        // join线程
        bool join()
        {
            if (_isdetach)
            {
                std::cout << _name << "已经被分离,无法join" << std::endl;
                return false;
            }
            int n = pthread_join(_tid, &_res);
            if (n == 0)
            {
                std::cout << _name << "join成功" << std::endl;
                return true;
            }
            else
            {
                std::cerr << _name << "join失败" << std::endl;
                return false;
            }
            return false;
        }

        ~pthread() {}

    private:
        pthread_t _tid;
        std::string _name;
        bool _isdetach;  // 是否分离
        bool _isrunning; // 是否运行
        void *_res;
        func_t _func;
    };

}

#endif

main.cc

复制代码
#include "pthread.hpp"

using namespace pthread_module;

int main()
{
    std::vector<pthread> pthreads;

    // 创建10个线程
    for (int i = 0; i < 10; i++)
    {
        pthreads.emplace_back([]()
                              {
        int cnt = 3;
        while(cnt--)
        {
            char name[128];
            pthread_getname_np(pthread_self(),name,sizeof(name));
            std::cout<< name <<"正在执行任务"<<std::endl;
            sleep(1);
        } });
    }

    for (int i = 0; i < 10; i++)
    {
        pthreads[i].start();
    }

    for (int i = 0; i < 10; i++)
    {
        pthreads[i].join();
    }

    return 0;
}
相关推荐
YuTaoShao10 分钟前
【LeetCode 每日一题】3010. 将数组分成最小总代价的子数组 I——(解法二)排序
算法·leetcode·排序算法
宴之敖者、21 分钟前
Linux——\r,\n和缓冲区
linux·运维·服务器
LuDvei23 分钟前
LINUX错误提示函数
linux·运维·服务器
未来可期LJ29 分钟前
【Linux 系统】进程间的通信方式
linux·服务器
Abona30 分钟前
C语言嵌入式全栈Demo
linux·c语言·面试
Lenyiin44 分钟前
Linux 基础IO
java·linux·服务器
The Chosen One9851 小时前
【Linux】深入理解Linux进程(一):PCB结构、Fork创建与状态切换详解
linux·运维·服务器
Kira Skyler1 小时前
eBPF debugfs中的追踪点format实现原理
linux
吴维炜2 小时前
「Python算法」计费引擎系统SKILL.md
python·算法·agent·skill.md·vb coding
No0d1es2 小时前
电子学会青少年软件编程(C语言)等级考试试卷(三级)2025年12月
c语言·c++·青少年编程·电子学会·三级