linux入门---多线程的控制

目录标题

线程库

要想控制线程就得使用原生线程库也可以将其称为pthread库,这个库是遵守posix标准的,与线程有关的函数都在这个库里面,并且绝大多数函数的名字都是以pthread_打头,那么要想使用这些函数库,要通过引入头文<pthread.h>,链接这些线程函数库时要使用编译器命令的"-lpthread"选项,那么这就是线程库的大致概念接下来我们就来理解一下这个库中的几个函数。

pthread_create

我们看看这个函数的声明:

第一个参数是输出型参数表示线程的id值,第二个参数表示线程的各种属性比如说创建线程的栈有多大,不过大部分情况下这个参数我们都不用太关心,就好比进程之类的也有属性比如说优先级之类的但是我们很少关心这些属性,不需要设置这些属性因为设置了也没用我们不关心他也不了解他所以将其直接设置为nullptr就可以了,第三个参数是函数指针就是创建的线程要执行的函数,这个参数最大的意义就是可以将程序的代码进行割裂,每个线程可以分配同样的或者不一样的入口函数相当于将代码块划分成好几个区,让不同的执行流执行不同的代码区,代码区域执行就可以在代码块内定义变量申请空间,那么资源就是通过这样的方式来进行分离和交付,第四个参数表示的就是要传递给这个线程的参数,最后线程创建成功就返回0,失败就返回对应的错误原因,对于传统的一些函数的返回值如果函数执行成功就返回0,失败返回-1,并且对全局变量errno赋值表示错误,但是pthreads函数出错时不会设置全局变量errno,因为errno是全局的,被每一个线程共享,所以一个线程对错误码进行设置后会影响其他的线程,所以大部分线程库函数出错后会将对应的错误码以函数的形式进行返回,那么这就是pthread_create函数的参数的介绍,接下来我们看看如何使用这个函数一次性创建多个线程。

如何一次性创建多个线程

既然要一次性创建多个线程,所以我们得使用vector容器来存储多个线程的pid,然后使用一个循环来不停的调用pthread_create函数来创建线程,那么这里为了方便我们就让多个线程执行同一个函数并且传递同样的参数,主线程将线程创建完成之后肯定还得做自己的事情,所以在创建线程的for循环之后还得添加一个while循环来让主线程一直运行下去以免结束,那这里的代码如下:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<vector>
using namespace std;
void* start_routine(void*args)
//所有创建的线程要执行的函数
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        cout<<"new thread create success name:"<<name<<endl;
        sleep(1);
    }
}
int main()
{
    vector<pthread_t> tids;
    #define NUM 10
    for(int i=0;i<NUM;i++)
    {
        pthread_t tid;
        pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    }
    while(true)
    {
        cout<<"new thread create success! name: main threade"<<endl;
        sleep(1);
    }
    return 0;
}

将程序运行一下就可以看到下面现象:

可以看到多个线程在不停的打印内容,并且我们再创建一个对话窗口查看指定线程的时候就可以看到下面这样的场景:

可以看到当前的程序有11个名为mytest的线程在同时执行,那么这就是一次性创建一批线程的大致做法,那么接下来我们要对这种做法进行改进,上面的代码虽然成功的创建了10个线程,但是每个线程的名字都是一样的,那么这就是第一个要改进的地方我们要给每个创建的线程都添加上一个编号,那么这里的做法就是用一个变量来表示编号,然后创建一个缓冲区将要传递给线程的参数先使用snprintf输出到缓冲区里面,然后再将缓冲区的内容作为参数传递给新创建的线程,这样通过for循环每个线程都可以得到不同的参数,那么这里改进的代码就如下:

cpp 复制代码
int main()
{
    vector<pthread_t> tids;
    #define NUM 10
    for(int i=0;i<NUM;i++)
    {
        pthread_t tid;
        char name_buffer[64];
        snprintf(name_buffer,sizeof(name_buffer),"%s,%d","thread",i);
        pthread_create(&tid,nullptr,start_routine,(void*)name_buffer);
    }
    while(true)
    {
        cout<<"new thread create success! name: main threade"<<endl;
        sleep(1);
    }
    return 0;
}

代码运行的结果如下:

可以看到这次的运行结果与上次的不太一样线程之间的名字好像确实不一样,但是大家仔细的观察一下就可以发现问题,根据我们的代码创建出来的线程的名字应该是从0到9,但是这里好像没看到0到3啊,这是为什么呢?那么这里我们就先做出一些修改将创建线程的for循环里面添加一个sleep函数让其没创建一个线程就休息1秒看看打印的结果如何:

可以看到这次运行的结果就符合我们的预期0-9都出现了,那这里就存在一个问题不加sleep的时候为什么创建的线程的编号会出现不全的现象而加上sleep之后却不会出现呢?原因很简单创建的新线程谁先运行是不确定的,而

我们传递给函数的不是缓冲区本身而是缓冲区的地址,并且当前循环里面的内容较为确定所以name_buffer即使被销毁了也会在同一个地方创建,这就导致了之前创建的线程还没有正式运行,name_buffer就已经销毁创建销毁创建到了其他内容并且缓冲区的地址还没有发生变化,所以就会出现我们看不到一些线程名的存在,那该如何处理这个问题呢?总不能一直指望sleep这种降低程序运行速度的函数来解决该问题吧,所以我们就采用类的方式来存储线程的名字,也就是用类来描述线程的名字,这个类里面存在一个字符数组用来存储线程的名字,还有一个pthread_t变量用来存储线程的tid,那么这里的代码如下:

cpp 复制代码
class ThreadDaTe
{
public:
    char name_buffer[64];
    pthread_t tid;
};

那么在创建线程的for循环里面就直接在堆上创建一个ThreadName对象,snprintf函数就直接往这个对象里面的name_buffer写入内容,调用pthread_create函数就直接传递TreadName对象的地址和对象中tid成员的地址代码如下:

cpp 复制代码
for(int i=0;i<NUM;i++)
{
    ThreadDate *td=new ThreadDate();
    snprintf(td->name_buffer,sizeof(td->name_buffer),"%s,%d","thread",i);
    pthread_cre	ate(&td->tid,nullptr,start_routine,td);
}

运行的结果如下:

可以看到这里没有休眠这里也出现了0-9,原理就是每次new的地址都是不一样的所以内容都不一样,指针随着循环销毁所以每次指针的内容也不一样,因为我们把ThreadDate对象的地址传递了过去,所以在执行的函数里面我们可以将该地址的类型进行转换变成ThreadDate*类型这样我们就可以对这个结构体里面的内容进行操作,然后在函数结束的时候再使用delete来销毁这个对象即可,那么这里的代码如下:

cpp 复制代码
class ThreadDate
{
public:
    char name_buffer[64];
    pthread_t tid;
};
    void* start_routine(void*args)
    //所有创建的线程要执行的函数
    {
        ThreadDate* td=static_cast<ThreadDate*>(args);
        int cnt=10;
        while(cnt)
        {
            
            cout<<"new thread create success name:"<<td->name_buffer<<" cnt: "<<cnt<<endl;
            --cnt;
            sleep(1);
        }
        delete td;
        return nullptr;
    }

int main()
{
    vector<ThreadDate*> tids;
    #define NUM 10
    for(int i=0;i<NUM;i++)
    {
        ThreadDate *td=new ThreadDate();
        snprintf(td->name_buffer,sizeof(td->name_buffer),"%s %d","thread",i);
        pthread_create(&td->tid,nullptr,start_routine,td);
        tids.push_back(td);
    }
    for(auto &iter:tids)
    {
        cout<<"create thread: "<<iter->name_buffer<<" : "<<iter->tid<<"success"<<endl;
    }
    while(true)
    {
        cout<<"new thread create success! name: main threade"<<endl;
        sleep(1);
    }
    return 0;
}

运行的结果如下:

可以看到这里打印的结果很乱,但是符合我们的预期。但是这里存在一个问题:当我们创建了多个线程,这些线程执行同一个函数,所以该函数一定是被多个线程执行的,所以当前的函数就是可重入的状态,所以得判断一下当前的函数是否会是可重入函数,我们函数里面创建了变量转换了指针,那这里会不会因为多线程执行而出现问题呢?答案是不会的(这里不考虑向显示器显示内容出现的问题),我们可以通过下面的代码来进行证明:

cpp 复制代码
void* start_routine(void*args)
//所有创建的线程要执行的函数
{
    sleep(1);
    ThreadDate* td=static_cast<ThreadDate*>(args);
    int cnt=10;
    while(cnt)
    {
        cnt--;
        cout<<"cnt: "<< cnt <<" &cnt "<<&cnt<<endl;
        sleep(1);
    }
    delete td;
    return nullptr;
}

代码的运行结果如下:
可以看到这里cnt变量的地址都不一样,因为函数内定义的变量都叫做局部变量具有临时性,这个特性不仅在之前的语言模式中适用,在现在的多线程情况下也没有问题,因为每一个线程都有自己的独立的栈结构,每个线程中创建的变量存放到各个进程的栈结构里面,不同线程中创建的变量不会发生冲突。

线程的终止

线程函数结束return的时候线程就算终止了,但是不能使用exit来终止线程因为exit是用来终止进程的,任何一个执行流调用exit函数退出线程都会导致整个进程被终止,所以得使用pthread_exit来终止线程

哪个执行流调用这个函数哪个执行流就会退出而不会影响其他的执行流,参数的意义我们后面再谈这里直接传递nullptr就行,我们可以用下面的代码来进行对比:

cpp 复制代码
void* start_routine(void*args)
//所有创建的线程要执行的函数
{
    sleep(1);
    ThreadDate* td=static_cast<ThreadDate*>(args);
    int cnt=10;
    while(cnt)
    {
        cnt--;
        sleep(1);
        cout<<"cnt: "<< cnt <<" &cnt "<<&cnt<<endl;
        exit(0);

    }
    delete td;
    return nullptr;
}

可以看到这里的大部分线程还没有往显示器上显示内容就被终止了,那么这就是exit函数的指向效果,将exit函数改成pthread_exit再来看看执行的结果如何:

可以看到这里的除了主线程其他的线程执行了一次for循环就结束了,而且一个执行流的结束并不会影响其他的执行流,那么这就是线程终止的方法一个是直接return退出,另外一个就是调用pthread_exit函数退出。但是这里有一个问题这两个退出的方式一个要返回一个void类型的指针,一个调用函数传递一个void类型的指针,那这个指针的作用是什么呢?我要是想得到线程返回的值该怎么做呢?pthread_exit函数的参数又表示着什么意思呢?那么接下来我们就要聊聊线程的等待的问题。

线程的等待

线程也是要被等待的,如果不等待的话也会造成类似僵尸进程的问题---内存泄漏。线程等待干的事情有:1.获取新线程的退出信息()2.回收新线程对应的pcb等内核资源防止内存泄漏,但是线程级别的内存泄漏问题并没有僵尸线程这样的概念,这个现象我们看不出来但是依然得对其进行回收,当然我也可以完全不关心这个退出信息,但是不关心线程的退出信息也得对线程进行等待。要想实现线程等待就得调用pthread_join函数,我们来看看这个函数的声明:

第一个参数表示要回收的线程id,第二个参数是一个二级指针这个指针的作用我们后面再谈这里直接传递nullptr即可,如果等待成功了就返回0,一般都不会等待失败除非传递的线程id有问题,那么我们上面的代码创建了一堆的线程,所以在回收的时候就得创建一个for循环来一个一个的回收,当回收成功之后就顺带打印回收线程的值,那么这里的代码如下:

cpp 复制代码
class ThreadDate
{
public:
    char name_buffer[64];
    pthread_t tid;
};
void* start_routine(void*args)
//所有创建的线程要执行的函数
{
    sleep(1);
    ThreadDate* td=static_cast<ThreadDate*>(args);
    int cnt=10;
    while(cnt)
    {
        cnt--;
        cout<<"cnt: "<< cnt <<" &cnt "<<&cnt<<endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    vector<ThreadDate*> tids;
    #define NUM 10
    for(int i=0;i<NUM;i++)
    {
        ThreadDate *td=new ThreadDate();
        snprintf(td->name_buffer,sizeof(td->name_buffer),"%s %d","thread",i);
        pthread_create(&td->tid,nullptr,start_routine,td);
        tids.push_back(td);
    }
    for(auto &iter:tids)
    {
        cout<<"create thread: "<<iter->name_buffer<<" : "<<iter->tid<<"success"<<endl;
    }
    for(auto &iter:tids)
    {
        int n =pthread_join(iter->tid,nullptr);
        assert(n==0);
        cout<<"join:"<<iter->tid<<" success "<<endl;
        delete iter;
        //外面统一释放因为上面打印的时候还要访问内存上的数据
        //如果函数中释放这里再访问可能就非法了
    }
    
    return 0;
}

代码的运行结果如下:

可以看到最后打印的数据就可以看到线程的回收成功了。那么接下来我们就来解答一下上面的问题:线程返回的和函数pthread_join传递的void*指针的作用是什么?我们说线程等待的时候需要回收线程对应的系统资源然后按照需求得到线程的返回信息,pthread_join函数的第一个参数表示要等待哪个线程,那么第二个void**类型的参数就和获取返回信息有关它是一个输出型参数,return返回的和函数pthread_exit的void*指针的效果是一摸一样的,用来获取线程函数结束时返回的退出结果,而pthread_join函数的第二个参数就专门用来获取记录退出结果void*指针,因为要把其他函数中的一级指针的内容输出到main函数中所以第二个参数的类型得是二级指针,这就跟swap函数中传递的不是变量的值而是变量的地址是一样的道理,那么接下来就用下面的代码来带着大家理解,首先修改一下记录名字的类,增加一个成员表示线程的编号:

cpp 复制代码
class ThreadDate
{
public:
    long long number;
    char name_buffer[64];
    pthread_t tid;
};

然后在创建线程的for循环里面就将循环次数i作为该线程的编号:

cpp 复制代码
for(int i=0;i<NUM;i++)
{
    ThreadDate *td=new ThreadDate();
    td->number=i;
    snprintf(td->name_buffer,sizeof(td->name_buffer),"%s %d","thread",i);
    pthread_create(&td->tid,nullptr,start_routine,td);
    tids.push_back(td);
}

然后在线程函数返回的时候就返回类中的number变量,因为该变量是个整数而返回的类型是void*所以得做强制类型转换

cpp 复制代码
void* start_routine(void*args)
//所有创建的线程要执行的函数
{
    sleep(1);
    ThreadDate* td=static_cast<ThreadDate*>(args);
    int cnt=2;
    while(cnt)
    {
        cnt--;
        cout<<"cnt: "<< cnt <<" &cnt "<<&cnt<<endl;
        sleep(1);
    }
    return (void*)td->number;//warning
}

这里的返回就相当于void* ret = (void*)td->number也就是将一个整型的数字写到了一个指针变量里,那么在main函数获取这个返回值的时候就得先创建一个void*类型的指针变量,然后将该变量的地址传递给pthread_join函数:

cpp 复制代码
 void *ret=nullptr;
 int n =pthread_join(iter->tid,&ret);

那么在这个函数里面就相当于创建了一个void*retp的变量然后*retp= return (void*)td->number;这样就将返回的值放到了指针变量ret里面,然后就可以打印返回的内容:

cpp 复制代码
for(auto &iter:tids)
{
    void *ret=nullptr;
    int n =pthread_join(iter->tid,&ret);
    assert(n==0);
    cout<<"join:"<<iter->tid<<" success,number:"<<(long long)ret<<endl;
    delete iter;
}

那么这里运行的结果就如下:

可以看到这里确实得到了线程返回的信息,所以上述过程简单的描述一下就是:线程执行函数的返回值放到线程库里面,因为不能在main函数中直接访问库中的内容,所以得通过函数pthread_join到线程库中获取线程函数的返回值,我们上面是返回的假的地址也就是用整数冒充的地址它都可以获取成功,那么我们未来要是返回堆上的地址,对象的地址等等都是没有任何问题,但是不能返回栈上的空间因为线程结束的时候会将它的栈释放。但是这里有个问题?之前学习进程等待的时候我们不仅可以获取进程退出对应的退出码,还可以获取对应的异常,那线程退出的时候能拿到对应的信号吗?答案是不行的因为信号是整体发给线程的,所以pthread_join函数默认函数会调用成功不考虑异常的问题异常问题是进程应该考虑的。

线程取消

在上面的学习过程中我们知道了两种线程终止的方式一个是通过return 来终止线程,另外一个是调用pthread_join函数来终止线程,那么这里我们来介绍第三个线程终止的方式也就是线程取消,线程是可以被取消的但是取消的前提是该线程已经跑起来了,当线程跑起来之后就可以调用phtread_cancel函数来取消线程,该函数的声明如下:

参数就表示要被取消的线程id,当线程被取消之后就可以看到线程函数的返回值就是-1,这个-1其实是一个宏PTHREAD_CANCLDE,比如说下面的代码:

cpp 复制代码
class ThreadDate
{
public:
    long long number;
    char name_buffer[64];
    pthread_t tid;
};
void* start_routine(void*args)
//所有创建的线程要执行的函数
{
    // sleep(1);
    ThreadDate* td=static_cast<ThreadDate*>(args);
    // int cnt=2;
    while(true)
    {
        sleep(1);
    }
    return nullptr;

}

int main()
{
    vector<ThreadDate*> tids;
    #define NUM 10
    for(int i=0;i<NUM;i++)
    {
        ThreadDate *td=new ThreadDate();
        td->number=i;
        snprintf(td->name_buffer,sizeof(td->name_buffer),"%s %d","thread",i);
        pthread_create(&td->tid,nullptr,start_routine,td);
        tids.push_back(td);
    }
    for(auto &iter:tids)
    {
        cout<<"create thread: "<<iter->name_buffer<<" : "<<iter->tid<<"success"<<endl;
    }
    for(auto &iter:tids)
    {
        pthread_cancel(iter->tid);
        cout<<"pthread cancel:"<<iter->name_buffer<<" success "<<endl;
    }
    for(auto &iter:tids)
    {
        void *ret=nullptr;
        int n =pthread_join(iter->tid,&ret);
        assert(n==0);
        cout<<"join:"<<iter->name_buffer<<" success, exit_code: :"<<(long long)ret<<endl;
        delete iter;
    }
    return 0;
}

运行的结果如下:

可以看到被取消的线程得到的函数返回值就是-1,也就是退出码为-1,那么这就是线程取消的特点。

分离线程

默认情况下新创建的线程是joinable的,当线程退出之后是需要对其进行pthread_join操作,否则无法释放线程申请的内核资源从而造成系统泄漏,但是这么做是有个前提:我们关心线程的返回结果需要查看线程返回的信息,所以我们得手动查看信息并顺便回收资源,那我们要是不关心线程的返回值呢?join是不是就成为了一种负担,万一在编写程序的时候忘记了释放资源还会照成内存泄漏啊,所以面对这种情况我们就想当我们不需要查看线程的返回信息时能不能让他自动的回收资源呢?所以这个时候就有了一个新的概念叫做分离线程pthread_self函数可以获取本线程的id,该函数的声明如下:

该函数不需要参数哪个线程调用这个函数这个函数就返回哪个线程的id,然后

使用pthread_detach可以使线程进行分离,该函数的声明如下:

该函数需要传递线程的id意思就是分离哪个线程,如果分离成功了就返回0分离失败了就返回对应的错误码,那么接下来我们就可以写一段代码来验证一下线程分离的这个概念,首先还是老的套路使用pthread_create创建一个线程,然后在线程执行的函数里面的我们就可以使用pthread_self函数和pthread_self函数将线程分离,为了不让线程结束的那么快,我们可以让线程循环的执行5秒,那么这里的代码就如下:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
using namespace std;
void* start_routine(void* args)
{
    string thread_name=static_cast<const char *>(args);
    pthread_detach(pthread_self());
    int cnt=5;
    while(cnt)
    {
        cout<<thread_name<<" runing.... "<<endl;
        cnt--;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    return 0;
}

因为当一个线程被分离的话是不能被等待的,所以我们就可以根据pthread_join函数的返回值来判断线程是否被分离成功:

cpp 复制代码
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    int i=pthread_join(tid,nullptr);
    if(i==0)
    {
        cout<<"join success"<<endl;
    }
    else
    {
        cout<<"result : "<<i<<":"<<strerror(i)<<endl;
    }
    return 0;
}

我们首先将线程分离的代码屏蔽一下,再执行一下程序就可以看到下面这样的现象

可以看到这里等待成功了,我们将线程分离的代码接触屏蔽再运行一下看看结果如何:

可以看到这里依然是等待成功了,那这是为什么呢?因为主线程和子线程谁先执行是不知道的,所以可能子线程还没有分离成功,主线程就已经阻塞式等待了,那么这个时候主线程依然会等待子线程,所以就会出现上面的情况,我们让主线程在等待之前先休息几秒钟然后在等待就可以看到下面这样的现象:

可以看到这里的等待就失败了,那么因为这个现象的存在我们在分离线程的时候一般让主线程来分离子线程,而不是子线程自己分离自己,比如说下面的代码:

cpp 复制代码
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    pthread_detach(tid);
    int i=pthread_join(tid,nullptr);
    if(i==0)
    {
        cout<<"join success"<<endl;
    }
    else
    {
        cout<<"result : "<<i<<":"<<strerror(i)<<endl;
    }
    return 0;
}

代码运行的结果如下:

可以看到程序的运行出现了错误,那么这就是线程的分离。

如何看待其他语言支持的多线程

任何语言要想在linux中如果要实现多线程,必定要使用pthread库,如何看待C++11中的多线程呢?c++11的多线程在linux环境中的本质都是对pthread库的封装,我们可以使用下面的代码来进行验证:

cpp 复制代码
#include<iostream>
#include<thread>
#include<unistd.h>
using namespace std;
void thread_run()
{
    while(true)
    {
        cout<<"我是新线程"<<endl;
        sleep(1);
    }
}
int main()
{
    thread t1(thread_run);
    while(true)
    {
        cout<<"我是主线程"<<endl;
        sleep(1);
    }
    t1.join();
    return 0;
}

makefile中的代码如下:

cpp 复制代码
mytest:test.cc
	g++ -o $@ $^ -std=c++11 -l pthread
mytest1:test1.cc
	g++ -o $@ $^ -std=c++11 -l pthread
.PHONY:
clean:
	rm -f mytest

可以看到我们当前的执行命令是告诉了库的名称的,将程序运行一下就可以看到下面这样的结果:

确实有两个执行流在不停的执行,并且使用指令 ps -aL查看轻量级进程的时候也可以看到确实有两个名为mytest1的轻量级进程:

如果我们要是将-l pthread去掉也就是不告诉操作系统有个名为pthread的库的话会出现什么样的现象呢?

可以看到这里是无法正常运行的(但是这里的报错我有点没预料到,因为在make的时候就应该报错说没有找到线程库,这里没有报错所以这里就仅作参考吧),那么这就说明c++虽然也有线程库但是这个线程库在linux环境中依然是对pthread库进行的封装。

线程id的本质

根据前面的学习我们知道操作系统中存在着进程地址空间和页表:

然后当我们每创建一个线程时都会创建一个PCB然后指向主进程的进程地址空间然后通过页表访问物理内存上的内容:

但是我们知道linux操作系统时没有提供创建线程的接口的,只提供了创建轻量级进程的接口clone,并且这个clone函数还十分的难用,比如说下面的图片:

所以因为操作系统不给我们提供创建多线程的接口而我们又只认可线程,所以就有人在用户程序员和操作系统之间提供了一个库,这个库就是原生线程库,当一个程序员使用线程的时这个线程肯定存在着很多的属性比如说线程的状态,线程的优先级,线程的栈结构等等,而且每个程序员都可以创建线程,一个机器又可以被多个程序员使用,所以一个操作系统中肯定会存在多个线程,那操作系统要不要对线程进行管理呢?答案是肯定得做管理,并且操作系统也有能力对其做管理,但是这里有个问题用户并不是直接从操作系统中申请的线程啊,他是先向原生线程库申请的线程,然后这个库再将其转换成为轻量级进程然后再向操作系统申请,一个机器可以被多个人使用而我用这个库提供的接口创建了线程那别人也可以使用这个库创建线程,所以在线程库里面也会存在多个线程,那线程库里面肯定得对创建的线程进行管理,管理的方式也是先描述再组织,描述就是对线程的属性进行描述但是这个描述很少,因为操作系统已经帮我们实现了一部分,所以pthread库中存在一些结构体描述线程,操作系统中也存在一些结构体描述轻量级进程的属性,并且库中的结构体和线程库中的结构体是一 一对应的,所以linux解决线程的方案就是用户级线程,用户关心的线程属性在库中,内核提供线程执行流的调度,linux用户级线程和内核轻量级进程的比率为1:1,库中提供属性不管行线程是如何被调度的,线程中的上下文如何,它只关心线程是什么?id是什么?栈的大小是多少?栈在什么位置?以及其他线程的属性,这些属性都是由库来维护的,那组织又是怎么做的呢?库只不过是一个磁盘文件,当我们创建的进程中用到了库文件是操作系统就会将这个库加载进内存当中然后映射到进程的地址空间中,在之前的学习过程中我们提到过进程地址空间中有一个区域为共享区,而pthread库映射到进程地址空间的时候实际上映射的就是共享区

这样用户就可以直接通过进程地址空间上的共享区和页表然后访问内存上的线程库,线程被创建时除了在内核中创建对应的PCB,还要在库中创建描述线程的结构也就是图片中的这个区域:

在这个结构体中就有线程的id,线程的局部存储,线程栈等等,每创建一个线程就创建一个描述线程的结构体(可以称之为TCB)然后就用数组将其组织起来,每一个线程在数组中都有起始地址所以就可以通过地址来访问对应线程的属性,而我们之前所说的那个地址就是线程对应在库中的数组的某个元素的地址,有了这个地址之后我们就可以访问线程的属性,之前我们说每个线程都有属于自己的栈结构那么这个栈就位于线程库当中,而主线程的栈在传统的栈上,所以这也是为什么当同一个函数被多个线程一起调用的时候不会出现可重入的问题,而我们之前说线程执行的函数在返回的时候会将信息进行返回,那么这个信息实际上就暂时的存储到库中对饮的元素当中,当我们使用join函数获取信息的时候得传递tid实际上该函数就是通过这个地址找到对应的数组元素,然后从该元素中获取对应的信息,那么当我们创建线程时是通过库来帮我们创建的,而库又是调用函数clone来进行创建

第一个函数就是要执行的函数,第二个函数就是对应的栈,在函数里面会创建好对应的栈结构然后将栈的起始地址传递给child_stack,然后线程可以使用child_stack这个栈而不是主线程的栈,那么这就是线程id的理解。

线程的局部存储

在之前的学习中我们知道线程中的大部分资源所有线程都是共享的,比如说创建了一个全局变量主线程对其进行修改,新线程就直接访问打印代码如下:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<string.h>
using namespace std;
int num=100;
void* start_routine(void* args)
{
    string thread_name=static_cast<const char *>(args);
    while(true)
    {
        cout<<thread_name <<" num: "<< num<< " &num= "<<&num<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    pthread_detach(tid);
    while(true)
    {
        num++;
        cout<<"我是主线程"<<" num: "<<num<< " &num= "<<&num<<endl;
        sleep(1);
    }
    return 0;
}

代码的运行结果如下:

可以看到新线程打印的结果和主线程打印的结果一摸一样的并且两个执行流打印的地址也是一样的,那么这就说明两个执行流访问的是同一个变量,但是我们在全局变量的前面添加__thread再运行一下看看结果如何:

cpp 复制代码
__thread int num=100;

可以看到这会两个线程打印的结果就不太一样了,而且两个线程获取到的地址也是不一样的,那么这就说明他们访问的是两个不一样的变量,所以__thread的功能就是将一个内置类型由之前的全局变量设置为线程的局部存储,没有__thread修饰的局部变量每一个线程都能够共享,被__thread修饰的全局变量每一个线程内部都有一份所以上面的地址不一样值也不一样,全局变量位于已初始化区域所以我们上面看到的地址较低,而共享区的地址较高所以后面我们看到的地址明显就大多了,那么这就是__thread的功能。

线程的封装

学了上述的几个函数,那么这里我们就来尝试着对线程进行封装,首先我认为每个线程都应该有个名字,所以类里面应该存在一个string变量用来记录名字,然后线程有对应的id所以类中还得有一个变量用来存储线程的tid,因为线程执行函数的时候有对应的参数,所以还得存在一个变量用来存储执行函数的参数,因为线程被创建出来要执行各种各样的函数,所以这里就可以使用functional创建一个对象用来接收各种各样的参数,那么这里的代码如下:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<string>
#include<funtional>
class Thread
{
    typedef std::function<void*(void*)> func_t;
public:

private:
    pthread_t _tid;//记录线程的tid
    std::string _name;
    void* _agrs;
    func_t _func;
};

类中的成员变量确定了接下来就要实现类的构造函数,我们希望类对象一经创建就可以创建线程执行对应的函数,所以构造函数的第一个参数就是函数对象,第二个参数就是函数的参数,第三个参数就是线程的编号:

cpp 复制代码
Thread(func_t func,void* agrs=nullptr,int number =0)
:_func(func)
,_args(args)
{
    
}

然后我们要干的事情就就是给线程创建一个名字,首先创建一个缓冲区然后使用snprintf将名字输入到缓冲区里面,最后将缓冲区的内容传递给_name就行:

cpp 复制代码
Thread(func_t func,void* agrs=nullptr,int number =0)
:_func(func)
,_agrs(agrs)
{
    char name_buffer[1024];
    snprintf(name_buffer,sizeof(name_buffer),"thread -%d",number);
    _name=name_buffer;
}

线程的名字创建完成之后我们就可以使用调用pthread_create函数来创建线程并执行函数,但是这里存在一个问题 ,传递给pthread_create的是函数指针但是我们这里是用function来接收的函数无法进行传递,所以我们这里可以再创建一个函数,在函数里面的调用_func对象即可,比如说下面的代码:

cpp 复制代码
void*tmp_func(void* args)
{   
    return _func(args)
}
Thread(func_t func,void* args=nullptr,int number =0)
:_func(func)
,_args(args)
{
    char name_buffer[1024];
    snprintf(name_buffer,sizeof(name_buffer),"thread -%d",number);
    _name=name_buffer;
    pthread_create(&_tid,nullptr,tmp_func,_args);
}

但是这么会存在一个问题mp_func函数是类中的函数它有一个隐藏的this参数,而pthread_create函数要求传递的函数只能由一个void*指针,所以直接这么传递肯定是不行的,所以这个时候有人会试着用static修饰来去掉this指针,但是static修饰的函数只能访问类中静态的成员变量和函数而_func对象和_agrs指针都是非静态的函数无法访问,所以这个时候有人又会说将这两个变量也修改为静态的不就可以了吗?但是这么做就会让函数和参数就属于类了而不是属于对象,也就是说每个对象可以调用的函数和参数都是一样的了,所以该方法是不可取的我们得另外寻找一个方法,首先可以确定的一点就是不能传递function对象,只能传递类中的静态函数(这里就不考虑友元函数和函数指针,因为后面的方法可以设计涉及的知识点),所以如何在静态函数中访问类中的非静态成员?那么这里就可以再创建一个类,类中有两个指针变量一个用来记录原本函数的参数一个用来记录Thread对象的地址:

cpp 复制代码
class context
{
public:
    void* _args;
    Thread* _this; 
    context()
    :_args(nullptr)
    ,_this(nullptr)
    {}
    ~context()
    {}
};

然后执行静态函数的时候我们就可以传递一个指向context对象的地址过去,然后在函数里面对地址的类型做出转换这样我们就可以访问context对象里面的内容,然后context里面又有Thread类型的指针这样就又可以访问Thread对象里面的内容,所以我们就可以再在Thread对象里面创建一个函数让其执行function对象的内容,这样就可以实现上面的内容:

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<string>
#include<cassert>
#include<functional>
class Thread;
class context
{
public:
    void* _args;
    Thread* _this; 
    context()
    :_args(nullptr)
    ,_this(nullptr)
    {}
    ~context()
    {}
};
class Thread
{
    typedef std::function<void*(void*)> func_t;
public:
    static void*tmp_func(void* args)
    {   
        context* ctx =static_cast<context *>(args);
        void* ret=ctx->_this->run(ctx->_args);
        delete ctx;
        return ret;
    }
    Thread(func_t func,void* args=nullptr,int number =0)
    :_func(func)
    ,_args(args)
    {
        char name_buffer[1024];
        snprintf(name_buffer,sizeof(name_buffer),"thread -%d",number);
        _name=name_buffer;
        context* ctx=new context();
        ctx->_args=args;
        ctx->_this=this;
        pthread_create(&_tid,nullptr,tmp_func,ctx);
    }
private:
    pthread_t _tid;//记录线程的tid
    std::string _name;
    void* _args;
    func_t _func;
};

然后我们就可以添加一个join函数,这个函数用来回收执行完成的线程,那么这个函数里面也就是调用pthread_join函数来实现的:

cpp 复制代码
void join()
{
    int n =pthread_join(_tid,nullptr);
    assert(n==0);
    (void)n;
}

最后就是析构函数这个函数这里不需要做任何事情直接为空就行,那么接下来我们就可以用下面的代码来进行测试:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<string>
#include<string.h>
#include"Thread.hpp"
using namespace std;
void* start_routine(void* args)
{
    string s1=static_cast<const char *>(args);
    while(true)
    {
        cout<<"我是新线程,我的参数是:"<<s1<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    Thread t1(start_routine,(void *)"one two three",1);
    while(true)
    {
        cout<<"我是主线程"<<endl;
        sleep(1);
    }
    return 0;
}

代码的运行结果如下:

符合我们的预期那么这就是线程控制的全部内容。

相关推荐
荼靡6033 分钟前
shell(三)
linux·服务器·数据库
zym大哥大13 分钟前
Linux的权限
linux·服务器
Stark-C15 分钟前
功能齐全,支持协作 | Docker部署一款支持多人共享的私密浏览器『n.eko』
运维·docker·容器
sheng12345678rui20 分钟前
mfc100u.dll是什么?分享几种mfc100u.dll丢失的解决方法
游戏·microsoft·电脑·dll修复工具·1024程序员节
嘟嘟Listing36 分钟前
设置jenkins时区记录
运维·jenkins
嘟嘟Listing37 分钟前
jenkins docker记录
java·运维·jenkins
伴野星辰37 分钟前
小乌龟TortoiseGit 安装和语言包选择
linux·运维·服务器
枫叶丹444 分钟前
【在Linux世界中追寻伟大的One Piece】多线程(一)
java·linux·运维
残念ing1 小时前
【Linux】—简单实现一个shell(myshell)
linux·运维·服务器
明月心9521 小时前
linux mount nfs开机自动挂载远程目录
linux·运维·服务器