目录

一、线程的概念和理解

线程的优点

线程异常:

二、线程TID的内容

1、主线程退出,整个线程退出。

2、线程退出需要等待        


本节内容,我们将详细讲解Linux线程的有关知识,并为同学们铺垫多线程的有关知识。

一、线程的概念和理解

理解线程之前,我们需要重新对进程进行理解

我们前面说一个task_struct有着一个进程地址空间,然后有页表,搭建其和物理内存的映射。

我们现在需要重新对进程进行定位理解:

(1)对于CPU来说,它在运行一个进程的时候,其看到的PCB可以是多个,然后共享一份地址空间。(说是PCB其实不够准确,应该叫TCB,但是和PCB非常相似,这里为了引出线程,暂时可以当PCB这样理解)

注意,这里你没有看错,就是多个PCB共享同一块地址空间。允许这样的情况存在。

linux java多核cpu多线程分配 linux的多线程_c++

如上图。

(2)我要提出问题了,那如果这样的话,进程到底是啥呢?

我们之前说进程是一系列的代码和数据以及组织它的数据结构,并且这主要的数据结构就是PCB。为什么会有多个PCB?

那现在呢?这个说法难道要被推翻了吗?

实际上,我们不难发现,我们在进程创建的时候,会在同时创建一大批的系统资源:比如PCB、进程地址空间、页表等。而当存在这样一种情况:多个PCB共享同一块进程地址空间的时候,就会有如上图中三个进程控制块同时指向code区域中的三份不同的代码的情形。我们是允许这样的情况出现的。

希望上述的例子你能够接受。可以不用理解,接受即可。

(3)那我们现在,对线程再做一下定义,线程到底算是什么呢?

我们说,进程是承担分配系统资源的基本实体。

那么线程,是系统调度的基本单位。(线程是在进程的地址空间内运行的)

我们之前说进程是系统调度的基本单位,实际上是不够准确的。

应该是线程。线程是进程的执行流!!!

上面的每一个PCB,都可以认为是一个线程(说法不准确,仅为了在此引出线程、方便理解,阶段性认知错误是必要的哈哈哈)。

所以,一个大的进程内部,是可以有多个线程的。

我们的每一个执行流并不是一个个进程。而是一个个线程。

而我们之前所学的,都是一个进程有一个线程的。即单线程的简单的执行流。

ps:还需要注意的是,在Linux中,是没有真正意义上的线程(注意,这里指的是Linux没有为其专门开辟新的数据结构)。而线程是用进程来模拟的。

为什么呢?线程和进程实际上是拥有许多相同属性的。如果我们再为线程专门去开辟对应的结构,那么调度关系就会变得相对复杂,而复杂的关系一般不会太高效。(不过Windows还是有专门的线程库的)

我们以后该怎么面对PCB呢?

我们依旧讲PCB叫做进程控制块。

不过,如果在一个进程中,如果只有一个PCB,那么该进程就是拥有单线程的进程。

如果有多个PCB,那么其就是拥有多线程的进程。

而所有的PCB/线程,都可以叫做轻量级进程。

结合上面的,我们来总结一下何为线程:

在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”,即更准确地来说,其是在一个进程地址空间内运行的执行流。

所以说,一切进程至少都有一个执行线程。而线程在进程内部运行,本质是在进程地址空间内运行。

在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
 

线程的优点

1、创建一个新线程的代价要比创建一个新进程小得多


2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多


3、线程占用的资源要比进程少很多


4、能充分利用多处理器的可并行数量


5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务


6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现


7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

注意:上述进程的优点的后四点也是进程的优点。

虽然Linux操作系统没有为我们提供多线程的库,但是呢,作为用户,我们又要用到多线程,所以,我们就用基于LInux系统开发的第三方库——pthread库  

1)创建线程接口:pthread_creat 

注意,编译链接的时候要加上 -pthread(如下图)

linux java多核cpu多线程分配 linux的多线程_c++_02

a、这里的第一个参数为线程id(就是我们后面要说的lwp),这个参数OS实际上并不知道,其是基于用户层面上开发的。但是呢,在运行到内核的时候,我们的OS的PCB会与这里的lwp存在着某种对应关系。

就是说,线程的管理和组织不再由OS来管控而是由用户层的库来管控。我们“先描述、再组织”的方式是放在库里面的了。(稍后我们再来看库里面有什么东西)

b、第二个参数表示线程属性,我们暂且不关心。

c、第三个参数表示线程执行流,说白了,就是去调用这个函数。通过这个函数来去完成新线程的执行流。

d、第四个参数表示第三个函数的参数。

其返回值成功就为0,若出错,就返回相应的错误码。

我们来看下面的一个例子:

linux java多核cpu多线程分配 linux的多线程_c++_03

原码:

1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 #include<pthread.h>  
  4   
  5   
  6 void* thread_run(void* arg)  
  7 {  
  8   while(1)  
  9   {  
 10     printf("I am %s\n",(char*)arg);  
 11     sleep(1);  
 12   }  
 13 }  
 14   
 15 int main()  
 16 {  
 17   pthread_t rid;  
 18   pthread_create(&rid, NULL , thread_run , (void*)"thread 1");  
 19   
 20   while(1)  
 21   {  
 22     printf("I am main thread\n");
 23     sleep(2);                                                                                      
 24   }
 25    
 26   return 0;
 27 }

linux java多核cpu多线程分配 linux的多线程_linux_04

运行截图:

linux java多核cpu多线程分配 linux的多线程_c++_05

可以很容易地发现,我们这里是有两个执行流的。

 

我们可以通过ldd,来查看我们的这个可执行文件链接的动态库。

linux java多核cpu多线程分配 linux的多线程_多线程_06

如上,我们可以看到,其链接的是一个pthread的动态库。 (与我们刚刚在编译的时候的-phread相对应)

那怎么证明我们的两个执行流(线程)是一个进程里的呢?

我们先来认识一个函数:pthread_self

linux java多核cpu多线程分配 linux的多线程_linux_07

其作用很简单,其是获得当前线程的id

我们将上面的代码改一下:

linux java多核cpu多线程分配 linux的多线程_多线程_08

 

linux java多核cpu多线程分配 linux的多线程_多线程_09

linux java多核cpu多线程分配 linux的多线程_linux_10

 那么现在,我们对于线程的理解,可以这样来说明了:

linux java多核cpu多线程分配 linux的多线程_linux_11

我们前面所说的结构体、控制块都是由OS创建并完成。

但是线程由于Linux操作系统没有为其有专门的线程库,我们又要用

所以就在用户层面, 由用户链接第三方库。这个第三方库叫做pthread库

在这个第三方库中,其会有模拟线程描述的结构体,叫tcb,它和PCB功能类似,它用来描述线程和组织线程,也叫做线程实体。

每一个这样的tcb中里面会有一个用来标识线程唯一性的lwp,它将来会以某种关系和PCB连接起来。

而每一个线程实体和Linux内核中的每一条执行流又是一一对应的关系。

所以说,OS为我们提供的是线程的执行功能,而线程的管理和组织是由用户空间来提供的。

来换一张图更加细致地说明:

linux java多核cpu多线程分配 linux的多线程_多线程_12

 左边是我们的进程地址空间,右边是我们链接的动态库

在外面的共享区,会有着pthread维护的结构体,遵循先描述、再组织,每一个结构体当中又会有着tid,线程栈这样的东西。

tid可以认为是该线程控制库在地址空间里的起始地址。

注意,在进程地址空间的栈是主线程栈!其他线程的栈是要在自己的线程栈中的,要不然数据就乱了。

我们解释完毕,再来说说一些其他的基本概念:

线程异常:

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

理由:线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

因为信号处理的基本单位是进程!所以线程的健壮性不强。

但是,合理的使用多线程,能提高CPU密集型程序的执行效率

合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

二、线程TID的内容

我们前面说到,进程是资源分配的基本单位,线程是调度的基本单位。

虽然说线程共享进程数据,但也拥有自己的一部分数据:(即下面的数据是线程自身私有的)

  • 线程ID
  • 一组寄存器(上下文)(重要)
  • 栈(重要)
  • errno
  • 信号屏蔽字
  • 调度优先级

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,

如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。

除此之外,各线程还共享以下进程资源和环境:(即下面的资源是各个线程共享的)

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

我们接着我们之前的代码,来说说线程退出和等待

1、主线程退出,整个线程退出。

因为进程是承担资源分配的基本实体。当主线程退出的时候,整个进程全部退出。整个进程都不存在了,自然所有的线程就都不会再存在了。

linux java多核cpu多线程分配 linux的多线程_执行流_13

2、线程退出需要等待        

pthread_join()

linux java多核cpu多线程分配 linux的多线程_多线程_14

 第一个参数是需要等到的线程ID,第二个参数是一个一级指针的接收列表。

什么意思呢?

我们还是按照刚刚的例子来说:(稍微改动一下)

linux java多核cpu多线程分配 linux的多线程_多线程_15

 我们通过pthread_join,来将线程tid的返回值拿了回来,然后再将其打印出来。

我们将其返回值打印了出来。 

我们来运行看一下:(其确实拿到了返回值10)

linux java多核cpu多线程分配 linux的多线程_后端_16

如果不去进行等待,就可能会造成类似于僵尸进程的情况,即我们新创建出来的PCB没有被立即回收。

那么结束线程的方式,我们可以再来总结补充一下都有哪些。

1、函数执行完了,线程结束;


2、通过pthread_exit()函数;该函数的用法和进程当中的exit是相同的。


3、pthread_cancel()函数,从外部将该线程杀掉。即直接在别的线程中调用pthread_cancel(tid)即可。有种人在家中坐,锅从天上来的感觉。

 

与进程不同的是,对于线程,我如果不想要等待,让这个线程自己玩自己的,我们可以让其分离。

用pthread_datch()函数。

来解释一下:


1、默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。


2、如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。(注意,这个时候,我们也是不会再拿到退出码的了)


3、分离的时候,就直接pthread_detach(pthread_self());就可以了。

 

但是需要注意的是,二者还是在一个进程当中,如果有一个出问题了,还是整个进程都会崩掉的。

我们在本节,主要为大家讲解的是线程有关的概念和一些基本操作,我们将在下一节为大家详细地讲解线程之间的同步、互斥以及并发的用法及注意事项等。