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

目录标题

  • 线程库
  • pthread_create
  • 如何一次性创建多个线程
  • 线程的终止
  • 线程的等待
  • 线程取消
  • 分离线程
  • 如何看待其他语言支持的多线程
  • 线程id的本质
  • 线程的局部存储
  • 线程的封装

线程库

要想控制线程就得使用原生线程库也可以将其称为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循环来让主线程一直运行下去以免结束,那这里的代码如下:

#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 10for(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循环每个线程都可以得到不同的参数,那么这里改进的代码就如下:

int main()
{vector<pthread_t> tids;#define NUM 10for(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,那么这里的代码如下:

class ThreadDaTe
{
public:char name_buffer[64];pthread_t tid;
};

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

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来销毁这个对象即可,那么这里的代码如下:

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 10for(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;
}

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

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就行,我们可以用下面的代码来进行对比:

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循环来一个一个的回收,当回收成功之后就顺带打印回收线程的值,那么这里的代码如下:

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 10for(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函数中传递的不是变量的值而是变量的地址是一样的道理,那么接下来就用下面的代码来带着大家理解,首先修改一下记录名字的类,增加一个成员表示线程的编号:

class ThreadDate
{
public:long long number;char name_buffer[64];pthread_t tid;
};

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

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*所以得做强制类型转换

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函数:

 void *ret=nullptr;int n =pthread_join(iter->tid,&ret);

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

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,比如说下面的代码:

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 10for(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秒,那么这里的代码就如下:

#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函数的返回值来判断线程是否被分离成功:

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;
}

我们首先将线程分离的代码屏蔽一下,再执行一下程序就可以看到下面这样的现象
在这里插入图片描述
可以看到这里等待成功了,我们将线程分离的代码接触屏蔽再运行一下看看结果如何:
在这里插入图片描述
可以看到这里依然是等待成功了,那这是为什么呢?因为主线程和子线程谁先执行是不知道的,所以可能子线程还没有分离成功,主线程就已经阻塞式等待了,那么这个时候主线程依然会等待子线程,所以就会出现上面的情况,我们让主线程在等待之前先休息几秒钟然后在等待就可以看到下面这样的现象:
在这里插入图片描述
可以看到这里的等待就失败了,那么因为这个现象的存在我们在分离线程的时候一般让主线程来分离子线程,而不是子线程自己分离自己,比如说下面的代码:

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库的封装,我们可以使用下面的代码来进行验证:

#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中的代码如下:

mytest:test.ccg++ -o $@ $^ -std=c++11 -l pthread
mytest1:test1.ccg++ -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的理解。

线程的局部存储

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

#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再运行一下看看结果如何:

__thread int num=100;

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

线程的封装

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

#include<iostream>
#include<pthread.h>
#include<string>
#include<funtional>
class Thread
{typedef std::function<void*(void*)> func_t;
public:private:pthread_t _tid;//记录线程的tidstd::string _name;void* _agrs;func_t _func;
};

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

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

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

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对象即可,比如说下面的代码:

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对象的地址:

class context
{
public:void* _args;Thread* _this; context():_args(nullptr),_this(nullptr){}~context(){}
};

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

#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;//记录线程的tidstd::string _name;void* _args;func_t _func;
};

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

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

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

#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;
}

代码的运行结果如下:
在这里插入图片描述
符合我们的预期那么这就是线程控制的全部内容。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/172637.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HarmonyOS DevEso环境搭建

DevEco Studio 3.1配套支持HarmonyOS 3.1版本及以上的应用及服务开发&#xff0c;提供了代码智能编辑、低代码开发、双向预览等功能&#xff0c;以及轻量构建工具DevEco Hvigor 、本地模拟器&#xff0c;持续提升应用及服务开发效率。 1.下载 官方网站&#xff1a; HUAWEI De…

数据结构和算法——用C语言实现所有排序算法

文章目录 前言排序算法的基本概念内部排序插入排序直接插入排序折半插入排序希尔排序 交换排序冒泡排序快速排序 选择排序简单选择排序堆排序 归并排序基数排序 外部排序多路归并败者树置换——选择排序最佳归并树 前言 本文所有代码均在仓库中&#xff0c;这是一个完整的由纯…

PTA L1-8 静静的推荐

PTA L1-8 静静的推荐 分数 20 全屏浏览题目 切换布局 作者 陈越 单位 浙江大学 天梯赛结束后&#xff0c;某企业的人力资源部希望组委会能推荐一批优秀的学生&#xff0c;这个整理推荐名单的任务就由静静姐负责。企业接受推荐的流程是这样的&#xff1a; 只考虑得分不低于 175 …

水性杨花:揭秘CSS响应式界面设计,让内容灵活自如,犹如水之变幻

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一、是…

Qt生成PDF报告

文章目录 一、示意图二、实现部分代码总结 一、示意图 二、实现部分代码 //! 生成测试报告 void MainWindow::createPdf(QString filename, _pdf_msg_& msg, const QMap<QString, int>& ok, const QMap<QString, int>& err) {//QDir dir;if(!dir.exis…

异步请求池——池式组件

前言 本文详细介绍异步请求池的实现过程&#xff0c;并使用DNS服务来测试异步请求池的性能。            两个必须牢记心中的概念&#xff1a; 同步&#xff1a;检测IO 与 读写IO 在同一个流程里异步&#xff1a;检测IO 与 读写IO 不在同一个流程 同步请求 与 异步请求…

Unity性能优化一本通

文章目录 关于Unity性能优化一、资源部分&#xff1a;1、图片1.1、 图片尺寸越小越好1.2、使用2N次幂大小1.3、取消勾选Read/Write Enabled1.4、图片压缩1.5、禁用多余的Mip Map1.6、合并图集 2、模型2.1.限制模型面数2.2.限制贴图的大小2.3.禁用Read/Write Enables2.4.不勾选其…

学习笔记:二分图

二分图 引入 二分图又被称为二部图。 二分图就是可以二分答案的图。 二分图是节点由两个集合组成&#xff0c;且两个集合内部没有边的图。换言之&#xff0c;存在一种方案&#xff0c;将节点划分成满足以上性质的两个集合。 性质 如果两个集合中的点分别染成黑色和白色&am…

Pytorch代码入门学习之分类任务(二):定义数据集

一、导包 import torch import torchvision import torchvision.transforms as transforms 二、下载数据集 2.1 代码展示 # 定义数据加载进来后的初始化操作&#xff1a; transform transforms.Compose([# 张量转换&#xff1a;transforms.ToTensor(),# 归一化操作&#x…

【QT开发(15)】QT在没有桌面的系统中可以使用

在没有桌面的系统中&#xff0c;可以使用QT库。QT库可以在没有图形用户界面&#xff08;GUI&#xff09;的环境中运行&#xff0c;例如在服务器或命令行终端中。 这样就可利用Qt的&#xff1a; 对象模型&#xff0c;信号和槽容器类多线程和多进程网络编程 等

wiresharak捕获DNS

DNS解析&#xff1a; 过滤项输入dns&#xff1a; dns查询报文 应答报文&#xff1a; 事务id相同&#xff0c;flag里 QR字段1&#xff0c;表示响应&#xff0c;answers rrs变成了2. 并且响应报文多了Answers 再具体一点&#xff0c;得到解析出的ip地址&#xff08;最底下的add…

CentOS 编译安装 nginx

CentOS 编译安装 nginx 修改 yum 源地址为 阿里云 curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repoyum makecache升级内核和软件 yum -y update安装常用软件和依赖 yum -y install gcc gcc-c make cmake zlib zlib-devel openss…

环形链表(C++解法)

题目 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#…

【计算机网络】认识协议

目录 一、应用层二、协议三、序列化和反序列化 一、应用层 之前的socket编程&#xff0c;都是在通过系统调用层面&#xff0c;如今我们来向上打通计算机网络。认识应用层的协议和序列化与反序列化 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应…

前端《中国象棋》游戏

源码下载地址 支持&#xff1a;远程部署/安装/调试、讲解、二次开发/修改/定制 查看视频 本程序是一个基于Html/css/javascrip的网页端象棋APP&#xff0c;其中引入JQuery来简便开发。 在程序中&#xff0c;使用一个Map二维数组来表示棋盘&#xff0c;通过给棋子设置不同的横坐…

FileWriter文件字符输出流

一.概念 以内存为基准&#xff0c;把内存中的数据以字符形式写出到文件中 二.构造器 public FileWriter(Filefile) 创建字节输出流管道与源文件对象接通 public FileWriter(String filepath) 创建字节输出流管道与源文件路径接通 public Filewriter(File file,boolean append) …

【MySQL】并发事务产生的问题及事务隔离级别

先来复习一下事务的四大特性&#xff1a; 原子性&#xff08;Atomicity&#xff09;&#xff1a;事务是不可分割的最小操作单位&#xff0c;事务中的所有操作要么全部执行成功&#xff0c;要么全部失败回滚&#xff0c;不能只执行其中一部分操作。一致性&#xff08;Consisten…

卷积神经网络的感受野

经典目标检测和最新目标跟踪都用到了RPN(region proposal network)&#xff0c;锚框(anchor)是RPN的基础&#xff0c;感受野(receptive field, RF)是anchor的基础。本文介绍感受野及其计算方法&#xff0c;和有效感受野概念。 1.感受野概念 在典型CNN结构中&#xff0c;FC层(…

vue核心面试题汇总【查缺补漏】

给大家推荐一个实用面试题库 1、前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;web前端面试题库 很喜欢‘万变不离其宗’这句话&#xff0c;希望在不断的思考和总结中找到Vue中的宗&#xff0c;来解答面试官抛出的…

FPGA设计时序约束七、设置时钟不确定约束

一、背景 在之前的时序分析中&#xff0c;通常是假定时钟是稳定理想的&#xff0c;即设置主时钟周期后按照周期精确的进行边沿跳动。在实际中&#xff0c;时钟是非理想存在较多不确定的影响&#xff0c;存在时延和波形的变化&#xff0c;要准确分析时序也需将其考虑进来&#x…