【Linux】线程

打印 上一主题 下一主题

主题 856|帖子 856|积分 2568

目次
线程概念
线程控制


在说线程之前,我们需要铺垫一些背景知识:
   1.重谈地点空间
  实际上,物理内存不是一整块,而是被划分为一份份4KB空间,OS举行内存管理,不是以字节为单位,而是以内存块为单位的,默认大小是4KB,4KB是主流Linux操纵体系用的大小。实际上,我们之前学过,体系和磁盘文件举行IO的基本单位是4KB,也就是8个扇区,全部盘算机中的巧合都是被精心计划过的。在程序被编译完成后,是有地点的,而且以数据块4KB的大小存好了。所谓程序加载,就是把磁盘上4KB的数据块加载到4KB的内存块上。我们把4KB的内存块叫做页框,4KB的数据块叫做页帧, OS对内存的管理工作,基本单位是4KB!
之前父子进程对数据举行修改时会发生写时拷贝,而写时拷贝的基本单位也是4KB,为什么修改很少的数据也要拷贝4KB呢?因为如果这个数据被修改,那其周围的数据大概率也要被修改,每次都写时拷贝对OS是一种负担,这其实是用空间来换时间。
在4GB内存空间中,一共有100多万个页框,OS要将这些页框管理起来,先形貌、再构造,用struct page管理页框,用一个布局体数组管理起来,struct page memory[1048576],用数组管理起来,这样每一个page都有了一个下标,第一个page的起始地点是0,下一个page的起始地点就是1*4KB,以此类推,就可以将每一个page下标转换为每一个内存页框的起始地点。这样,对内存的管理就变为对数组的增删查改。

别的,我们之前在谈页表时,并没有具体说,如果是4GB的地点空间,在页表中一一将捏造地点和物理地点对应,每组对应关系再加上一个状态标记位(按1个字节),那就是一组捏造地点和物理地点对应关系要占用9个字节,那就是36GB,这也太大了吧,所以,实际上肯定不能这样映射。那真实的页表是什么样子的?捏造地点是怎样转换为物理地点的呢?
其实,捏造地点在OS看来不是一个团体,而是将32位拆为10位(2^10=1024)、10位(2^10=1024)、12位(2^12=4096)。起首,将捏造地点的前10位作为索引(第一张表),所以第一张表一共1024项,这张表称为页目次,(页目次最多占4字节*1024=4KB)页目次中的每一项中存放的内容是第二张表(页表)的地点,所以页目次可以指向很多张的页表,每一张页表也是有1024项,根据第二个10位索引页表中的某一项,而页表中某一项存放的内容是指向页框的起始地点,然后再拿上捏造地点的低12位(范围是[0,4095])作为页内偏移,是任一个字节的偏移量。总结来看,先拿着捏造地点的前10位作为页目次的下标去索引,通过页目次这一项的内容找到所指向的页表,然后通过捏造地点的第二个10位作为下标去索引页表的某一项,这个页表的某一项存放的是指向的页框的起始地点,最后根据捏造地点的后12位作为某一个页框内的偏移量,就可以找到任一个字节,也就是说,任意一个捏造地点&(0xFFF)==页框号。实际上,捏造地点的前20位加起来的作用是搜索页框,在加上捏造地点的低12位页内偏移,就能索引到页框里的任意一个字节。这种分配方案我们称为二级页表。
我们来算一笔账,页表里每项2个字节,一个页表就是2KB,一共有1024张页表,所以全部页表加起来一共是2MB,加上一级页目次4KB。
但是如今有些尴尬,我们只能上述方式找到一个字节的地点,所以C/C++里对每个变量都设置了类型,这样就能拿到我们想要的任何数据。

CPU读取的是捏造地点,在CPU中要将捏造地点根据页表转换为物理地点,那CPU怎样找到页表呢?实际上,在CPU中存在cr3,每一个进程会把自己对应的页表中页目次的起始地点放在寄存器当中,这样,捏造地点被读到CPU,找到页表后,通过CPU中的MMU电路直接找到捏造地点,

在地点空间中,正文代码限定了一批捏造地点空间的范围,而且依靠页表才能看到对应的实际物理空间。如今假设正文代码由20个函数构成,给 每一个函数分配一个执行流,那代码由并行转为串行,这在技术上是可行的。我们知道,函数地点其实是一批代码的入口地点,函数的每行代码都有地点,而且同一个函数我们认为地点是连续的,所以函数是连续的代码地点构成的代码块,这意味着一个函数对应一批连续的捏造地点!将这20个函数划分,本质上是对页表举行划分,捏造地点的本质是一种资源!!!
线程概念

先来说一下官方的概念,线程,是在进程内部运行,是CPU调理的基本单位。之前我们学过,进程之间的代码数据和内核数据布局之间都是相互独立的,那如果创建进程时不再创建地点空间和页表,而是只创建一个新的task_struct,假如正文代码分成4份,第1份代码由第1个进程来执行,第二份代码由第2个进程来执行,然后接着创建第3、4个进程,上面我们形貌的就是Linux中的线程,这就是定义的第一句“线程在进程内部运行”。之间我们对进程的定义是进程=内核数据布局+进程代码和数据,如今我们站在内核的角度,给进程定义,进程是负担分配体系资源的基本实体
说到这里,大概还不不是很明白,下面就先讲一个故事:
在我们巨大的国家,分配资源的基本实体是家庭,OS就是国家,家庭就是OS的一个个进程,在我们家庭里,会有爸爸妈妈爷爷奶奶,我们此时大概在学校上课,我们执行上课的代码,爸爸妈妈在上班,他们执行上班的代码,爷爷奶奶在遛弯,他们执行养老的代码,我们家庭的每一个人都在做着自己的事情,我们每一个人把自己的工作做好,这样就达到一个神奇的结果,就能把这个家的日子过好。每一个家庭的使命就是把日子过好,家庭中的每一个人就是一个线程。

对比一下我们之前学的进程,实际上,我们之前学的进程内部只有一个执行流,而如今的进程中有多个执行流,所以只有一个执行流的进程是有多个执行流的进程的特殊情况。

假设我们如今OS要单独计划线程,就要计划新建、停息、销毁、调理,那线程要不要和进程产生关联,这里所说的几点就是要管理线程,先形貌再构造,struct tcp,tcb布局体里面的成员就要有线程的id,优先级,状态,上下文,链接属性,而形貌进程的布局体是struct pcb,每个进程有多个线程,

上图这种计划方案其实是windows中真实存在的线程控制块,CPU在调理时先选择一个进程,再选择此中一个线程。然而,在计划Linux时,发现计划线程时,线程的各种属性(id,优先级,状态,上下文,链接属性等)进程也都有,那为什么还要单独计划一个数据布局tcb来表示线程呢?此外,如果进程和线程计划成两套,那调理算法是不是也要计划成两套?所以,Linux的计划者就想能不能复用pcb,用pcb统一表示执行流,这样的话,我们就不需要为线程单独计划数据布局和调理算法了,这就是Linux的方案,用进程模拟的线程!!!显然Linux的方案更良好。
站在CPU的角度,在Linux中,它所调理的task_struct<=进程,CPU用不用区分task_struct是进程还是线程?不用区分!因为在CPU看起来都是执行流,所以CPU看到的执行流<=进程,所以Linux中的执行流叫做轻量级进程

话不多说,我们先来用代码来见一见线程:
在Linux中,使用pthread_create函数来创建新的线程,

   第一个参数thread是一个线程id,是输出型参数。第二个参数attr是属性,一般设为null,第三个参数start_routine是一个参数为void*、返回值为void *的函数指针,一旦线程创建乐成,主线程继续向下执行,新线程执行这个函数指针所对应的方法。第四个参数args是参数。
  

    如果线程创建乐成返回0,失败返回错误码。
  1. //新线程
  2. void* threadStart(void* args)
  3. {
  4.     while(1)
  5.     {
  6.         std::cout << "new thread running..." << " ,pid: " << getpid() << std::endl;
  7.         sleep(1);
  8.     }
  9. }
  10. int main()
  11. {
  12.     pthread_t tid;
  13.     pthread_create(&tid, nullptr, threadStart, (void *)"thread-new");
  14.     //主线程
  15.     while(1)
  16.     {
  17.         sleep(1);
  18.         std::cout << "main thread running..." << " ,pid: " << getpid() << std::endl;
  19.     }
  20.     return 0;
  21. }
复制代码
运行以上程序,我们发现固然只有一个进程,但是两个执行流可以一起执行,原因就是他们是属于同一个进程的线程。 

其实,我们可以使用ps -aL检察线程,

我们看到这两个线程的pid是一样的,他俩另有LWP(Light Weight Process,轻量级进程),这就是线程的id。别的,我们发现,此中有一个的LWP和pid一样,所以,OS调理的时候,看的是pid还是LWP?肯定是LWP。pid和LWP类似的是主线程,差别的是新线程。
如今我们还是有两个题目:
1.已经有多进程了,为什么还要有多线程呢?
创建一个新进程,既要创建pcb,开发地点空间,又要创建页表,还要加载代码和数据,因此创建进程成本非常高!而创建线程只需要1.创建一个pcb 2.把进程已有的资源给你,因此,创建线程成本非常低(启动)。此外,在运行期间,如果切换进程,需要保存上下文,切换进程页表、地点空间,但是在切换线程时,上下文要掩护起来,但是地点空间、页表就不用切换了,因此运行期间,线程调理成本低(运行)。别的,当删除一个进程时,要开释pcb、地点空间、代码和数据等,而删除线程只需要开释pcb,因此,删除一个线程更简朴(死亡)!
以上只是说的线程的长处,但是它也是存在缺点的。在多线程中,他们共享地点空间、页表、代码和数据,如果此中一个线程出现野指针报错,就是这个进程出异常了,那这个进程就被干掉了,一个线程崩溃会导致其他线程崩溃。所以,如果代码如果没写好,其结实性会比较差。而多进程没有这个题目。进程和线程同时存在时因为它们都有不可取代性。
2.差别体系对于进程和线程的实现不一样?为什么OS讲义只有1本?
我们来回首一下线程的定义,线程,是在进程内部运行,是CPU调理的基本单位,固然差别OS对这句话的实现不一样,但是它们都服从了线程的定义,所以说操纵体系是盘算机界的哲学。
下面我们再来说一道常见的面试题,为什么线程的调理成本更低?
CPU为了加速访问内存的服从,CPU中会存在cache(硬件上),当CPU在访问某一行代码时,会把这一行附近的相关代码和热点数据全部预先加载到CPU的cache中,这一部分称为进程的热数据,所以CPU在访问数据时,先去cache中去找,找到了就直接从cache中拿数据,没找到才会去内存中拿数据,然后将这个数据置换到cache中,我们通过lscpu指令可以检察CPU中的缓存,
 

这就意味着,如果切换进程,除了切换pcb、地点空间、页表,对于cache中的热点数据,切换后的进程用不上,此前保存的数据全部作废,切换进程后,catch里的数据要重新写入,这个过程就比较慢了。而切换线程时,cache之前保存的热点数据大概会用到,不要丢弃所以也就不需要重新加载热数据,所以线程切换服从高。
所以,一个线程去执行一个函数的本质,就是这个线程拥有了正文代码的一小块,就是拿到了一小部分捏造地点空间范围,也就是只使用页表的一部分,每个线程各自使用一小部分捏造地点,所以捏造地点本质上就是一种资源。比如一个线程拿了20行代码,另一个拿了30行代码,不就是也把页表拿了一部分吗。

如果是盘算麋集型应用,并不是创建的线程越多越好,而是谨慎创建合适的数量,一般是和CPU的核数一样。如果是IO麋集型应用,可以答应多创建一些线程。
线程也有很多缺点:
1.结实性降低。一个线程出题目,那这个进程直接终止,所以其他线程就终止了。
2.缺乏访问控制。由于大部分地点空间上的内容都是共享的,每个线程都能看到,一个线程大概把另一个线程的数据修改。
Linux中进程和线程对比
1.进程是资源分配的基本单位
2.进程是资源分配的基本单位
3.线程共享进程数据,但也拥有自己的一部分数据,如线程ID、一组寄存器、栈、errno、信号屏蔽字、调理优先级等。
此中,线程私有的部分中,一组寄存器和栈是最紧张的,一组寄存器中存放的是硬件上下文数据,这反应了线程可以动态运行;栈,线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区。
进程的多个线程共享同一地点空间,因此代码段、数据段都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:


线程控制

在我们编译源文件时,多加了pthread这个库,为什么会这样呢?

其实在Linux中,不存在线程,而只有轻量级进程,而作为OS学习者,只学过创建进程、终止进程、调理进程、等待进程,但是Linux体系只会给上层用户提供创建轻量级进程的接口,所以就需要存在中间软件层--pthread库,是Linux体系自带的,原生线程库,对轻量级进程接口举行封装,按照线程的接口方式,交给用户,这样就保持了Linux体系的纯粹性。
线程创建、线程等待、线程终止

在线程创建时,需要包罗pthread.h,而且在编译时要加上-pthread库。在新线程创建完成后,主线程会继续向后执行,新线程转而会去执行void* threadrun(void* args)对应的代码,所以执行流就一分为二,实际上是并行运行,pthread_create的第四个参数args传给threadrun做参数传入。
同样的,在创建好线程之后,还需要对线程举行等待,使用pthread_join,

一般是由主线程等待新线程,其第一个参数thread就是pthread_create的第一个参数的返回值,第二个参数一般设为nullptr,这个参数实际上是为了得到threadrun的返回值。这个函数返回值的含义和pthread_create一样。

写了上面这段代码,我们题目1来了,main和new线程谁先运行呢?实际上是不确定的!题目2:我们盼望谁最退却出?我们盼望主进程最退却出,因为父进程要回收子进程的退出信息!那怎样来保证main最退却出呢?就是通过pthread_join保证,new线程不退,main就阻塞式等待new线程退出。如果main不join,那主线程运行完退出,进程就退出,全部其他线程也就退出了,所以强烈不推荐这种做法,因为主线程退出了,new线程还没把使命执行完。那如果主线程不退出,也不join,此时就会造成类似于僵尸进程的题目,new线程退出时,其所对应的资源也就会被维护起来,维护起来就是等mian线程去拿new线程的返回值,如果mian一直不拿,就会造成类似僵尸进程。
  1. void* threadRun(void* args)
  2. {
  3.     int cnt = 10;
  4.     while(cnt)
  5.     {
  6.         std::cout << "new thread run ...,cnt : " << cnt-- << std::endl;
  7.         sleep(1);
  8.     }
  9. }
  10. int main()
  11. {
  12.     pthread_t tid;//unsigned long int
  13.     //问题1.main和new线程谁先运行呢?不确定
  14.     int n = pthread_create(&tid,nullptr,threadRun,(void*)"thread-1");
  15.     if(n != 0)
  16.     {
  17.         std::cerr << "create thread error" << std::endl;
  18.         return 1;
  19.     }
  20.     std::cout << "main thread join begin..." << std::endl;
  21.     //2.我们期望谁最后退出?main thread ,如何保证?
  22.     n = pthread_join(tid,nullptr);//join来保证
  23.     if(n == 0)
  24.     {
  25.         std::cout << "main thread wait success" << std::endl;
  26.     }
  27.     return 0;
  28. }
复制代码

题目3:创建了进程就有了tid,tid是什么样子?是什么呢?


我们看到,线程的id是红框里的一大串,那我们之间看到LWP好像并不是这样,

那线程id是什么呢?其实是一个捏造地点,关于这点我们下面再谈。
题目4:全面对待线程函数传参


在这里我们传的是(void*)"thread-1",这个(void*)"thread-1"传给了threadRun的args,运行程序,发现新线程吸取到了这个参数:

然而,这里要说的是,这个参数并不是只能传字符串、整数等,我们也可以传递类对象的地点
  1. class ThreadData
  2. {
  3. public:
  4.     std::string _name;
  5.     int _num;  
  6.     //other
  7. };
  8. void* threadRun(void* args)
  9. {
  10.     ThreadData* td = static_cast<ThreadData*>(args);
  11.     int cnt = 10;
  12.     while(cnt)
  13.     {
  14.         std::cout<< td->_name << " run ...,num is " << td->_num << " cnt : " << cnt-- << std::endl;
  15.         sleep(1);
  16.     }
  17.     return nullptr;
  18. }
  19. std::string PrintToHex(pthread_t& tid)
  20. {
  21.     char buffer[64];
  22.     snprintf(buffer,sizeof(buffer),"0x%lx",tid);
  23.     return buffer;
  24. }
  25. int main()
  26. {
  27.     pthread_t tid;//unsigned long int
  28.     //问题1.main和new线程谁先运行呢?不确定
  29.     ThreadData td;
  30.     td._name = "thread-1";
  31.     td._num = 1;
  32.     int n = pthread_create(&tid,nullptr,threadRun,(void*)&td);
  33.     if(n != 0)
  34.     {
  35.         std::cerr << "create thread error" << std::endl;
  36.         return 1;
  37.     }
  38.     std::string tid_str = PrintToHex(tid);//按照16进制打印出来
  39.     std::cout << "tid : " << tid_str << std::endl;
  40.     std::cout << "main thread join begin..." << std::endl;
  41.     //2.我们期望谁最后退出?main thread ,如何保证?
  42.     n = pthread_join(tid,nullptr);//join来保证
  43.     if(n == 0)
  44.     {
  45.         std::cout << "main thread wait success" << std::endl;
  46.     }
  47.     return 0;
  48. }
复制代码
这样,我们就可以给线程传递多个参数,甚至方法了。但是main函数中的ThreadData td;属于在栈上开发的空间,所以新线程访问的是主线程栈上的变量,这种做法不太推荐,一方面,破坏了主线程的完整性和独立性,另一方面,如果再来一个新线程,这个新线程通过传参还是可以访问到这个变量,一个线程把这个变量改了不就影响另一个线程了吗?所以,我们推荐在堆上申请空间,然后把在堆上申请的空间地点拷贝给线程,堆空间一旦被申请出来,其实其他线程也能看到,但必须得有地点,把这个地点交给一个线程,未来如果有第二个线程,就子啊堆上重新new一块空间交给线程,这样每个线程都有一块堆空间,这样多线程就不会互相干扰了。

我们再来谈这个函数的第二个参数retval,这是一个输出型参数,需要传一个void*的变量地点,在新线程结束后,threadrun函数会返回一个void*的返回值,而这个返回值会被主线程的pthread_join获取,未来要通过void* ret,把&ret传给pthread_join,从而吸取到threadrun的返回值。
  1. class ThreadData
  2. {
  3. public:
  4.     std::string _name;
  5.     int _num;  
  6.     //other
  7. };
  8. void* threadRun(void* args)
  9. {
  10.     ThreadData* td = static_cast<ThreadData*>(args);
  11.     int cnt = 10;
  12.     while(cnt)
  13.     {
  14.         std::cout<< td->_name << " run ...,num is " << td->_num << " cnt : " << cnt-- << std::endl;
  15.         sleep(1);
  16.     }
  17.     delete td;
  18.     return (void*)0;
  19. }
  20. std::string PrintToHex(pthread_t& tid)
  21. {
  22.     char buffer[64];
  23.     snprintf(buffer,sizeof(buffer),"0x%lx",tid);
  24.     return buffer;
  25. }
  26. int main()
  27. {
  28.     pthread_t tid;//unsigned long int
  29.     //问题1.main和new线程谁先运行呢?不确定
  30.     ThreadData* td = new ThreadData();
  31.     td->_name = "thread-1";
  32.     td->_num = 1;
  33.     int n = pthread_create(&tid,nullptr,threadRun,(void*)td);
  34.     if(n != 0)
  35.     {
  36.         std::cerr << "create thread error" << std::endl;
  37.         return 1;
  38.     }
  39.     std::string tid_str = PrintToHex(tid);//按照16进制打印出来
  40.     std::cout << "tid : " << tid_str << std::endl;
  41.     std::cout << "main thread join begin..." << std::endl;
  42.     //2.我们期望谁最后退出?main thread ,如何保证?
  43.     void* code = nullptr;
  44.     n = pthread_join(tid,&code);//join来保证
  45.     if(n == 0)
  46.     {
  47.         std::cout << "main thread wait success,new thread exit code : " << (uint64_t)code << std::endl;
  48.     }
  49.     return 0;
  50. }
复制代码

我们可以看到,主线程吸取到了新线程的退出码。
题目5:怎样全面对待线程函数返回
a.只思量正确的返回,不思量异常,因为异常了,整个进程就崩溃了,包括主进程。
b.我们可以传递任意类型,也可以传递类对象的地点。
所以,这里我们应该能明白,为什么pthread_create的第三个参数函数指针的参数是void*、返回值是void*,这样我们就能传入任意类型、返回任意类型。
题目6:怎样创建多线程呢?
我们直接来看代码:
  1. std::string PrintToHex(pthread_t& tid)
  2. {
  3.     char buffer[64];
  4.     snprintf(buffer,sizeof(buffer),"0x%lx",tid);
  5.     return buffer;
  6. }
  7. const int num = 10;
  8. void* threadrun(void* args)
  9. {
  10.     std::string name = static_cast<const char*>(args);
  11.     while(true)
  12.     {
  13.         std::cout << name << " is running..." << std::endl;
  14.         sleep(1);
  15.         break;
  16.     }
  17.     return args;
  18. }
  19. int main()
  20. {
  21.     std::vector<pthread_t> tids;
  22.     for(int i = 0 ; i < num ; i++)
  23.     {
  24.         //1.有线程的id
  25.         pthread_t tid;
  26.         //2.有线程的名字
  27.         char* name = new char[128];
  28.         snprintf(name,128,"thread-%d",i+1);
  29.         pthread_create(&tid,nullptr,threadrun,/*线程名字*/name);
  30.         //3.保存所有线程的id信息
  31.         tids.emplace_back(tid);
  32.     }
  33.     //join to to
  34.     for(auto tid : tids)
  35.     {
  36.         // std::cout << PrintToHex(tid) << " quit..." << std::endl;
  37.         void* name = nullptr;
  38.         pthread_join(tid,&name);
  39.         std::cout << (const char*)name << " quit..." << std::endl;
  40.         delete (const char*)name;
  41.     }
  42.    return 0;
  43. }
复制代码

题目7:新线程怎样终止?
我们知道,main函数结束,main thread结束,表示进程结束,而新线程结束,只代表自己结束了。
a.函数return。不能使用exit终止一个线程,exit是专门用来终止进程的,不能用来终止线程
b.接口pthread_exit。这个接口等于线程内部的return。

c.main thread调用接口pthread_cancel。线程能被取消,前提是线程得存在。

一般都是主线程取消新线程。新线程被取消的退出结果是-1。-1的定义如下:
  1. #define PTHREAD_CANCELED ((void *) -1)
复制代码
题目8:可不可以不join新线程,让他执行完就退出呢?
可以!

pthread_datach函数,即线程分离,
a.一个线程被创建,默认是joinable的,必须要被join的。
b.如果一个线程被分离,线程的工作状态是分离状态,不需要/不能被join的,仍旧属于进程内部,但是不需要被等待了
如今,新线程要自动和主线程分离,先来在熟悉一个函数pthread_self,类似与getpid,哪个线程调用pthread_self,就返回哪个线程自己的id。

在threadrun函数中,让新线程和main thread分离: 
  1. void* threadrun(void* args)
  2. {
  3.     pthread_detach(pthread_self());//和main thread分离
  4.     std::string name = static_cast<const char*>(args);
  5.     while(true)
  6.     {
  7.         std::cout << name << " is running..." << std::endl;
  8.         sleep(3);
  9.         break;
  10.     }
  11.     // return args;
  12.     pthread_exit(args);//专门用来终止一个线程的
  13.     // exit(1);
  14. }
复制代码
同样的,main thread也可以自动和新线程分离,前提是新线程必须存在:

说完Linux下的多线程,我们如今有一个小插曲,其实C++11也有多线程,也有其原生线程库,我们使用C++11创建线程,
  1. #include<iostream>
  2. #include<string>
  3. #include<vector>
  4. #include<thread>
  5. #include<unistd.h>
  6. #include<stdlib.h>
  7. #include<pthread.h>
  8. void threadrun(int num)
  9. {
  10.     while(num)
  11.     {
  12.         std::cout << "thread-1: " << " num : " << num << std::endl;
  13.         num--;
  14.         sleep(1);
  15.     }
  16. }
  17. int main()
  18. {
  19.     std::string name = "thread-1";
  20.     std::thread mythread(threadrun,10);
  21.     while(true)
  22.     {
  23.         std::cout << "main thread..." << std::endl;
  24.         sleep(1);
  25.     }
  26.     mythread.join();
  27.     return 0;
  28. }
复制代码
如果我们在Makefile只用C++11,

这样编译时其实会报错:

所以,C++11中创建多线程编译时,也要加-lpthread,

然后,编译运行以上程序,我们看到结果:

所以,C++11中的多线程本质,就是对原生线程库接口的封装!!!
实际上,无论Linux、Windows还是MacOS,每一款操纵体系都有自己对应的创建进程方案,但为什么说语言具有跨平台性呢?因为无论在什么平台下,C++11代码都是一样的,在Linux、Windows、MacOS中都提供了C++11的库,对上都提供了一样的线程接口,所以语言上是同一种创建线程的方式,但是每一种平台实现库的方式肯定是不一样的。所以未来任何语言,只需要把原生线程库的接口学懂了,上层语言只需要熟悉接口就可以了。文件操纵,也是如此!

到如今,我们接着谈题目3中提到的tid到底是什么?我们写了如下代码,编译运行,
  1. std::string ToHex(pthread_t tid)
  2. {
  3.     char buffer[128];
  4.     snprintf(buffer,sizeof(buffer),"0x%lx",tid);
  5.     return buffer;
  6. }
  7. void* threadrun(void* args)
  8. {
  9.     std::string name = static_cast<const char*>(args);
  10.     while(true)
  11.     {
  12.         std::cout << name << " is running...,tid: " << ToHex(pthread_self()) << std::endl;
  13.         sleep(1);
  14.     }
  15. }
  16. int main()
  17. {
  18.     pthread_t tid;
  19.     pthread_create(&tid,nullptr,threadrun,(void*)"thread-1");
  20.     std::cout << "new thread running,tid: " << ToHex(tid) << std::endl;
  21.     pthread_join(tid,nullptr);
  22.     return 0;
  23. }
复制代码

我们发现用户级所看到的tid值和其LWP肯定不相等,所以这里我们得出一个结论:给用户提供的线程ID,不是内核中的lwp,而是由pthread库自己维护的一个唯一值,其实也好明白,LWP是轻量级进程的ID,不需要呈现给用户,库内部也要负担对线程的管理。
实际上,tid是一个地点,要明白tid,我们起首要明白pthread库,和动态库类似,pthread库就是存在于磁盘上的一个文件,

mythread也是磁盘上的一个文件(可执行程序),mythread运行时,要起首被加载到内存上,在内存上就要有自己的代码和数据,然后也要配套有pcb、地点空间和页表,CPU调理这个程序时就会去执行内部代码。接下来创建线程,要提前把库加载到内存,映射到我进程的地点空间!!!具体来说,就是映射到地点空间的堆栈之间的共享区,未来想访问pthread库中任意一个函数的地点,都能通过页表找到对应的方法。
而我们大概正在运行多个和mythread一样的程序,此时只需要把它地点空间的共享区通过页表映射到已经加载到内存中的pthread库,此时多个进程就能使用同一个库里的方法来举行线程创建了,所以pthread库叫做共享库,这样每一个进程都可以只用pthread共享库来创建多线程了。具体示意图如下:

实际上,Linux维护的是轻量级进程的属性,可是在用户层用的是线程,我要的是线程的ID、状态等属性,可是与线程相关的属性在Linux内核中是没有的,所以线程相关的属性就要由库举行维护,pthread库对线程也具有分配ID的功能,要负担对线程的管理,所以pthread库怎样做到对线程管理呢?先形貌,再构造!我们在使用pthread_create的时候,什么叫做创建线程呢?创建线程又做了什么呢?只有内核中的LWP是不够的,LWP中不包括任何包罗线程的概念。所以创建线程时,pthread库会为我们创建一个上图中的布局(其实就是一个布局体),也就是申请了一个内存块,此中第一项struct pthread中存放了线程在用户级最基本的属性,第三项线程栈就是用户级线程所拥有的独立的栈布局。每创建一个线程就给我们申请这样的内存块,全部的内存块连续存放。所谓先形貌,这个布局体包罗了库中创建形貌线程的相关布局体字段属性;所谓再构造,可以先明白为把内存块用数组的形式管理起来。换言之,未来如果我们想找一个线程的全部属性,我们只需要找到线程控制块的地点就可以了,所以pthread_t id就是一个地点,就是线程控制块的地点!怎么明白呢?我们之间学fopen的时候,其返回值FILE*中的FILE是一个布局体,里面包罗了文件的相关属性,那这个FILE对象在那里呢?在C标准库里!所以我们访问文件对象不就是在拿着地点访问文件对象吗?

所以,我们应该能明白,在pthread_join的时候,是在拿着线程ID找到对应的线程控制块,然后从里面取出线程返回值给*retval,再开释对应的线程控制块,所以这就是为什么我们能拿到线程退出结果的原因。
因此,Linux线程=pthread库中线程的属性集+LWP,是1:1的。那lwp线程在运行怎么怎么保证把自己产生的临时变量存放在自己的线程栈上呢?既然有lwp概念,那么必然后lwp的体系调用,比如clone:

lwp可以调用clone第二个参数就可以指定栈空间,而pthread库内部就是类似对这种体系调用的封装。
那内存控制块中的线程局部存储是干什么用的呢?我们来看以下程序:
  1. int gval = 100;
  2. std::string ToHex(pthread_t tid)
  3. {
  4.     char buffer[128];
  5.     snprintf(buffer, sizeof(buffer), "0x%lx", tid);
  6.     return buffer;
  7. }
  8. void *threadrun(void *args)
  9. {
  10.     std::string name = static_cast<const char *>(args);
  11.     while (true)
  12.     {
  13.         std::cout << name << " is running...,tid: " << ToHex(pthread_self()) << " ,gval:" << gval << " ,&gval:" << &gval << std::endl;
  14.         gval++;
  15.         sleep(1);
  16.     }
  17. }
  18. int main()
  19. {
  20.     pthread_t tid;
  21.     pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");
  22.     while (true)
  23.     {
  24.         std::cout << "main thread running ,gval:" << gval << " ,&gval:" << &gval << std::endl;
  25.         sleep(1);
  26.     }
  27.     pthread_join(tid, nullptr);
  28.     return 0;
  29. }
复制代码

我们发现,只要新线程改变了全局变量gval的值,主线程也能立即看到。那么,如果gval比较特殊,不能共享,只能让它们各自单独拥有一份gval,所以在Linux的g++中,存在__thread,用__pthrea去修饰gval变量,然后再次运行程序,

原因就在于,在编译时,一旦一个内置变量被__thread修饰,这样就在每个线程的线程控制块中各自存一个gval,这就叫线程局部存储。留意,__thread只在Linux下有用,而且只能修饰内置类型。






免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

反转基因福娃

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表