Linux从系统到网络|Linux-多线程


多线程

  • 线程概念
  • 线程控制
    • POSIX线程库
    • 创建线程
    • 线程ID和地址空间
    • 线程等待
    • 线程终止
    • 线程分离
  • 线程优缺点
  • 线程异常和线程用途

线程概念
  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
之前的进程的创建经常画的图:
Linux从系统到网络|Linux-多线程
文章图片

当创建1个进程时,系统会为它创建进程控制块(task_struct)、虚拟地址空间(mm_struct)、页表等的一些数据结构,当把磁盘中的数据和代码加载进内存中后,虚拟地址和物理地址通过页表建立映射关系。
此时我们再创建一批"进程",并不创建地址空间,只创建task_struct,共用第1个进程的地址空间,分别执行代码的某个部分,创建的效果如下:
Linux从系统到网络|Linux-多线程
文章图片

此时创建的就是3个线程,每个task_struct就是1条执行流,因为是共享进程的地址空间所以线程在进程的地址空间内运行。
重新理解进程
Linux从系统到网络|Linux-多线程
文章图片

此时从内核角度看进程:进程时承担分配资源的基本实体。所以当创建1个进程时,系统会创建task_struct、虚拟地址空间、页表,和物理内存构建映射关系,打开进程默认打开的文件、信号等等。
我们之前学的进程都是只有1个task_struct,也就是只有1条执行流。
CPU如何看待task_struct
CPU不管有多少条执行流,我只看task_struct,你task_struct有1条执行流就是单执行流的task_struct,有多执行流,你就是多执行流的task_struct。如下图:
Linux从系统到网络|Linux-多线程
文章图片

Linux从系统到网络|Linux-多线程
文章图片

所以,CPU看到的PCB要比传统的进程更加轻量化。
在Linux中,没有真正意义的多线程,因为没有为多线程设计数据结构,而是用进程模拟的。
在操作系统中有很多进程,1个进程中再有几个线程,如果再为线程创建同样的数据结构,那么代价就太大了。反正多线程也是执行任务的,Linux就直接复用了进程控制块,所以Linux中的所有执行流都叫做轻量级进程。1个task_struct后可能挂着好几个线程,而且共享地址空间,所以tastk_struct后可能是地址空间的一部分。
线程控制 POSIX线程库
  • pthread库是由第3方提供的
  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
错误检查:
  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
创建线程
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);

参数说明:
  • thread:返回线程ID
  • attr:设置线程的属性,attr为NULL表示使用默认属性(一般给NULL即可)
  • start_routine:是个函数地址,线程启动后要执行的函数
  • arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
下面用pthread_create来创建线程:
1 #include.h> 2 #include 3 #include 4 void* mythread(void* arg) 5 { 6while(1) 7{ 8printf("i am mythread %s\n",(char*)arg); 9sleep(1); 10} 11 } 12 int main() 13 { 14pthread_t tid; 15 16pthread_create(&tid,NULL,mythread,(void *)"mythread 1"); 17while(1) 18{ 19printf("i am mian thrad\n"); 20sleep(2); 21} 22return 0; 23 }

主线程每隔2秒打印1句,新线程每隔1秒打印1句。看效果:
Linux从系统到网络|Linux-多线程
文章图片

此时使用ps axj的命令查看进程信息:
Linux从系统到网络|Linux-多线程
文章图片

我们只看到了1个进程,进程中有2个线程,这2个线程属于同1个进程。
ps -aL显示轻量级进程:
Linux从系统到网络|Linux-多线程
文章图片

它们的pid是一样的,LWP表示轻量级进程,LWP是不一样的。CPU根据LWP进行调度,系统根据PID来判断是不是同1个进程。
线程ID和地址空间
  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程的ID和内核中的LWP是不一样的
  • 内核中的LWP是进程调度,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
获取线程id
获取线程id有2种方法:
1.创建线程是通过输出型参数
2.使用pthread_self()函数获得
pthread_self函数原型:
pthread_t pthread_self(void);

例:新线程通过调用pthread_self函数获取自己的线程ID,主线程也通过调用pthread_self函数获取自己的线程ID,并通过输出型参数获取新线程的ID。
1 #include.h> 2 #include 3 #include 4 void* mythread(void* arg) 5 { 6while(1) 7{ 8printf("i am :%s,pid:%d,id:%p\n",(char*)arg,getpid(),pthread_self()); 9sleep(1); 10} 11 } 12 int main() 13 { 14pthread_t tid; 15 16pthread_create(&tid,NULL,mythread,(void *)"mythread 1"); 17while(1) 18{ 19printf("i am mian thrad pid:%d,my id: %p,newthread id:%p\n",getpid(),pthread_self(),tid); 20sleep(2); 21} 22return 0; 23 }

如下图:
Linux从系统到网络|Linux-多线程
文章图片

主线程获得新线程ID和新线程通过pthread_self获得的ID是一样的。
pthread_t是什么类型呢?
Linux中没有意义上的线程,是用进程模拟的,只提供LWP,内核只需要管理LWP,用户使用的线程要由线程库自己来管理。如何管理?先描述,在组织,所以就在线程库中管理。
ldd查看线程依赖的库:
Linux从系统到网络|Linux-多线程
文章图片

看到依赖的是动态库。前面学动态库的时候,我们知道动态库是当进程运行时被加载到共享区,此时该进程内的所有线程都可以看到这个动态库。
Linux从系统到网络|Linux-多线程
文章图片

每个线程都有自己的私有栈,主线程用的地址空间的原生栈,其余的线程用的栈就在共享区中开辟的。每个线程都有自己的struct_pthread,里面包含了线程的各种属性,每个线程都有线程局部存储,当中有线程切换时的上下文数据。
Linux从系统到网络|Linux-多线程
文章图片

所以,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。也就是每个线程在库里的起始地址。通过这些地址可以找到每个线程。
线程等待
为什么需要线程等待?
  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。
函数原型:
int pthread_join(pthread_t thread, void **value_ptr);

参数说明:
  • thread:线程ID
  • value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0,失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
    PTHREAD_ CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
我让新线程运行5秒后返回,为了便于查看将将退出码设置成特殊值,主线程在死循环打印。
3 #include 4 void* mythread(void* arg) 5 { 6int n = 0; 7char* msg=(char*)arg; 8while(n <= 5) 9{ 10printf("i am :%s,pid:%d\n",msg,getpid()); 11++n; 12sleep(1); 13} 14return((void*)11); 15 } 16 int main() 17 { 18pthread_t tid; 19 20pthread_create(&tid,NULL,mythread,(void *)"mythread 1"); 21void* ret = NULL; 22pthread_join(tid,&ret); 23while(1){ 24printf("i am mian thrad pid:%d,exit code: %d\n",getpid(),(long int)ret); 25sleep(1); 26} 27return 0; 28 }

Linux从系统到网络|Linux-多线程
文章图片

明明主线程在死循环打印,为什么等新线程退出后才打印?
从这里可以得出主线程是在阻塞式等待的。
之前进程时有3种状态:
1.代码跑完,结果正确
2.代码跑完,结果错误
3.代码异常终止
线程也是跟进程一样,而关于线程的等待,只默认退出码是正确的,出错整个线程就挂了,出现错误就是进程的问题。我们不知道是具体哪个线程出现问题,只知道整个进程退出了。
例:我让新线程除0,整个进程都挂了
Linux从系统到网络|Linux-多线程
文章图片

这也说明了线程的健壮性不强。
线程终止 如果需要只终止某个线程而不终止整个进程,可以有三种方法:
  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
return退出
先让新线程运行5秒后退出:
1 #include 2 #include3 #include 4 void* mythread(void* arg) 5 { 6printf("i am :%s,pid:%d,id:%p\n",(char*)arg,getpid(),pthread_self()); 7sleep(5); 8return (void*)1; 9 } 10 int main() 11 { 12pthread_t tid; 13 14pthread_create(&tid,NULL,mythread,(void *)"mythread 1"); 15while(1) 16{ 17printf("i am mian thrad pid:%d,my id: %p,newthread id:%p\n",getpid(),pthread_self(),tid); 18sleep(1); 19} 20return 0; 21 }

Linux从系统到网络|Linux-多线程
文章图片

看到新线程退出后,主线程还在跑。那我们让主线程退出,看看新线程会不会继续跑:
1 #include.h> 2 #include 3 #include 4 void* mythread(void* arg) 5 { 6while(1) 7{ 8printf("i am :%s,pid:%d,id:%p\n",(char*)arg,getpid(),pthread_self()); 9sleep(1); 10} 11//return (void*)1; 12 } 13 int main() 14 { 15pthread_t tid; 16 17pthread_create(&tid,NULL,mythread,(void *)"mythread 1"); 18printf("i am mian thrad pid:%d,my id: %p,newthread id:%p\n",getpid(),pthread_self(),tid); 19sleep(5); 20return 0; 21 }

Linux从系统到网络|Linux-多线程
文章图片

看到主线程退出后新线程也就退出了。
pthread_exit函数
函数原型:
void pthread_exit(void *value_ptr);

参数说明:
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
例:让新线程运行5秒后退出
1 #include.h> 2 #include 3 #include 4 void* mythread(void* arg) 5 { 6int n = 0; 7char* msg=(char*)arg; 8while(n <= 5) 9{ 10printf("i am :%s,pid:%d\n",msg,getpid()); 11++n; 12sleep(1); 13} 14pthread_exit ((void*)11); 15 } 16 int main() 17 { 18pthread_t tid; 19 20pthread_create(&tid,NULL,mythread,(void *)"mythread 1"); 21void* ret = NULL; 22pthread_join(tid,&ret); 23while(1){ 24printf("i am mian thrad pid:%d,exit code: %d\n",getpid(),(long int)ret); 25sleep(1); 26}

Linux从系统到网络|Linux-多线程
文章图片

新线程退出码为11.
pthread_cancel函数
函数原型:
int pthread_cancel(pthread_t thread);

参数说明:
thread:线程ID
返回值:成功返回0;失败返回错误码
例:让主线程取消新线程
1 #include.h> 2 #include 3 #include 4 void* mythread(void* arg) 5 { 6int n = 0; 7char* msg=(char*)arg; 8while(n <= 5) 9{ 10printf("i am :%s,pid:%d\n",msg,getpid()); 11++n; 12sleep(1); 13} 14pthread_exit ((void*)11); 15 } 16 int main() 17 { 18pthread_t tid; 19 20pthread_create(&tid,NULL,mythread,(void *)"mythread 1"); 21pthread_cancel(tid); 22void* ret = NULL; 23pthread_join(tid,&ret); 24while(1){ 25printf("i am mian thrad pid:%d,exit code: %d\n",getpid(),(long int)ret); 26sleep(1); 27} 28return 0; 29 }

Linux从系统到网络|Linux-多线程
文章图片

退出码变成了-1不是我们设置的11了。
线程分离 1.默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
2.如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
3.可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
函数原型:
int pthread_detach(pthread_t thread);

参数说明:
thread分离线程的ID
例:我们让新线程分离
3 #include 4 void* mythread(void* arg) 5 { 6int n = 0; 7char* msg=(char*)arg; 8while(n <= 5) 9{ 10printf("i am :%s,pid:%d\n",msg,getpid()); 11++n; 12sleep(1); 13} 14pthread_detach(pthread_self()); 15pthread_exit ((void*)11); 16 } 17 int main() 18 { 19pthread_t tid; 20 21pthread_create(&tid,NULL,mythread,(void *)"mythread 1"); 22while(1){ 23printf("i am mian thrad pid:%d\n",getpid()); 24sleep(1); 25} 26return 0; 27 }

Linux从系统到网络|Linux-多线程
文章图片

新线程退出后系统自动回收线程对应的资源,不需要主线程join。
线程优缺点
优点:
  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
缺点
1.性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型
线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变。
2.健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3.缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4.编程难度提高:
编写与调试一个多线程程序比单线程程序困难得多
线程异常和线程用途
线程异常:
1.单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
2.线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途:
【Linux从系统到网络|Linux-多线程】1.合理的使用多线程,能提高CPU密集型程序的执行效率
2.合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)但不是越多越好。

    推荐阅读