Linux线程篇(上)

今天为大家讲一讲Linux中的线程。这部分的知识细节比较多,篇幅可能较长,但我们一步一步将每一个知识点搞清楚,Linux线程对我们来说就小菜一碟啦!

线程的概念:

众所周知,linux中每创建出一个概念,都会使用先描述再组织的方法。例如进程我们使用PCB来描述的,文件我们用struct_file描述并用文件描述符表进行管理。但是Linux中其实并没有线程这个概念,而我们这里所说的线程本质是一个轻量级进程(后面我们详细讲)所以Linux就没有自己的系统调用来创建一个线程,而是有系统调用创建一个轻量级进程!因此想要在Linux环境下使用,就必须调用pthread库。

线程的本质:

当一个进程运行时并创建出一个线程,OS为该系统创建出和进程一摸一样的pcb,该pcb中的虚拟地址空间和进程中的一摸一样,因此线程是通过和主进程看到同一份资源来运行的。它没有自己的虚存,页表,和主进程共享一份,因此它和进程的区别就是它变轻了,因此线程本质就是一个轻量级进程。线程一旦被创建,线程和进程的大部分资源都是共享的。线程就是进程里面的一个执行流

所以cpu在调度一个进程的时候,它看到的一个一个的pcb,而今天的pcb它可能是一个进程,也可能是一个线程,但是cpu不管是进程还是线程,只要调度就运行pcb里的代码和数据。因此线程是cpu调度的基本单位。

在今天我们对进程有了新的认识,以前我们知道进程=内核数据结构+数据代码。而今天进程是承担系统资源分配的基本实体。进程就相当于一个大家庭,而线程就是家庭成员,家庭成员共同努力是为了让这个家变得更好。线程也是共同努力合作完成一个任务。

线程的调度:

线程的创建:

以下是创建一批线程的代码:

第一个参数是线程的pid,第二个参数我们默认填nullptr,第三个参数是线程调用的方法,是四个参数是调用方法的形参。

class ThreadData
{
public:
    int number;
    pthread_t tid;
    char namebuffer[64];
};

void *start_routine(void *args)
{
    // sleep(1);
    // 一个线程如果出现了异常,会影响其他线程吗?会的(健壮性或者鲁棒性较差)
    // 为什么?进程信号,信号是整体发给进程的!

    ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
    int cnt = 10;
    while (cnt)
    {
        cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; // bug
        cnt--;
        sleep(1);
        cout << "new thread create success, name: " << td->namebuffer << " cnt: " << cnt-- << endl;
       
    }
}


int main()
{

     vector<ThreadData*> threads;
#define NUM 10

     for(int i = 0; i < NUM; i++)
        {
 
            ThreadData *td = new ThreadData();
            td->number = i+1;
            snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
            pthread_create(&td->tid, nullptr, start_routine, td);
            threads.push_back(td);
    
            // pthread_create(&tid, nullptr, start_routine, (void*)"thread one");
            // pthread_create(&tid, nullptr, start_routine, namebuffer);
            // sleep(1);
        }

这时就会有人困惑,为什么不能将namebuffer在外面创建好,直接用下面的方式创建线程:

pthread_create(&tid, nullptr, start_routine, namebuffer);

因为创建一批线程,在for循环里面所有的资源都是共享的。所以当你把namebuffer当作参数传入的时候。当参数传给start_routine完毕时,线程才完全构建好。因为线程运行的优先顺序是随机的,速度非常快。所以可能前面进程还没有完全的讲自己的namebuffer完全构建好,就被后面的进程刷新覆盖了(namebuffer出了作用域被销毁,下一个线程被创建时,由于函数栈帧的原因,使用的nabuffer和上一个线程使用的位置一样,资源共享)。所以使用传入new出来的指针的方法,每一个线程看到的是独自的空间。

 在任务中,局部变量cnt、td是独立的,因为每一个线程又拥有一个独立栈,当我们打印cnt的地址时,发现它们各自的地址是不一样的。

线程退出:

线程退出有两种方法:一种是返回值退出,另一种是使用系统调用。

那为什么不能使用exit?之前我们讲过exit退出是操作系统向进程发信号最后杀死进程。如果使用的话进程里的所有线程就会被杀死,就不能达到单个线程退出的目的。

返回值退出很简单,我们可以直接返回一个nullptr,如果你想传其他的值你也可以这样做:

但如果你想返回的不是一个数而是一组数,你也可以返回一个类的地址。但前提是类对象必须是new出来的,因为出了作用域战阵被销毁,返回的指针必将是一个野指针。

class ThreadReturn
{
public:
    int exit_code;
    int exit_result;
};


 ThreadReturn * tr = new ThreadReturn();
    tr->exit_code = 1;
    tr->exit_result = 106;
    return &tr;

 另一种方法是使用pthread_exit():

 参数就是你需要线程退出时返回的值。

线程取消:

线程也是可以被取消的,记住:取消线程前的其前提是这个线程已经开始跑起来了!!

线程等待:

线程和进程一样需要被等待,因为需要拿到线程退出的信息,以及回收对应线程的pcb资源:

首先我们学习一下pthread_join这个接口:

第二个参数是用来接收线程的返回值的,它是一个输出型参数便于我们获取返回值的结果,以下是具体的用法:

为什么要在用户栈上定义一个变量传到pthread_join里面呢?其背后的原理如下:

当线程执行一个任务并且结束时,它的返回值是要被保存到pthread库里面去的,当我们将上面的ret的地址传进去,并且调用pthread_join时,将返回值赋值给了*ret。这样就直接把返回值赋值给了ret,以下是实例图:

线程的分离:

当一个线程进行分离以后,它就不需要被join了,如果join,就会报错:

现在我们来谈一谈线程id的作用:

进程需要被操作系统先描述再组织,那么线程需要也需要这样吗?答案是:是的。

在我们创建线程的时候,第二个参数是一个输出型参数,它就是一个结构体,它的作用就是描述一个线程并进行更好的管理。

 我们都知道当我们使用线程的时候,需要引入线程库,线程库被加载到虚拟地址空间的共享区中,而线程库中就是我们创建的一个一个线程,并以结构体的方式管理起来:

所以线程id就是每个线程在动态库的起始位置,通过起始地址就可以找到每一个线程线程独立的栈结构也是在共享区,并被每一个pthread_attr_t这个结构体管理着,所以线程各自的数据不会相互影响。而主线程的栈就是在虚存的栈区!!

那么上图中的线程局部存储是什么呢?我们用代码来演示:

 int num =0;

void* task(void* args)
{

    while(1)
    {

        cout<<"我是一个新线程,num:"<<num<<" "<<"&num:"<<&num<<endl;
        sleep(1);
        ++num;
    }
}

int main()
{

    pthread_t tid;
    int n =pthread_create(&tid,nullptr,task,(void*)"new thread");
    assert(n==0);
    (void)n;

    while(1)
    {
        cout<<"我是一个主线程,num:"<<num<<" "<<"&num:"<<&num<<endl;
        sleep(1);
    }

    return 0;
}

以上代码的结果:

毋庸置疑,因为num是全局变量,一个线程对该值做修改,另一个线程也会看到改变后的值。所以两个线程看到的是同一个num,那么进行如下修改(thread前面加两个杠),结果会变成怎样呢?

以下是运行结果:

 可以看到,两个线程看到的是不同地址的num,并且地址要比之前大了不少。原因就是加了__thread以后数据就变成了线程的局部存储,该值是存储在共享区的,已初始化代码区的地址比共享区的地址低,所以看到地址变大了。因为是局部存储,所以新线程对该值做修改不会影响其他进程。

到这里线程的控制就全部结束了,接下来我们封装以下线程库中的接口,使它和c++一样使用起来更加方便:

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <assert.h>
using namespace std;

class Thread;
class Context
{
public:
    Context()
        : _this(nullptr), _args(nullptr)
    {
    }

    ~Context()
    {
    }

public:
    Thread *_this;
    void *_args;
};

class Thread
{

public:
    typedef function<void *(void *)> func_t;

    static void *task(void *args)
    {
        // 静态成员不能访问类内的非静态成员,所以必须将类的上下文传进来
        Context *ctx = static_cast<Context *>(args);
        void *ret = ctx->_this->_func(ctx->_args);
        delete ctx;
        return ret;
    }

    Thread(func_t func, void *args = nullptr, int num = 0)
        : _func(func), _args(args), _num(num)
    {
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "Pthread %d", _num);
        _name = namebuffer;

        Context *text = new Context();
        text->_this = this;
        text->_args = _args;

        // 调用c式的接口识别不出来C++的东西,如_func。
        // pthread_create(&_tid,nullptr,_func,_args);
        int n = pthread_create(&_tid, nullptr, task, text);
        assert(n == 0);
        (void)n;
    }

    void join()
    {
        int n = pthread_join(_tid, nullptr);
        assert(n == 0);
        (void)n;
    }

    ~Thread()
    {
    }

private:
    pthread_t _tid;
    int _num;
    string _name;
    func_t _func;
    void *_args;
};

所以到后面的学习,我们就可以用我们写好的线程,这样使用起来更加方便:

到这里线程控制的讲解就全部结束了,创作不易谢谢大家的支持!

猜你喜欢

转载自blog.csdn.net/m0_69005269/article/details/130670668
今日推荐