进程线程知识

发布时间:2024年01月23日

一 初识linux线程

?1?线程由来

??????? 我们之前说创建一个进程,要创建进程控制块pcb,进程地址空间,页表,而且我之前的博客中都有意无意的说明这个pcb是描述进程的,是os用来管理进程的,而有了线程后,就要纠正pcb是描述进程这个说法。

????????不过要说清楚线程由来这得先介绍介绍进程地址空间,进程地址空间实际上是进程的资源窗口,因为进程地址空间能让进程看到自己所有的资源,例如我们可以看到堆栈范围,代码段的大小,就像是一个表格记录你家里房产多少,地多少亩,所以说进程地址空间是进程的资源窗口,不过通过这个表格我们并不知道资源在哪里,但是我们知道我们有这些资源,而要访问这些资源,得靠页表

? ? ? ??

????????父进程创建子进程时,子进程也会创建自己的地址空间结构体(mmstruct),页表等结构,可以认为父进程会直接把自己的资源拷贝一份给子进程,为了效率,这种拷贝是一种写实拷贝,但我们为了方便理解就认为此时子进程已经有一份自己的资源了,可是linux中有一种"进程",我们父进程创建时只需要创建一个pcb对象,这种特殊的进程就是线程,此时线程的资源显然是直接用父进程的资源,那怎么用父进程的资源呢,通过父进程资源窗口和页表不就能看到父进程的资源了吗。

????????当我们知道了进程内是有多个线程pcb的时候,此时就要更正一个观点了,这个pcb就不是描述进程的,而是描述一个线程,又称为执行流的,之前都是一个进程内就一个线程,这个线程等价于进程,那表示线程的pcb说是表示进程的也没错。后面我们学线程创建的时候就知道线程是被我们调用某个函数创建出来的,显然最开始的时候进程一定会自带一个pcb,后面创建一次,进程多一个pcb,但是第一个pcb代表主线程,后面的均认为是次线程。
?????????既然是多个线程共享进程资源,那大家如何执行不同的代码呢,后面我们就知道是创建线程的函数让线程指向不同的代码块,其实也就是让pcb指向不同的代码。

2 线程特性

? ? ? ? 前面的线程概念说了linux创建线程就只创建一个pcb,需要的资源,和原先的进程分享,接下来就详细谈谈线程的特性,这个特性是一般认为操作系统中的线程应该是具有什么样的特点,但是并非是规定具体的os应该如何实现。

1?线程在进程内部执行,什么意思呢

????????在linux看来,线程在进程内部执行,其实是在进程的地址空间内运行。因为线程就是一个执行流,是执行流就必须要有代码和数据吧,它的代码从哪来呢,我们之前说了线程是和主线程共享这个地址空间,如下图。

????????显然线程共享主线程的所有资源,所以线程用的资源就是原来主线程的资源,线程运行不就是个访问资源的过程吗,那不就是只能访问地址空间内的资源,这不就是只能在进程的地址空间内运行吗。

2?线程的执行粒度比进程细

????????为什么说线程的执行粒度比进程细,实际上是主线程将自己要执行的代码分给了其它线程那这些线程的代码一般量比较小,所以说执行粒度更细。创建线程的时候我们应该能更好理解如何分代码给其它线程。? ? ? ?

二 重新定义进程线程

? ? ? ? 1?线程

????????线程是调度的基本单位,以前说进程是 = task_struct+代码和数据,现在来看好像单一的PCB+代码和数据不算一个完整的进程,因为一个进程内会有多个线程,多个线程也会有多个PCB,这也是属于进程的资源,如下图,所以完整的进程包括内部的执行流分支,进程地址空间,页表和代码和数据所占用的内存。由此得,创建进程是什么,也就是准备线程的pcb,准备执行流资源,创建进程地址空间,创建页表,申请内存。

????????还有个观念就是我以前一直认为cpu是调度进程的,并且认为调度进程就是调度进程的pcb,前面已经说了多线程下,进程不仅仅只有一个pcb,所以此时要更正了,由线程是cpu的调度单位得,cpu调度的主要是进程的执行流(又叫线程)的pcb对象,不是仅仅调度进程,对于cpu来说: cpu执行的时候不区分进程线程,都是执行流,但是切换调度的时候肯定是要看情况是切换线程还是进程的,不然怎么知道什么时候只要切换pcb,什么时候页表等数据结构都要被切换。

? ? ? ? 噢,此时我感觉对cpu调度的理解更深刻了一点,在大O(1)调度算法中,是所有的pcb在排队,这些都是线程,都是执行流,此时有一个主线程被cpu执行了,过了一会创建次线程,我想cpu会把这个新创建的次线程抢先放到运行队列,安排下一个切换成这个次线程执行,此时是线程切换。我这里提及到线程抢先因为你想想,如果主线程a执行完下一个就是次线程b,此时就是线程切换,就比较省事,如果是其它进程的线程c,那就要进程切换,要切成次线程b又是进程切换,会比较浪费时间,所以cpu为了效率,一定会尽可能地把主次线程放在一起。

????????有个补充概念是线程的时间片是来自进程的,因为时间片也是资源在主线程内有一个总的时间片,当线程的时间片到了,要切换线程,还要对主线程时间片--,当主线程时间片归零,说明整个进程的时间片到了,此时就是切换整个进程的了。

? ? ? ? 不过上述概念还有点不好理解,要再理一理,线程时间片到了,如何找到主线程对它的时间片做--??我们目前就认为是线程库提供了将所有执行流联系起来的方法,才能让cpu不断地找到这个主线程的时间片做更新和记录,这样os才知道什么时候是让cpu切换线程,什么时候是切换进程。

2?进程

????????进程是os分配资源的基本单位,因为线程使用的都是进程分配的资源,所以说进程是os分配资源的基本单位。创建新的线程现在来看就是创建一个pcb,到了后面我们会补充,还会在共享区创建一个属于线程的堆栈空间,但是这个资源还是在进程的地址空间内,再次验证线程用的还是进程的资源。

????????而以前的进程就是个单执行流,那这个执行流需要线程库管理吗,肯定不用啊,是先有进程,才有的线程概念,才有了线程库来维护多余的执行流。

3 linux实现线程方案

? ? ? ? 显然一个线程要访问什么资源,执行到哪了等等,这些都是描述线程的属性,进程的内核数据结构中的pcb不就保存着进程的状态等属性吗,难道可以复用,没错,linux就是直接复用了原先进程的内核数据结构体pcb,用来描述线程的结构体,所以os只要继续管理调度pcb即可,多了个线程概念对os来说也就是多了点调度和管理的pcb,没什么改变。这就是linux的方案,因为设计linux的人认为,描述进程和线程几乎是一样的,没必要多写一份代码。

? ? ? ? 也就是说而随着线程的出现,linux借用进程的pcb来描述线程,使得进程的含义更加丰富,原先的一个pcb就从表示进程转而变成线程了。难道只提供个pcb就是线程了吗,当然不是,linux设计者开发了一个线程库,对pcb做了封装才是我们使用的线程。

4?线程库由来

????????综上,当os中有进程线程概念时,从cpu视角来看,它调度的执行流有如下关系: 线程<=执行流<=进程,因为执行流可能是一个进程,也可能是一个线程,所以执行流大于等于线程,小于等于进程,所以线程又被称为轻量级进程,至于为什么更轻量化,第四点线程周边概念会提及。?所以Linux中没有明确的线程概念。只有轻量级进程的概念,所以不会提供线程的系统调用,只会提供轻量级线程的系统调用,也就是只提供创建pcb的接口。

? ? ? ? 为什么这么奇怪,只提供创建pcb接口,还要封装成库才为外部提供线程功能,一般不都是直接在系统搞一个线程吗,让os内有一个线程说得倒容易,实际上有很多工作要做的。

????????如果linux中有线程的概念,此时我们就要描述线程,然后再用一些数据结构组织管理线程,哪怕这个结构可以复用,但是我们很多时候可能就是单线程,根本就没有多线程,然后os用来组织线程的数据结构是已经创建好了,就等着维护你创建的线程了,结果没有,虽然多线程有用,但不想设计到linux中让linux代码变得冗余。那线程又有用,显然最后得提供吧,所以linux只提供创建轻量级进程的接口,然后封装成动态库,这样用户要用线程,就包含对应的的库,这样的好处就是可以几乎不增加os的管理代码,而且动态库代码是共享的,这样就不会使得一个进程一份代码,非常冗余。

????????可是我们后面时候的时候却发现gcc还要带-l选项指定库名,pthread库不是linux自己做的库吗,怎么会变成第三方库呢?这其实是由于pthread库在发展中,逐渐脱离了linux内核维护者的维护,而变成由第三方去维护这个库,所以在后面的linux发布版本中,用的都是第三方设计的pthread库。

????????c++对这个线程库又做了封装,使用时同样要带-lthread,这就证明了底层是封装了pthread库。?c++语言写线程创建的代码,在linux和在windows都可以跑,应该是安装的语言库在底层实现了两份代码,分别调用了不同的原生线程库,又调用了不同的系统调用。但是提供给用户接口是一样的,保证在不同的平台下使用是一致的。不过最重要的是g++编译时要以c++11标准编译。

void threadRun()
{
    int num = 0;
    while(1)
    {
        std::cout<<"我是次线程,id: "<<getpid()<<" g_val:"<<g_val<<" &g_val "<<&g_val<<std::endl;
        if(num++ > 10)
            break;
        sleep(1);
    }
}
int main()
{
    thread t1(threadRun);
    t1.join();
    return 0;
}

5 windows实现线程方案

????????Windows实现线程方案则是用一个特定的tcb结构描述线程,然后实现管理tcb的数据结构,本来一堆pcb的调度已经挺麻烦的了,现在每个pcb还跟着一堆线程的tcb,调度的时候就不是简简单单的管理pcb了,现在还要对pcb对应的一堆线程tcb做增删改查,显然要对原先系统的代码做比较大的改动,原先代码经过多次测试和修改,已经没有太大问题了,现在大改势必会出现更多的bug,显然linux这种不大改原先代码也提供线程的方案是更卓越的。

?三?再谈地址空间

????????1 虚拟地址如何转为物理地址

????????首先虚拟地址是32位的,再来看看页表的结构,首先肯定不是一整块的,然后左边放虚拟地址,右边放物理地址,还要权限位,假设一行十个字节,而虚拟地址有2^32个,那此时页表需要2^32个映射行,一行十个字节,那总共就需要40个G,这说明首先肯定不是一个一个地址去映射。

????????那之前是把内存按4kb划分的,那就只需要2^20行,只要来一个虚拟地址,取高位到低位的20位用来映射对应行,找到内存块的起始地址,后12个比特位用来映射具体地址即可,最后占用空间也就40mb,但是还是有一点点不足,因为进程申请内存是这里分散的,不可能遍布2^20个内存块,也就不需要那么多行,毕竟有些已经是内核的了,所以没必要一下子定义一个这么大的数组,实际结构如下图,页表由两级页表构成。

????????首先虚拟地址有32个比特位,前10个比特位是转为一级页表的下标,而一级页表内存的是二级页表的地址,中间10个比特位转为二级页表的下标,二级页表保存页框的起始地址,后12个比特位用于给页框起始地址转为具体物理地址的偏移量。保证了精确对应某个物理地址。

? ? ? ? 求页表总大小,一个二级页表也就占4kb,对应4mb的物理内存,而最多会有1024个二级页表,那就需要4mb,页目录预计只占1kb。而且一个进程不可能一下子用完全部内存,所以对于一个进程来说,二级页表是不全的,而且进程只管理3G,所以二级页表不可能达到1024个,则页表总大小可能连1mb都没有这种映射方式,首先减少了映射的行数,就是因为将页表拆分了,如果用一个有2^20个元素的数组来映射内存会造成浪费,因为有一大部分内存可能都不属于进程,但我们还是留了映射行。

????????那能拿到物理首地址,如何拿到所有数据,如何知道这个数据有多少个字节呢,所以访问变量要指定类型,就是为了连续读数据,显然这个类型是给cpu,先找到物理地址,然后cpu根据类型天然知道要读取几个字节,也就会控制硬件读取多少字节。

? ? ? ? 页目录其内可以先不填数据,也就是说二级目录可以没有,cpu内有个寄存器,保存引起异常的虚拟地址,因为有些时候是数据暂时还未加载到内存,此时引起缺页中断,然后要申请内存,创建二级目录,再访问,此时上次的虚拟地址是肯定短时间内要用的,当然要保存到cpu内。

????????当我们了解了页表,就可以理解给线程分配资源本质就是给虚拟地址范围,然后通过页表找到资源,例如划分代码,如何让线程执行不同的代码块,肯定是pcb内的某个成员记录了各自代码的地址,后面创建线程的时候我们就知道是直接把不同的函数给线程,不同函数地址天然不同,线程也就可以指向不同的代码了,函数执行完也就结束了,终止地址都不用保存了。如果有些资源没有划分,例如全局数据区,那就所有线程共享。当然,每个线程执行的代码,访问的数据可能重复,这可能出现问题,这不就是需要线程同步和互斥了吗。

四 线程与进程周边概念

1 为什么线程比进程更轻量化

? ? ? ? (1) 因为创建释放更轻量化,只需要创建释放一个pcb就可以了。

? ? ? ? (2)?切换更轻量化,为什么说切换更轻量化呢?因为切换的上下文少了,页表,地址空间都不需要从cpu放下来,恢复时就恢复得更少,不过就几个寄存器,为什么说线程切换效率更高呢?因为在cpu中有一个cache区域,是用于缓存进程高频使用的数据,而要把进程高频使用的数据缓存起来,称为加热,这个是比较耗费时间的,而切换线程不需要重新缓存数据到cache了,因为你线程高频数据不就是进程高频数据的一部分吗,所以线程切换不需要加热,所以比进程切换快。????????

2?线程优点

12.18

? ? ? ? 健壮性降低是指: 线程之间并非是独立的,进程间才是独立的,当一个线程出问题,整个进程,也就是进程内的所有线程都会挂掉。缺乏访问控制也是健壮性降低的原因。

3 线程的独立数据

线程id和线程的上下文,以及线程的栈,栈只有一个如何独立,后提。???????

? ? ? ? 据前面所说,线程是调度单元,那每个线程就一定要能被区分,所以那每个线程就要有自己的id。这个id存哪呢,感觉存在pcb内。

五 线程创建

????????1 pthread_create

参数1?输出型参数,用来返回线程的id,注:这个id并非是线程标识符

参数2?用来设置线程的属性,一般不手动设置,传NULL默认即可。

参数3?指明线程要执行的函数,这个是线程的入口函数,可以控制线程执行不同的代码,此时就让该线程变成了新的执行分支。

????????探究传入函数的参数和返回值,都是void*类型的,就是为了能接收各种类型(linux下指针大小为8字节),之所以返回值是void*,也是没办法,因为不知道你函数会return?什么类型的数据,又不能写个void,就只能写个void*,让外部获取后自行处理。

至于参数4,则是给线程执行入口函数传的参数。

?2 pthread_create使用

?????????由主线程创建新线程,创建新线程后,将来新线程会执行传入的函数,主线程继续往后执行。创建成功返回0,失败返回出错原因,不设置errno。

void* threadRotiune(void*)
{
    while(true)
    {
        std::cout<<"我是次线程,id: "<<getpid()<<std::endl;
        sleep(1);
    }
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,threadRotiune,NULL);
    while(true)
    {
        std::cout<<"我是主线程,id: "<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

???????

????????这两个线程的执行是并发的。编译时,给次线程传了threadRotiune函数,而主线程继续往后执行,这两个执行流的pcb就指向了不同的代码块,主次线程执行完各自的函数就结束了,这就实现了执行代码的分离。

????????但是我们g++编译时却会报出链接时报错,因为pthread库是第三方库,g++编译要加-l+库名选项。但是由于已经安装到系统路径下了,所以不用指明库和头文件的所在路径。

????????单执行流下,这两个死循环肯定是不可以同时跑的,所以必然是多个执行流分开执行两个死循环代码,而且有os在帮忙切换两个执行流。

????????但是ps ajx查却发现只有一个进程,要用ps aL查启动的轻量级进程,又或者说是执行流。如下图,能查到两个执行流,他们的pid都一样,显然多线程下执行流间不用pid来区分,tty是终端名,CMD可能是进程名,LWP是什么?主线程的pid咋和LWP一样,不过主次线程的LWP却不一样,显然LWP就是执行流间的标识符。

????????os调度凭借lwp来分辨,根本不看pid,没有了解线程的时候,为什么都是用pid来区分呢,因为之前没有其它线程的时候,此时只有一个主线程,主线程的pid等于lwp,所以说用pid来标识也没有问题。kill -9 + LWP可以给指定线程发信号,反正os能通过LWP找到线程,有意思的是任一个线程收到信号所有线程都会退出,显然这里任意一个线程都能找到所有的执行流,我先前也提过进程内的执行流肯定是有联系的。? ? ?

????????主次线程都调用某个show函数。

????????此时显示结果也很有意思,之所以new thread和main thread一同显示,我认为是次线程打印到new thread就被切换了,然后就到了主线程打印main thread+换行,刷新缓冲区,才会一同显示,此时show函数就被重入了。

????????线程间共用地址空间,能看到同一份资源,一个修改,另一个也能看见,天然可以通信。

六 线程控制

????1?线程id

????????在线程创建时第一个参数就是返回线程id,这个线程id是什么呢,打印如下图,怎么和lwp和pid都不一样,前面说过为动态库的线程库在共享区会管理新创建的线程,会保存一些有关线程的属性结构体,而这个线程id就是线程属性在共享区的地址。

????????主,新线程谁先被调度不知道,但是主不退,新线程退出,应该也会出现类似僵尸进程的情况,但查不到这种情况,当然,常理来看,我们肯定是要等待回收的,因为我们需要知道这个线程有没有做好我交代的事情。

?2 线程等待函数

????????会阻塞等待指定线程的,出错码是直接返回的,而不是设置在errno中,免得多线程对一个变量进行覆盖。

参数1?线程的id,又叫tid? ? ? ? 参数2?首先我们知道次线程执行的函数是会返回void*的,而参数2就是外部用来获取这个函数的返回值,是个输出型参数,因为返回值是void*,返回值存在线程内,可能是pcb,也可能在共享区,phread_join要获取这个,只能传个变量的地址,也就是二级指针。join的时候一般不考虑异常,因为真出异常的话,主线程也没了,更无法考虑异常了。

3 非阻塞join等待

????????而让线程分离,就是告诉系统我不关心这个线程的返回值,线程退出后直接回收就好了。而且不管是可以线程对自己进行分离操作,也可以对其它线程进行分离操作,

????????分离了后就不可以join了,join会返回22号错误码。注意:分离线程只是修改线程的属性,表示线程是否可以被join,此时被分离的线程还是共享着进程资源,而不是直接把线程分出去了。

七 线程终止

1?终止线程

????????不能用exit,这是终止进程的,如果在线程内用,会直接终止进程,而不是仅仅终止当前线程。不过为什么pthread_exit的参数是void*呢,显然和函数返回值的意义是一样的。不过次线程的return?只会终止当前线程,而main函数退出了,则整个进程都会退出了。

2 取消运行中的线程

? ? ? ? 参数还是线程的tid,前提是该进程已经启动。若线程以这种方式结束,此时join接受的退出码为-1。

????????在进程那里我们学过进程替换,线程也有程序替换,在任意的线程内做程序替换,所有线程都被销毁,按照新程序重新分配线程资源。

3 再谈线程执行函数

????????前面只是大致说了线程的执行函数的参数是void*,那我们就可以发散一下思维,例如传个类对象,也可以返回一个类对象指针。

class Request
{
public:
    Request(int start,int end,const string& threadname)
    :_start(start)
    ,_end(end)
    ,_threadname(threadname)
    {
        ;
    }
    int _start; 求和起始数
    int _end;    求和终止数
    string _threadname; 线程名
};
class Response 返回类型
{
public:
    Response(int result,int exitcode)
    :_result(result)
    ,_exitcode(exitcode)
    {
        ;
    }
    int _result;记录计算结果
    int _exitcode;记录退出码
};

void* sumcount(void* argv)
{
    Request* req = static_cast<Request*>(argv);
    Response* res = new Response(0,0);//在堆上new一个对象
    for(int start = req->_start;start < req->_end;start++)
    {
        res->_result+=start;
    }
    return (void*)res; 返回计算结果
}

int main()
{
    pthread_t id;
    Request* req = new Request(1,100,"thread->1 ");
    int num = 0; 
    while(true)
    {

        std::cout<<"我是主线程,id: "<<getpid()<<std::endl;
        if(num++ > 5)
            break;
        sleep(1);
    }
    cout<<"开始等待次线程"<<endl;
    void* ret;
    pthread_join(id,&ret);
    Response* res = static_cast<Response*>(ret);
    cout<<"回收成功: "<<"计算结果为:"<<res->_result<<" 退出码为: "<< res->_exitcode<<endl;
    delete req;
    delete res;
    return 0;
}

????????static_cast用于非多态类型间的转换。一般转换的两种类型最好是有联系的,例如int和double都是整型家族的。

? ? ? ? 显然堆空间也是共享的,因为在主线程new的对象,次线程可以使用,而且次线程new的对象,主线程也可以delete处理。后面我们在讲线程id的时候会再提及线程的栈不是共享的。

4 线程id真面目

? ? ? ? 先来尝试获取一下,pthread_self可以获取调用该函数的线程的tid。

????????linux创建轻量级进程的系统调用:clone。

????????fn为函数指针,这就验证了前面所说在pcb是会记录自己的执行流的执行函数,所以各个线程pcb记录了不同的函数,也就能执行不同代码child_stack是要传入一个自定义栈,保证每个pcb都指向自己的栈,还有其它参数不关心。?因为每个线程都会有自己的调用函数链,所以它们的栈帧结构是必须独立的,否则压栈出栈不就乱套了,那这个栈在哪呢?共享区,那为什么要在共享区呢?因为线程库是要维护线程概念的,也就是管理线程的属性,而线程的栈也是线程的属性,也要被管理,所以都在线程库里,也就都在共享区了。

????????还有就是我们先前说了linux中没有线程概念,只有轻量级进程,线程是库来维护的,由于会有多个线程,也就会有多个线程的属性要被管理。实际上动态库内部是维护了一个数组,每个元素是包含线程的属性,局部存储地址,栈地址这些的集合,这个集合就可以看做是库级别的线程tcb。而线程tid就是对应线程属性存放位置的地址

? ? ? ? 可是什么是线程的属性呢,线程属性不是已经复用pcb管理完了吗,简单理解就是线程有些属性还是和进程属性不同的,所以需要库维护,例如线程的栈。线程库就是一些代码,也就是在共享区申请了一些空间用于维护栈,不用维护线程执行流,线程执行流就可以看做是底层的pcb结构体,这个有os管理就可以了。

????????由此得虽然线程又被称为轻量级进程,但是轻量级进程是内核层面的,而线程是对这一内核做封装的,所以这两个并不能完全划等号,这一点要注意。

????????既然我们要用这个库里的代码,因为线程库是动态库,显然线程库就要加载到内存中。为什么不是静态库呢,因为每个进程都要创建线程,也就都要执行创建方法,没必要一个进程创建一次,就把代码拷贝到自己的程序中,所以动态库只保存一份在内存,其它的进程映射使用即可。噢,总而言之,是在线程库内对线程做的描述,也是线程库内对线程的属性做管理,但是内存中只有一个线程库,由此说明其实所有进程创建的线程都在一个库内被管理。???????

八 线程补充

1 LWP和线程id

????????LWP和线程id是分别给os区分执行流,线程库管理线程的。但是线程库要对自己管理的线程属性做管理,为了方便搜索线程属性,设计者就把线程属性存放位置的虚拟地址返回给了用户。那为什么不用LWP呢,反而整出个线程id,假如用LWP,这个时候有个人对linux不太了解,但是想使用linux的线程,此时他发现这个LWP是什么呢,你说它是线程id吧,进程也有LWP,使用者要了解了轻量级进程,才知道为什么进程线程都有LWP,这会增添很多负担。

2 演示多线程创建

#include <pthread.h>
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include<vector>
using namespace std;
#define NUM 3
struct threadData
{
    string threadname;
};
string toHex(pthread_t tid)将地址转为16进制并存入buf数组中,%x只格式化32位
                     而tid转为地址却有64位,所以这里会发生截断,应该用%lx或者%llx来格式化
{
    char buf[128] = {0};
    sprintf(buf,"0x%llx",tid);
    return buf;
}
void *threadRun(void *arg)
{
    threadData* td = static_cast<threadData*>(arg);
    int num = 0;
    while (true)
    {
        cout << getpid() <<" "<<td->threadname<<" "<<toHex(pthread_self())<<endl;
        sleep(1);
        if(num++ > 10)
            break;
    }
    return nullptr;
}
int main()
{
    vector<pthread_t> vt;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        threadData* td = new threadData;这个变量必须在堆上
        td->threadname = "thread->"+ to_string(i);
        pthread_create(&tid, nullptr, threadRun, td); 不然会给次线程传一个局部变量的地址
                                                    会造成野指针
        vt.push_back(tid);
    }
    for(int i = 0; i < NUM;i++)
    {
        pthread_join(vt[i],nullptr);
    }
    return 0;
}

?显示结果如下:

3?栈独立验证

我们修改一下上面的一个函数。

void *threadRun(void *arg)
{
    threadData* td = static_cast<threadData*>(arg);
    int num = 0;
    while (true)
    {
        cout << getpid() <<" "<<td->threadname<<" 线程id: "<<toHex(pthread_self())<<" num:"<<num<<" "<<&num<<endl;
        sleep(1);
        if(num++ > 10)
            break;
    }
    return nullptr;
}

????????在线程调用函数内定义一个变量,每个线程都对其进行修改,打印如下,显然每个线程的栈帧是独立的。

????????前面说了线程栈主要是为了维护线程调用函数的栈帧,而且每个执行流有一个栈,看似是独立的,但是不意味着进程间的执行流不可以互相访问栈上元素,接下来就验证互相访问。

????????用一个全局指针获取线程局部变量的地址即可验证是否可以访问,不过我觉得如果次线程都运行完了,主线程才访问这个局部变量应该会野指针。

????????当一个全局变量被多个线程访问时,这个全局变量就是共享资源,访问时是会出问题的,要在线程同步互斥再讨论,那如何给线程定义一个私有的变量呢????????

?????????定义时加上_ _thread,这就是线程的局部存储。

__thread int g_val = 0;

????????打印结果如下图。只能用于内置类型。

????????所以在共享库内,一个线程属性就包括一个属性结构体,一个栈和一个局部存储。简单理解动态库管理线程属性,就是定义了一个比较大的数组,所有进程的线程属性都在这里统一管理。

????????减少系统调用的使用,这样每个线程都会保存自己的线程id,如下代码。

__thread int number = 0;
__thread int pid = 0;

????????最大的用处就是在线程的调用函数中都可以直接使用线程局部存储的变量,这样如果在调用函数中我们还要获取线程id,就可以直接用而不需要再次获取了。

文章来源:https://blog.csdn.net/m0_62792369/article/details/134766854
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。