当前位置: 首页 > news >正文

网站建设考虑哪些因素/东莞网站建设优化技术

网站建设考虑哪些因素,东莞网站建设优化技术,政府网站开发预算,广州天河区网站设计公司目录标题 线程库pthread_create如何一次性创建多个线程线程的终止线程的等待线程取消分离线程如何看待其他语言支持的多线程线程id的本质线程的局部存储线程的封装 线程库 要想控制线程就得使用原生线程库也可以将其称为pthread库,这个库是遵守posix标准的&#xf…

目录标题

  • 线程库
  • 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;
}

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

相关文章:

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

目录标题 线程库pthread_create如何一次性创建多个线程线程的终止线程的等待线程取消分离线程如何看待其他语言支持的多线程线程id的本质线程的局部存储线程的封装 线程库 要想控制线程就得使用原生线程库也可以将其称为pthread库&#xff0c;这个库是遵守posix标准的&#xf…...

基于android的 rk3399 同时支持多个USB摄像头

基于android的 rk3399 同时支持多个USB摄像头 一、前文二、CameraHal_Module.h三、CameraHal_Module.cpp四、编译&烧录Image五、App验证 一、前文 Android系统默认支持2个摄像头&#xff0c;一个前置摄像头&#xff0c;一个后置摄像头 需要支持数量更多的摄像头&#xff0…...

【Qt之控件QTreeView】设置单元格高度、设置图标尺寸

设置列宽 设置高度 自定义代理 继承QItemDelegate&#xff0c;实现sizeHint ()方法&#xff0c;设置自定义委托。 class itemDelegate : public QItemDelegate {Q_OBJECTpublic:explicit itemDelegate(QObject *parent 0) : QItemDelegate(parent){}~itemDelegate(){}virtua…...

力扣42.接雨水(java,暴力法、前缀和解法)

Problem: 42. 接雨水 文章目录 思路解题方法复杂度Code 思路 要能接住雨水&#xff0c;感性的认知就是要形成一个“下凹区域”&#xff0c;则此时我们就要比较当前柱子和其左右柱子高度的关系&#xff0c;易得一个关键的式子&#xff1a;当前小区域的积水 min&#xff08;当前…...

hdlbits系列verilog解答(移位寄存器)-23

文章目录 一、问题描述二、verilog源码三、仿真结果 一、问题描述 您将获得一个具有两个输入和一个输出的模块 my_dff &#xff08;实现 D 触发器&#xff09;。实例化其中的三个&#xff0c;然后将它们链接在一起以形成长度为 3 的移位寄存器。端口 clk 需要连接到所有实例。…...

Linux命令记载

服务器基本操作 SSH登录服务器 ssh -p 端口号 用户名服务器IP 输入密码SFTP上传文件 #输入密码 #使用get命令下载远程服务器的文件&#xff0c;比如/usr/test.txt sftp>get /usr/test.txt#使用put命令上传本地文件到服务器&#xff0c;比如/usr/test1.txt sftp> put /…...

Flume 快速入门【概述、安装、拦截器】

文章目录 什么是 Flume&#xff1f;Flume 组成Flume 安装Flume 配置任务文件应用示例启动 Flume 采集任务 Flume 拦截器编写 Flume 拦截器拦截器应用 什么是 Flume&#xff1f; Flume 是一个开源的数据采集工具&#xff0c;最初由 Apache 软件基金会开发和维护。它的主要目的是…...

【pandas技巧】group by+agg+transform函数

目录 1. group by单个字段单个聚合 2. group by单个字段多个聚合 3. group by多个字段单个聚合 4. group by多个字段多个聚合 5. transform函数 studentsgradesexscoremoney0小狗小学部female958441小猫小学部male938362小鸭初中部male838543小兔小学部female909314小花小…...

一文解读WordPress网站的各类缓存-老白博客

缓存是一种重要的WordPress优化手段&#xff0c;用于提高网站的性能和加载速度。减少计算量&#xff0c;有效提升响应速度&#xff0c;让有限的资源服务更多的用户。本文老白博客便从自己的使用简单给大家介绍下WordPress的缓存&#xff0c;包括 站点缓存&#xff08;Page Cach…...

从零开始:开发直播商城APP的技术指南

时下&#xff0c;直播商城APP已经成了线上购物、电子商务的核心组成&#xff0c;本文将为您提供一个全面的技术指南&#xff0c;帮助您从零开始开发一个直播商城APP。我们将涵盖所有关键方面&#xff0c;包括技术堆栈、功能模块、用户体验和安全性。 第一部分&#xff1a;技术…...

GZ035 5G组网与运维赛题第6套

2023年全国职业院校技能大赛 GZ035 5G组网与运维赛项&#xff08;高职组&#xff09; 赛题第6套 一、竞赛须知 1.竞赛内容分布 竞赛模块1--5G公共网络规划部署与开通&#xff08;35分&#xff09; 子任务1&#xff1a;5G公共网络部署与调试&#xff08;15分&#xff09; …...

分类预测 | Matlab实现KOA-CNN-GRU-selfAttention多特征分类预测(自注意力机制)

分类预测 | Matlab实现KOA-CNN-GRU-selfAttention多特征分类预测&#xff08;自注意力机制&#xff09; 目录 分类预测 | Matlab实现KOA-CNN-GRU-selfAttention多特征分类预测&#xff08;自注意力机制&#xff09;分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matla…...

【Qt】QString怎么转成int

2023年10月29日&#xff0c;周日晚上 第一种方法 这种方法会尝试将 QString 对象转换为 int 类型。如果转换成功&#xff0c;将返回转换后的 int 值&#xff1b;如果转换失败&#xff08;例如&#xff0c;字符串中包含非数字字符&#xff09;&#xff0c;则返回 0。 QString…...

ubuntu 22.04 安装python-pcl

ubuntu 22.04 安装python-pcl 安装python-pcl修复bug 由于python-pcl库基本已经停止维护&#xff0c;所以Ubuntu22.04 在使用pip install python-pcl安装的时候会出现版本不适配的原因 安装python-pcl 使用Ubuntu22系统自带python3安装python-pcl&#xff0c;随后将下载的包拷…...

【题解】[GenshinOI Round 3 ]P9817 lmxcslD

题目传送门 分析 看到这道题我一开始是有点懵的&#xff0c;但是看了看数据范围&#xff0c;发现有几个点有 n 为质数 的特殊性质&#xff0c;结论先行&#xff0c;大胆猜测是不是可以贪心&#xff0c;所以先打了一个最傻的代码上去试试. void solve(){cin >> n >&…...

在pycharm中,远程操作服务器上的jupyter notebook

一、使用场景 现在我们有两台电脑&#xff0c;一台是拥有高算力的服务器&#xff0c;另一台是普通的轻薄笔记本电脑。如何在服务器上运行jupyter notebook&#xff0c;同时映射到笔记本电脑上的pycharm客户端中进行操作呢&#xff1f; 二、软件 pycharm专业版&#xff0c;jupy…...

SQL 运算符

SQL 运算符 运算符是保留字或主要用于 SQL 语句的 WHERE 子句中的字符&#xff0c;用于执行操作&#xff0c;例如&#xff1a;比较和算术运算。 这些运算符用于指定 SQL 语句中的条件&#xff0c;并用作语句中多个条件的连词。 常见运算符有以下几种&#xff1a; 算术运算符比…...

中间件安全-CVE 复现K8sDockerJettyWebsphere漏洞复现

目录 服务攻防-中间件安全&CVE 复现&K8s&Docker&Jetty&Websphere中间件-K8s中间件-Jetty漏洞复现CVE-2021-28164-路径信息泄露漏洞CVE-2021-28169双重解码信息泄露漏洞CVE-2021-34429路径信息泄露漏洞 中间件-Docker漏洞复现守护程序 API 未经授权访问漏洞…...

系列九、什么是Spring bean

一、什么是Spring bean 一句话&#xff0c;被Spring容器管理的bean就是Spring bean。...

轻量封装WebGPU渲染系统示例<4>-CubeMap/天空盒(源码)

当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/version-1.01/src/voxgpu/sample/ImgCubeMap.ts 此示例渲染系统实现的特性: 1. 用户态与系统态隔离。 2. 高频调用与低频调用隔离。 3. 面向用户的易用性封装。 4. 渲染数据和渲染机制分离。 5. 用户…...

Linux 环境变量 二

目录 获取环境变量的后两种方法 环境变量具有全局属性 内建命令 和环境变量相关的命令 c语言访问地址 重新理解地址 地址空间 获取环境变量的后两种方法 main函数的第三个参数 &#xff1a;char* env[ ] 也是一个指针数组&#xff0c;我们可以把它的内容打印出来看看。 …...

Beyond Compare4 30天试用到期的解决办法

相信很多小伙伴都有在使用Beyond Compare 4软件&#xff0c;如果我们没有激活该软件&#xff0c;就只有30天的评估使用期&#xff0c;那么过了这30天后我们怎么继续使用呢&#xff1f;下面小编就来为大家介绍方法。 打开Beyond Compare4&#xff0c;提示已经超出30天试用期限制…...

sentinel规则持久化-规则同步nacos-最标准配置

官方参考文档&#xff1a; 动态规则扩展 alibaba/Sentinel Wiki GitHub 需要修改的代码如下&#xff1a; 为了便于后续版本集成nacos&#xff0c;简单讲一下集成思路 1.更改pom 修改sentinel-datasource-nacos的范围 将 <dependency><groupId>com.alibaba.c…...

【Linux】tail命令使用

tail 命令可用于查看文件的内容&#xff0c;有一个常用的参数 -f 常用于查阅正在改变的日志文件。 语法 tail [参数] [文件] tail命令 -Linux手册页 著者 由保罗鲁宾、大卫麦肯齐、伊恩兰斯泰勒和吉姆梅耶林撰写。 命令选项及作用 执行令 tail --help 执行命令结果 参…...

【数据结构】面试OJ题——时间复杂度2

目录 一&#xff1a;移除元素 思路&#xff1a; 二&#xff1a;删除有序数组中的重复项 思路&#xff1a; 三&#xff1a;合并两个有序数组 思路1&#xff1a; 什么&#xff1f;你不知道qsort&#xff08;&#xff09; 思路2&#xff1a; 一&#xff1a;移除元素 27. 移…...

LibreOffice编辑excel文档如何在单元格中输入手动换行符

用WPS编辑excel文档的时候&#xff0c;要在单元格中输入手动换行符&#xff0c;可以先按住Alt键&#xff0c;然后回车。 而用LibreOffice编辑excel文档&#xff0c;要在单元格中输入手动换行符&#xff0c;可以先按住Ctrl键&#xff0c;然后回车。例如&#xff1a;...

ideaSSM在线商务管理系统VS开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 SSM 在线商务管理系统是一套完善的信息管理系统&#xff0c;结合SSM框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码 和数据库&#xff0c;系统主…...

数据结构 | 顺序表专题

数据结构 | 顺序表专题 文章目录 数据结构 | 顺序表专题课前准备1. 目标2. 需要的储备知识3. 数据结构相关概念 开始顺序表1、顺序表的概念及结构2、顺序表分类3、动态顺序表的实现初始化顺序表打印顺序表内存容量的检查顺序表的尾插顺序表的尾删顺序表的头插顺序表的头删在顺序…...

C++可视化 有穷自动机NFA 有穷自动机DFA

一、项目介绍 根据正则表达式,可视化显示NFA&#xff0c;DFA&#xff1b;词法分析程序 二、项目展示...

vite vue3 ts 使用sass 设置样式变量 和重置默认样式

1.安装scss 样式支持依赖 yarn add -D sass 2.使用sass <div><!-- 测试使用sass --><h1>测试使用sass</h1> </div><style scope lang"scss"> div {h1 {color: red;} } </style> 效果&#xff1a; 3.通过npm下载并复制…...