Linux多线程编程【基础 | 实战】解析

前言

线程是轻量级的进程(LWP: Light Weight Process),在Linux环境下线程的本质仍是进程,进程是资源分配的最小单位,线程是操作系统调度执行的最小单位

解析进程与线程关系

标准层面:线程是一个执行分支,执行粒度比进程更细,调度成本更低。线程是进程内部的一个执行流。

内核层面:线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体。

拓展:

  • 内核角度来理解进程:承担分配系统资源的基本实体,叫做进程;
  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”;
  • 一切进程至少都有一个执行线程;
  • 线程在进程内部运行,本质是在进程地址空间内运行;
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化;
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

需要明确的是,一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建,虚拟地址和物理地址就是通过页表建立映射的,如下图:

Linux多线程编程【基础 | 实战】解析

每个进程都有自己独立的进程地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性,但如果我们在创建“进程”时,只创建task_struct,并要求创建出来的task_struct和父task_struct共享进程地址空间和页表,那么创建的结果就是下面这样的:

Linux多线程编程【基础 | 实战】解析

此时创建的实际上就是四个线程:

  • 其中每一个线程都是当前进程里面的一个执行流,也就是我们常说的“线程是进程内部的一个执行分支”。

  • 线程在进程内部运行,本质就是线程在进程地址空间内运行,也就是说曾经这个进程申请的所有资源,几乎都是被所有线程共享的。

解析

1、进程内的多个线程共享部分

(1) 因为多个线程在同一个地址空间,因此所谓的代码段(Text Segment)、数据段(Data Segment)都是共享的:

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

(2) 除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表。(进程打开一个文件后,其他线程也能够看到)
  • 每种信号的处理方式。(SIG_IGN、SIG_QUIT或者自定义的信号处理函数)
  • 当前工作目录。(cwd)
  • 用户ID和组ID。

2、多个线程共享进程数据,但也拥有自己的私有数据

  • 线程ID。
  • 一组寄存器。(存储每个线程的上下文信息)
  • 栈。(每个线程都有临时的数据,需要压栈出栈,各线程间的栈不共享
  • errno。(C语言提供的全局变量,每个线程都有自己的)
  • 信号屏蔽字。
  • 调度优先级。

让我们通过一张图概括线程与进程的包含问题:

Linux多线程编程【基础 | 实战】解析

因此,所谓的进程并不是通过task_struct来衡量的,除了task_struct之外,一个进程还必须要有进程地址空间、文件、信号等等,合起来才能称之为一个进程。

线程是进程的一个执行分支,是在进程内部运行的一个执行流,是操作系统进行运算调度的最小单位。线程的执行流分为两种:单执行流与多执行流,如下图所示:

Linux多线程编程【基础 | 实战】解析

Linux多线程编程【基础 | 实战】解析

这里我们讲了很多进程与线程之间的关系,接下来让我们一起来了解一些基本概念。

一、基本概念

  1. 线程就是程序的执行路线,它是进程内部的控制序列,是资源的调度单位的基本单位,或者说它是进程的一部分。

  2. 线程是轻量级的,没有自己独立的内存资源、代码段、数据区、堆区、环境变量、命令行参数、文件描述符、信号处理函数、当前目录等资源。

  3. 线程拥有自己独立的栈内存,也就是有自己独立的局部变量,还有独立的线各ID、错误码、信号掩码。

  4. 一个进程可以同时拥有多个线程,也就是拥有多个执行路线,其中一个叫主线程。

  5. 线程是进程的一部分,而且是负责执行的那部分。

二、POSIX线程

POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。

  1. UNIX和Linux系统早期是没有线程概念的,线程首先使用在windows系统中,后面发现线程有它独特的优势,然后各个厂商各自提供私有的线程库,而且接口、实现的差异比较大,不易移植。

  2. 在1995年制定的POSIX标准中,规定了统一的线程编程接口,遵循POSIX标准的线程实现被统称为POSIX线程,也叫pthread。

  3. pthread包含一个头文件 pthread.h 和一个共享库 libpthread.so。因此在编译线程代码时需要-lpthread参数。

三、多线程API函数

1、线程创建

当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,这个线程就叫做主线程。

  • 主线程是产生其他子线程的线程。
  • 通常主线程必须最后完成某些执行操作,比如各种关闭动作。

注意:每个线程都有唯一的线程ID,ID类型为pthread_t,可理解为:typedef unsigned long int pthread_t;本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现。多线程库封装现成的函数pthread_t pthread_self(void);供我们调用,返回当前线程的线程ID,其作用对应进程中 getpid() 函数。

函数pthread_create用于创建一个线程,详情如下:

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数说明:

  • pthread_t tid:线程id的类型为pthread_t,通常为无符号整型,当调用pthread_create成功时,通过tid指针返回。

  • const pthread_attr_t *attr:指定创建线程的属性,如线程优先级、初始栈大小、是否为守护进程等。可以使用NULL来使用默认值,通常情况下我们都是使用默认值。

  • void *(*start_routine) (void *):函数指针start_routine,指定当新的线程创建之后,将执行的函数。

  • void *arg:线程将执行的函数的参数。如果想传递多个参数,请将它们封装在一个结构体中。

返回值说明:

  • 线程创建成功返回0,失败返回错误码。

引用头文件:#include <pthread.h>

编译方法:gcc -o <文件名> <文件名.c> -lpthread

问题为什么连接线程库要指明库名?标准库不用指明库名?

  • 因为标准库是语言自带的,第三方库不是语言自带的,可能是系统或者是用户自己安装的,线程库是Linux系统安装的,不是语言提供的,对于gcc编译器来说是第三方库。gcc默认连接库是标准库(语言提供的)。编译器命令行参数中没有第三方库的名字。所以给编译器指明库名。
  • 强调:找到库所在路径和使用该路径下的库文件,是两码事。找到路径找不到库,还需要指明库名。标准库中因为编译器命令行中有该库名。

1)无参数线程函数创建线程

创建一个线程,线程执行的函数的参数为NULL,源代码如下:

#include <pthread.h>#include <stdio.h>
void* thfunc(void *arg){ printf("in thfuncn"); return (void*)0;}
int main(int argc, char* argv[]){ pthread_t pid; int ret;
ret = pthread_create(&pid,NULL,thfunc,NULL); if(ret) { printf("pthread_create fail:%dn",ret); return -1; } sleep(1); printf("in main:thread is created.n"); return 0;}

编译源文件,生成可执行文件,运行如下:

[root@localhost home]# gcc -o phtread_test phtread_test.c -pthread[root@localhost home]# [root@localhost home]# lsmaster  phtread_test  phtread_test.c[root@localhost home]# ./phtread_test in thfuncin main:thread is created.

2)带参数线程函数创建线程

创建一个线程,并传递结构体作为参数,源代码如下:

#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>
typedef struct student{ int score; char *name;}Student;
void *thfunc(void *arg){ //线程函数 Student *p = (Student*)arg; printf("in thfunc:name=%s,core=%dn",p->name,p->score);//输出结构体内容 return (void *)0;}
int main(){ pthread_t tid; int ret; Student stu; stu.score = 90; stu.name = "Qin"; ret = pthread_create(&tid, NULL, thfunc, (void *)&stu); //创建线程 if(ret){ printf("pthread_creat failed:%dn", ret); return -1; }    //用于等待某个线程退出,成功返回0,否则返回Exxx(为正数) pthread_join(tid, NULL); printf("in main:thread is createdn");
return 0;}

编译源文件,生成可执行文件,运行如下:

[root@localhost 0629]# gcc -o  pthread_test_arg pthread_test_arg.c -lpthread[root@localhost 0629]# lspthread_test_arg  pthread_test_arg.c[root@localhost 0629]# ./pthread_test_arg in thfunc:name=Qin,core=90in main:thread is created

3)创建一个线程,共享进程数据

#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>
int global=100;
void *thfunc(void *arg){ //线程函数 global++; printf("in thfunc:global=%d,n",global); return (void*)0;}
int main(){ pthread_t tid; int ret;
ret = pthread_create(&tid, NULL, thfunc, NULL); //创建线程 if(ret){ printf("pthread_creat failed:%dn", ret); return -1; } pthread_join(tid, NULL); //等待子线程结束 global++; printf("in main:global is:%dn",global);
return 0;}

编译源文件,生成可执行文件,运行如下:

[root@localhost share]# gcc -o pthread_test_share pthread_test_share.c -lpthread[root@localhost share]# ./pthread_test_share in thfunc:global=101,in main:global is:102

2、线程等待

Linux平台的pthread有两种状态:joinable状态和unjoinable状态:

  • joinable状态的线程,当线程函数自己返回退出时或调用pthread_exit时都不会释放线程所占用堆栈和线程描述符。只有当你调用了pthread_join之后这些资源才会被释放,因此,需要main函数或者其他线程去调用pthread_join函数。

  • unjoinable状态的线程,这些资源在线程函数退出时或调用pthread_exit时会自动被释放。设置unjoinable状态设置有两种办法:一是在调用pthread_create时指定;二是子线程创建后在子线程中调用pthread_detach(pthread_self())分离线程自身,状态改为unjoinable状态,确保资源自动的释放。

一个线程被创建出来默认的状态是joinable,如果一个线程结束运行但没有被join,则它的状态类似于进程中的僵尸进程,即还有一部分资源没有被回收(退出状态码)。所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源(类似于wait,waitpid) 。如果创建线程者不对新线程进行等待,那么这个新线程的资源也是不会被回收的,就会产生类似于“僵尸进程”的问题,也就是内存泄漏。

函数pthread_join 用来等待子线程结束,函数会让主线程挂起(即休眠,让出CPU),直到子线程退出,pthread_join能让子线程所占资源释放。如果子线程已经结束,那么该函数会立即返回。

int pthread_join(pthread_t tid,void **retval);

参数说明:

  • tid:被等待线程的ID。
  • retval:retval通常设为NULL,如果不为NULL,返回被等待线程的返回值。

返回值说明:

  • 线程等待成功返回0,失败返回错误码。

引用头文件:#include <pthread.h>

注意: pthread_join函数默认是以阻塞的方式进行线程等待的,并且指定的线程必须是joinable的。。

如下两个Demo演示了线程创建出来后,没有被join和被join的场景,即没有调用pthread_join等待子线程运行结束和调用pthread_join等待子线程运行结束,让我们对比看一下会发生什么。

Demo1: 使用默认状态,注释掉pthread_join函数

pthread_join_default源文件如下:

#include <stdlib.h>#include <unistd.h>#include <stdio.h>#include <pthread.h>
void *thread_function(void *arg){ int i; for (i = 0; i < 8; i++) { printf("%s:Thread working...! %d n", __FUNCTION__, i); sleep(1); } return NULL;}
int main(void){ pthread_t mythread;
if (pthread_create(&mythread, NULL, thread_function, NULL)) { printf("error creating thread."); abort(); } /* if ( pthread_join ( mythread, NULL ) ) { printf("error join thread."); abort(); } */ printf("%s:Thread done! n", __FUNCTION__); exit(0);}

使用gcc编译源文件,注意编译多线程的程序,要在gcc命令尾部加上-lpthread,链接pthread库。运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_join_default pthread_join_default.c -lpthread[root@localhost 77]# ./pthread_join_default main:Thread done!

根据结果可知,子线程的内容未被打印出来,因为主线程main 跑的比子线程快,子线程中 thread_function 的打印是不会打印出来的,其所占用的资源也未被回收,也就不能被复用, 这就造成了资源的泄漏。

Demo2: 使用pthread_join 阻塞主线程

pthread_join()使得子线程合入主线程,主线程阻塞等待子线程结束,然后回收子线程资源。

将上述Demo 1 中的注释放开,修改后的pthread_join_default源文件如下:

#include <stdlib.h>#include <unistd.h>#include <stdio.h>#include <pthread.h>
void *thread_function(void *arg){ int i; for (i = 0; i < 8; i++) { printf("%s:Thread working...! %d n", __FUNCTION__, i); sleep(1); } return NULL;}
int main(void){ pthread_t mythread;
if (pthread_create(&mythread, NULL, thread_function, NULL)) { printf("error creating thread."); abort(); }
if ( pthread_join ( mythread, NULL ) ) { printf("error join thread."); abort(); }
printf("%s:Thread done! n", __FUNCTION__); exit(0);}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_join_default pthread_join_default.c -lpthread[root@localhost 77]# ./pthread_join_default thread_function:Thread working...! 0 thread_function:Thread working...! 1 thread_function:Thread working...! 2 thread_function:Thread working...! 3 thread_function:Thread working...! 4 thread_function:Thread working...! 5 thread_function:Thread working...! 6 thread_function:Thread working...! 7 main:Thread done!

可以看到输出结果中子线程 thread_function 的打印。因此,使用pthread_join函数不仅可以使子线程的资源被回收,也可以使子线程的逻辑流被执行到,从而发挥想要实现的作用。

通过pthread_join得到的终止状态是不同的。线程退出和进程退出一样,有三种状态:

  1. 代码正常运行,结果正确,正常退出
  2. 代码正常运行,结果不正确,不正常退出
  3. 代码出现异常,异常退出

提示:前两种情况以退出码来表述退出情况,后面一种以退出信号来表示

问题:但是线程等待函数的第2个参数返回的是被等待线程执行函数的返回值,也就是退出码,没有表示线程异常退出的情况,这是为什么?

:因为某个线程如果运行异常终止,整个进程都会终止。进程异常终止,就属于进程的等待处理的范畴了,不属于线程范畴。比如:一个线程函数有除0操作,硬件MMU发现异常,操作系统收到异常,向该进程发出信号,终止进程。信号处理的单位是进程。

总的来说就是,被等待线程只关心正常运行的退出情况,获取退出码。不关心异常退出情况,异常退出情况上升至进程处理范畴。

问题:这里我们怎么获取退出码呢?

:调用pthread_join函数的线程默认以阻塞方式等待线程id为tid参数的线程终止,线程以不同的方式终止,得到的终止状态不同:

  • 如果线程通过return终止,pthread_join函数的第二个参数retval直接指向return后面的返回值;
  • 如果线程通过pthread_exit终止,pthread_join函数的第二个参数retval直接指向pthread_exit参数‘;
  • 如果线程通过被其它线程调用pthread_cancel终止,pthread_join函数的第二个参数retval直接存放的是一个常数宏PTHREAD_CANCELED,值是-1。#define PTHREAD_CANCELED (void *)-1
  • 如果不关心返回值,可以将ret_val设为NULL。

接下来让我们一起用实例分别说明正常退出和异常退出状态,其中正常退出分为return、pthread_exit、pthread_cancel三种方式,异常退出则会直接终止进程。pthread_exit、pthread_cancel的详细介绍见下文。

1)正常退出

(1)return

pthread_test.c源文件如下:

#include <stdio.h>#include <unistd.h>#include <pthread.h>
void *thread1(void *arg){ int num=0; while(1){ sleep(1); num++; printf("thread1 pid:%d, tid:%lun",getpid(),*(pthread_t*)arg); if(num==5) break; } //退出该线程 return (void*)1;}
int main(){ pthread_t tid; //创建线程 pthread_create(&tid,NULL,thread1,(void*)&tid); printf("main pid:%d, tid:%lun",getpid(),tid);
//保存退出码 void* ret=NULL; //等待线程退出 pthread_join(tid,&ret); printf("thread1 is quit,quit code is %dn",(unsigned long)ret);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_test pthread_test.c -lpthread[root@localhost 77]# ./pthread_test main pid:5934, tid:139685158942464thread1 pid:5934, tid:139685158942464thread1 pid:5934, tid:139685158942464thread1 pid:5934, tid:139685158942464thread1 pid:5934, tid:139685158942464thread1 pid:5934, tid:139685158942464thread1 is quit,quit code is 1

根据程序运行结果可知,被等待线程返回退出码1且其第二个参数ret收到返回值1。证明pthread_join函数的第二个参数ret直接指向return后面的返回值。

注意:在线程中使用return代表当前线程退出,但是在main函数中使用return代表整个进程退出,也就是说只要主线程退出了那么整个进程就退出了,此时该进程曾经申请的资源就会被释放,而其他线程会因为没有了资源,自然而然的也退出了。

问题1:为什么printf中直接使用ret?

答:因为返回值是void*, ret也是void*, 用ret接受的返回值。

问题2:为什么强转为unsigned long?

答:因为void*是指针,指针在32位CPU下是32位寻址空间,即4字节;在64位CPU是64位寻址空间,即8字节。如果强转成int类型可能会截断,从而导致数据错误。而unsigned long一般跟CPU的可寻址范围的字节长度是相等的,所以Linux内核中有很多代码都是用unsigned long来替代指针,用unsigned long表示的地址一般不允许引用该地址所在的内存,如果要强制引用或者访问这片内存,则需要在unsigned long前面加上强制指针转换 (char *)unsigned long。

看到这里,可能有的朋友比较疑惑,指针大小自己记得好像是和操作系统位数有关系的,怎么这里却说是和CPU位数有关系呢,这里不得不拓展一点小知识。

拓展:指针的大小究竟和什么有关系?

解析:指针的大小和下面这3种均有关系,当下述3种位数不同时,取最小的位数。

  1. cpu位数(32位数4字节,64位数8字节)
  2. 操作系统位数(32位数4字节,64位数8字节)
  3. 编译器的位数(32位数4字节,64位数8字节)

比如,如果CPU、系统都是64位的,但编译器是32位的,那么很显然指针只能是32位,即4字节大小。

(2)pthread_exit

在原pthread_test.c源文件基础上,修改如下:

#include <stdio.h>#include <unistd.h>#include <pthread.h>
void *thread1(void *arg){ int num=0; while(1){ sleep(1); num++; printf("thread1 pid:%d, tid:%lun",getpid(),*(pthread_t*)arg); if(num==5) break; } //退出该线程 //return (void*)1; pthread_exit((void*)1);}
int main(){ pthread_t tid; //创建线程 pthread_create(&tid,NULL,thread1,(void*)&tid); printf("main pid:%d, tid:%lun",getpid(),tid);
//保存退出码 void* ret=NULL; //等待线程退出 pthread_join(tid,&ret); printf("thread1 is quit,quit code is %dn",(unsigned long)ret);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_test pthread_test.c -lpthread[root@localhost 77]# ./pthread_test main pid:7392, tid:140455491106560thread1 pid:7392, tid:140455491106560thread1 pid:7392, tid:140455491106560thread1 pid:7392, tid:140455491106560thread1 pid:7392, tid:140455491106560thread1 pid:7392, tid:140455491106560thread1 is quit,quit code is 1

根据程序运行结果可知,被等待线程返回退出码同上。证明pthread_join函数的第二个参数ret直接指向pthread_exit后面的返回值。

注意: exit函数的作用是终止进程,任何一个线程调用exit函数也代表的是整个进程终止。

(3)pthread_cancel

在(2)的pthread_test.c源文件基础上,修改如下:

#include <stdio.h>#include <unistd.h>#include <pthread.h>
void *thread1(void *arg){ int num=0; while(1){ sleep(1); num++; printf("thread1 pid:%d, tid:%lun",getpid(),*(pthread_t*)arg); if(num==5) break; } //退出该线程 //return (void*)1; //pthread_exit((void*)1);}
int main(){ pthread_t tid; //创建线程 pthread_create(&tid,NULL,thread1,(void*)&tid); printf("main pid:%d, tid:%lun",getpid(),tid); sleep(5);
//退出线程ID为tid的线程 pthread_cancel(tid);
//保存退出码 void* ret=NULL; //等待线程退出 pthread_join(tid,&ret); printf("thread1 is quit,quit code is %dn",(unsigned long)ret);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_test pthread_test.c -lpthread[root@localhost 77]# ./pthread_test main pid:8221, tid:139941163689728thread1 pid:8221, tid:139941163689728thread1 pid:8221, tid:139941163689728thread1 pid:8221, tid:139941163689728thread1 pid:8221, tid:139941163689728thread1 is quit,quit code is -1

根据程序运行结果可知,被等待线程返回值是-1,证明pthread_join函数的第二个参数ret直接存放的是常数宏PTHREAD_CANCELED的值。

2)异常退出

修改pthread_test.c源文件,如下:

#include <stdio.h>#include <unistd.h>#include <pthread.h>
void *thread1(void *arg){ int num=0; int b; b/=0; while(1){ sleep(1); num++; printf("thread1 pid:%d, tid:%lun",getpid(),*(pthread_t*)arg); if(num==5) break; } //退出该线程 return (void*)1; //pthread_exit((void*)1);}
int main(){ pthread_t tid; //创建线程 pthread_create(&tid,NULL,thread1,(void*)&tid); printf("main pid:%d, tid:%lun",getpid(),tid); sleep(5);
//保存退出码 void* ret=NULL; //等待线程退出 pthread_join(tid,&ret); printf("thread1 is quit,quit code is %dn",(unsigned long)ret);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_test pthread_test.c -lpthreadpthread_test.c: 在函数‘thread1’中:pthread_test.c:9:5: 警告:被零除 [-Wdiv-by-zero]    b/=0;     ^[root@localhost 77]# lspthread_test  pthread_test.c[root@localhost 77]# ./pthread_test main pid:8594, tid:140524144858880浮点数异常(吐核)

根据程序运行结果可知,并没有收集到退出码,进程意外终止了。

3、线程退出

调用线程退出函数,当前线程会马上退出,不会影响其他线程的正常运行,不管是在子线程和主线程都可以使用。pthread_exit()函数一般用于线程主动终止自身的情景下。

void pthread_exit(void* retval);

参数说明:

  • retval: 线程退出时携带的数据(函数返回值),当前子线程会得到该数据,相当于return (void*) retval,若不需要,指定为NULL。

引用头文件:#include <pthread.h>

线程退出函数,重点需要关注的就是回收子线程数据,常用的做法是在子线程退出的时候使用pthread_exit()的参数将数据传出,在回收这个子线程的时候通过pthread_join()的第二个参数来接受子线程传递出的数据。

注意:使用return和pthread_exit返回的指针所指向的内存单元必须是全局或者是malloc分配的,不能是在线程函数栈上分配的,因为线程退出时,函数栈帧被释放了。

如下让我们通过示例来说明回收子线程数据时,错误的使用方法和正确的使用方法。

1)返回指向线程函数栈上分配的变量的指针

pthread_join_test.c源文件:

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <pthread.h>
// 定义结构体struct Persion{ int id; char name[36]; int age;};
// 子线程的处理代码void* working(void* arg){ printf("我是子线程, 线程ID: %ldn", pthread_self()); int i; for(i=0; i<9; ++i) { printf("child == i: = %dn", i); if(i == 6) { struct Persion p; p.age = 12; strcpy(p.name, "Alex"); p.id = 100; // 该函数的参数将这个地址传递给了主线程的pthread_join() pthread_exit(&p); } } return NULL; // 代码执行不到这个位置就退出了}
int main(){ // 1. 创建一个子线程 pthread_t tid; pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ldn", tid); // 2. 子线程不会执行下边的代码, 主线程执行 printf("我是主线程, 线程ID: %ldn", pthread_self()); int i; for(i=0; i<3; ++i) { printf("i = %dn", i); }
// 阻塞等待子线程退出 void* ptr = NULL; // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存 // 这个内存地址就是pthread_exit() 参数指向的内存 pthread_join(tid, &ptr); // 打印信息 struct Persion* pp = (struct Persion*)ptr; printf("子线程返回数据: name: %s, age: %d, id: %dn", pp->name, pp->age, pp->id); printf("子线程资源被成功回收...n");
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 78]# gcc -o pthread_join_test pthread_join_test.c -lpthread[root@localhost 78]# [root@localhost 78]# ./pthread_join_test 子线程创建成功, 线程ID: 139867083597568我是主线程, 线程ID: 139867091879744i = 0i = 1i = 2我是子线程, 线程ID: 139867083597568child == i: = 0child == i: = 1child == i: = 2child == i: = 3child == i: = 4child == i: = 5child == i: = 6子线程返回数据: name: , age: 1481975384, id: 4子线程资源被成功回收...

根据上述结果可知,回收子线程数据错误,因为子线程退出时,函数栈帧被释放了,因此在函数中定义的局部结构体变量中的值就变成了随机值,所以在回收子线程数据时,绝对不能使用局部变量,必须使用全局变量或者malloc分配的变量。

2)返回指向全局变量的指针

基于pthread_join_test.c修改后的源文件如下:

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <pthread.h>
// 定义结构体struct Persion{ int id; char name[36]; int age;};//定义全局结构体struct Persion p;
// 子线程的处理代码void* working(void* arg){ printf("我是子线程, 线程ID: %ldn", pthread_self()); int i; for(i=0; i<9; ++i) { printf("child == i: = %dn", i); if(i == 6) { p.age = 12; strcpy(p.name, "Alex"); p.id = 100; // 该函数的参数将这个地址传递给了主线程的pthread_join() pthread_exit(&p); } } return NULL; // 代码执行不到这个位置就退出了}
int main(){ // 1. 创建一个子线程 pthread_t tid; pthread_create(&tid, NULL, working, NULL);
printf("子线程创建成功, 线程ID: %ldn", tid); // 2. 子线程不会执行下边的代码, 主线程执行 printf("我是主线程, 线程ID: %ldn", pthread_self()); int i=0; for(; i<3; ++i) { printf("i = %dn", i); }
// 阻塞等待子线程退出 void* ptr = NULL; // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存 // 这个内存地址就是pthread_exit() 参数指向的内存 pthread_join(tid, &ptr); // 打印信息 struct Persion* pp = (struct Persion*)ptr; printf("子线程返回数据: name: %s, age: %d, id: %dn", pp->name, pp->age, pp->id); printf("子线程资源被成功回收...n");
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 78]# gcc -o pthread_join_test pthread_join_test.c -lpthread[root@localhost 78]# ./pthread_join_test 子线程创建成功, 线程ID: 140373544281856我是主线程, 线程ID: 140373552564032i = 0i = 1i = 2我是子线程, 线程ID: 140373544281856child == i: = 0child == i: = 1child == i: = 2child == i: = 3child == i: = 4child == i: = 5child == i: = 6子线程返回数据: name: Alex, age: 12, id: 100子线程资源被成功回收...

根据上述结果可知,回收子线程数据正确,因为子线程函数中使用了指向全局变量的指针传递返回值,因此在线程退出时,该块内存地址未被释放,因此主线程中可以成功访问该返回值。

4、线程取消

线程是可以被取消的,我们可以使用pthread_cancel函数取消某一个线程,pthread_cancel函数的函数原型如下:

int pthread_cancel(pthread_t tid);

参数说明:

  • tid:被取消线程的ID。

返回值说明:

  • 线程取消成功返回0,失败返回错误码。

引用头文件:#include <pthread.h>

线程是可以取消自己的,取消成功的线程的退出码一般是-1。虽然线程可以自己取消自己,但一般不这样做,我们往往是用于一个线程取消另一个线程,比如主线程取消新线程。

这里我们通过一个demo来演示,pthread_cancel_test.c源文件如下:

#include<stdio.h>#include <stdlib.h>#include<pthread.h>#include<unistd.h>
void* Routine(void* arg){ char* msg = (char*)arg; int count = 0; while (count < 5){ printf("I am %s...pid: %d, ppid: %d, tid: %lun", msg, getpid(), getppid(), pthread_self()); sleep(1); count++; } pthread_exit((void*)6666);}int main(){ pthread_t tid[5]; int i; for (i = 0; i < 5; i++){ char* buffer = (char*)malloc(64); sprintf(buffer, "thread %d", i); pthread_create(&tid[i], NULL, Routine, buffer); printf("%s tid is %lun", buffer, tid[i]); } pthread_cancel(tid[0]); pthread_cancel(tid[1]); pthread_cancel(tid[2]); pthread_cancel(tid[3]); printf("I am main thread...pid: %d, ppid: %d, tid: %lun", getpid(), getppid(), pthread_self()); for (i = 0; i < 5; i++){ void* ret = NULL; pthread_join(tid[i], &ret); printf("thread %d[%lu]...quit, exitcode: %dn", i, tid[i], ret); } return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 78]# gcc -o pthread_cancel_test pthread_cancel_test.c -lpthread[root@localhost 78]# ./pthread_cancel_test thread 0 tid is 139921746663168thread 1 tid is 139921738270464thread 2 tid is 139921729877760thread 3 tid is 139921721485056thread 4 tid is 139921713092352I am main thread...pid: 17667, ppid: 3835, tid: 139921754945344I am thread 0...pid: 17667, ppid: 3835, tid: 139921746663168thread 0[139921746663168]...quit, exitcode: -1I am thread 4...pid: 17667, ppid: 3835, tid: 139921713092352I am thread 3...pid: 17667, ppid: 3835, tid: 139921721485056I am thread 2...pid: 17667, ppid: 3835, tid: 139921729877760I am thread 1...pid: 17667, ppid: 3835, tid: 139921738270464thread 1[139921738270464]...quit, exitcode: -1thread 2[139921729877760]...quit, exitcode: -1thread 3[139921721485056]...quit, exitcode: -1I am thread 4...pid: 17667, ppid: 3835, tid: 139921713092352I am thread 4...pid: 17667, ppid: 3835, tid: 139921713092352I am thread 4...pid: 17667, ppid: 3835, tid: 139921713092352I am thread 4...pid: 17667, ppid: 3835, tid: 139921713092352thread 4[139921713092352]...quit, exitcode: 6666

此时可以发现,0、1、2、3号线程退出时的退出码不是我们设置的6666,而是-1,只有未被取消的4号线程的退出码是6666,因为只有4号进程未被取消。 此外,新线程也是可以取消主线程的

5、线程分离

我们知道Linux下线程默认的状态是joinable,如果一个线程结束运行但没有被join,则它的状态类似于进程中的Zombie Process,即还有一部分资源没有被回收(退出状态码)。因此,创建线程者会调用pthread_join来等待线程运行结束,并得到线程的退出代码,回收其资源。但是调用pthread_join(pthread_id)后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。

比如,在Web服务器中当主线程为每个新来的链接创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的链接),这时可以在子线程中加入代码pthread_detach(pthread_self())或者在父线程中调用pthread_detach(thread_id)(非阻塞,可立即返回),这会将该子线程的状态设置为detached,则该子线程运行结束后会自动释放所有资源。

int pthread_detach(pthread_t tid);

参数说明:

  • tid:被分离线程的ID。

返回值说明:

  • 成功返回0,失败返回错误码。

引用头文件:#include <pthread.h>

注意:可以是线程组内其它线程对目标线程分离,也可以是线程分离自己

如果不关心返回值,我们可以告诉系统,将线程分离,当线程退出后,将自动释放线程资源,如下演示创建新线程的线程,不关心新线程的返回值,使用线程分离。

pthread_test.c源文件如下:

#include <stdio.h>#include <unistd.h>#include <pthread.h>
void *thread1(void *arg){ //分离该线程 pthread_detach(pthread_self()); sleep(1); printf("thread1 pid:%d, tid:%lun",getpid(),*(pthread_t*)arg);
//退出该线程 return (void*)1; //pthread_exit((void*)1);}
int main(){ pthread_t tid; //创建线程 pthread_create(&tid,NULL,thread1,(void*)&tid); printf("main pid:%d, tid:%lun",getpid(),tid); sleep(5);
//保存退出码 void* ret=NULL; //等待线程退出 pthread_join(tid,&ret); printf("thread1 is quit,quit code is %dn",(unsigned long)ret);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_test pthread_test.c -lpthread[root@localhost 77]# ./pthread_test main pid:25180, tid:139943567070976thread1 pid:25180, tid:139943567070976thread1 is quit,quit code is 0

新线程返回值是1,但是实际返回的是0,说明没有收到返回值,因为ret本来就是NULL。

虽然线程分离了,但是当分离的线程因为异常终止,依然会导致进程终止,接下来让我们通过示例来说明。

pthread_test.c源文件修改如下:

#include <stdio.h>#include <unistd.h>#include <pthread.h>
void *thread1(void *arg){ //分离该线程 pthread_detach(pthread_self()); sleep(1); printf("thread1 pid:%d, tid:%lun",getpid(),*(pthread_t*)arg); int b; b/=0;
//退出该线程 return (void*)1; //pthread_exit((void*)1);}
int main(){ pthread_t tid; //创建线程 pthread_create(&tid,NULL,thread1,(void*)&tid); printf("main pid:%d, tid:%lun",getpid(),tid); sleep(5);
//保存退出码 void* ret=NULL; //等待线程退出 pthread_join(tid,&ret); printf("thread1 is quit,quit code is %dn",(unsigned long)ret);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_test pthread_test.c -lpthreadpthread_test.c: 在函数‘thread1’中:pthread_test.c:12:6: 警告:被零除 [-Wdiv-by-zero]     b/=0;      ^[root@localhost 77]# ./pthread_test main pid:27154, tid:139621331527424thread1 pid:27154, tid:139621331527424浮点数异常(吐核)

根据上述结果可知,因为线程异常从而导致进程被终止。

线程设置为分离状态时,线程主动与主控线程断开关系。线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)。网络、多线程服务器常用。

进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。(注意进程没有这一机制) 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止(或者进程终止被回收了)。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。将上述pthread_test.c源文件稍作修改如下:

#include <stdio.h>#include <string.h>#include <unistd.h>#include <pthread.h>
void *thread1(void *arg){ //分离该线程 pthread_detach(pthread_self()); sleep(1); printf("thread1 pid:%d, tid:%lun",getpid(),*(pthread_t*)arg);
//退出该线程 //return (void*)1; pthread_exit((void*)1);}
int main(){ pthread_t tid; int err; //创建线程 pthread_create(&tid,NULL,thread1,(void*)&tid); printf("main pid:%d, tid:%lun",getpid(),tid); sleep(5);
//保存退出码 void* ret=NULL; //等待线程退出 err = pthread_join(tid, &ret); printf("-------------err= %dn", err); if (err != 0) fprintf(stderr, "thread_join error: %sn", strerror(err)); else fprintf(stderr, "thread exit code %dn", (unsigned long)ret);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# ./pthread_test main pid:28886, tid:140684657252096thread1 pid:28886, tid:140684657252096-------------err= 22thread_join error: Invalid argument

上述结果表明不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误(22号错误)。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

6、线程信号

别被名字吓到,pthread_kill可不是kill,而是向线程发送一个signal。还记得signal吗,大部分signal的默认动作是终止进程的运行,所以,我们才要用signal()去抓信号并加上处理函数。

int pthread_kill(thread_t tid, int sig);

参数说明:

  • tid: 要发送信号的线程。

  • sig: 要发送的信号,0是保留信号,用来判断线程是否还存在。

返回值说明:

  • 成功: 返回0。

  • 线程不存在:返回ESRCH。

  • 信号不合法:返回EINVAL。

引用头文件:#include <pthread.h>

向指定tid的线程发送sig信号,如果线程代码内不做处理,则按照信号默认的行为影响整个进程,也就是说,如果你给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出。

由于pthread_t类型的线程ID只在线程组内是唯一的,其他进程完全可能存在线程ID相同的线程,所以pthread_kill只能向同一个进程的线程发送信号。

接下来通过示例演示pthread_kill的用法,pthread_kill_test.c源文件如下:

#include <stdio.h>#include <pthread.h>#include <signal.h>#include <unistd.h>#include <errno.h>
void *thrfunc(void *arg){ int count = 50; while(1){ printf("thread work:threadID=%d, count=%dn",pthread_self(),count); sleep(1); count--; }
return (void*)0;}
int main(){ pthread_t tid; int ret;
ret = pthread_create(&tid, NULL, thrfunc, NULL); sleep(5); printf("main thread:threadID=%dn",pthread_self());
int kill_rc = pthread_kill(tid,0); if(kill_rc == ESRCH) printf("the specified thread did not exists or already quitn"); else if(kill_rc == EINVAL) printf("signal is invalidn"); else printf("the specified thread is aliven"); pthread_join(tid, NULL);
return 0;}read is aliven");
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_kill_test pthread_kill_test.c -lpthread[root@localhost 77]# ./pthread_kill_test thread work:threadID=1648453376, count=50thread work:threadID=1648453376, count=49thread work:threadID=1648453376, count=48thread work:threadID=1648453376, count=47thread work:threadID=1648453376, count=46main thread:threadID=1656735552the specified thread is alivethread work:threadID=1648453376, count=45thread work:threadID=1648453376, count=44thread work:threadID=1648453376, count=43^C

根据上述运行结果可知,通过函数pthread_kill实现了子线程是否存活的检测,即当发送sig信号为0且子线程存活时,函数确实返回0。让我们看一下当子线程运行结束时,该函数会返回什么?修改上面pthread_kill_test.c源文件如下:

#include <stdio.h>#include <pthread.h>#include <signal.h>#include <unistd.h>#include <errno.h>
void *thrfunc(void *arg){ int count = 5; while(count>0){ printf("thread work:threadID=%d, count=%dn",pthread_self(),count); sleep(1); count--; }
return (void*)0;}
int main(){ pthread_t tid; int ret;
ret = pthread_create(&tid, NULL, thrfunc, NULL); sleep(10); printf("main thread:threadID=%dn",pthread_self());
int kill_rc = pthread_kill(tid,0); if(kill_rc == ESRCH) printf("the specified thread did not exists or already quitn"); else if(kill_rc == EINVAL) printf("signal is invalidn"); else printf("the specified thread is aliven"); pthread_join(tid, NULL);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# vi pthread_kill_test.c[root@localhost 77]# gcc -o pthread_kill_test pthread_kill_test.c -lpthread[root@localhost 77]# ./pthread_kill_test thread work:threadID=700397312, count=5thread work:threadID=700397312, count=4thread work:threadID=700397312, count=3thread work:threadID=700397312, count=2thread work:threadID=700397312, count=1main thread:threadID=708679488the specified thread did not exists or already quit

根据运行结果可知,当子线程运行结束,函数确实返回了ESRCH。由此可见,根据pthread_kill函数的返回值,我们就可以判断线程是否存活。

接下来让我们再通过实例演示一下pthread_kill发送其他信号给线程,源码如下:

#include <stdio.h>#include <unistd.h>#include <signal.h>#include <errno.h>#include <pthread.h>#include <stdlib.h>     // sleep() 函数
//signal处理函数static void signal_handler(int sig_num){ if (sig_num == SIGUSR1) { printf("thread quit by myown Custom Signal!n"); (void)pthread_exit((void *)1); }
return;}
//线程执行的函数void * thread_Fun(void * arg) { if (signal(SIGUSR1, signal_handler) == SIG_ERR) { printf("Fail to register signaln"); }
printf("子线程开始执行n"); printf("子线程ID:%dn",pthread_self());
while(1);}
int main(){ pthread_t tid; int value; int res; //创建tid 线程 res = pthread_create(&tid, NULL, thread_Fun, NULL); if (res != 0) { printf("线程创建失败n"); return 0; } sleep(2);
//检测tid线程是否存在 int kill_rc = pthread_kill(tid, 0); if (kill_rc == 0) { printf("Child thread exists and then kill it.n"); pthread_kill(tid, SIGUSR1); }
printf("sleep.n"); pthread_join(tid, NULL); sleep(3); printf("main exit.n"); return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_kill_quit_test pthread_kill_quit_test.c  -lpthread[root@localhost 77]# ./pthread_kill_quit_test 子线程开始执行子线程ID:607971072Child thread exists and then kill it.sleep.thread quit by myown Custom Signal!main exit.

这里通过pthread_kill发送用户自定义信号SIGUSR1,如果不设置signal函数,默认会使进程终止,这里通过设置signal处理函数,自定义对线程的处理,即直接退出子线程。

7、线程 ID 比较

在 Linux 中线程 ID 本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的 ID,但是线程库是可以跨平台使用的,在某些平台上 pthread_t 可能不是一个单纯的整形,这种情况下比较两个线程的 ID 必须要使用比较函数,该函数主要用于检查两个线程是否相等,函数原型如下:

int pthread_equal(pthread_t t1, pthread_t t2);

参数说明:

  • t1:要比较的线程的线程 ID

  • t2:要比较的线程的线程 ID

返回值说明:

  • 如果两个线程 ID 相等返回非 0 值,如果不相等返回 0

引用头文件:#include <pthread.h>

接下来让我们通过实例演示一下pthread_equal比较两个线程的ID是否相等,源码如下:

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <pthread.h>   void* func_one(void* ptr) {     int cnt=3;    while(cnt--)    {       printf("thread 1ID:%dn",pthread_self());    }    return (void*)1;}   void* func_two(void* ptr) {     printf("thread 2 ID:%dn", pthread_self());    sleep(5);     return 0;}   int main() {     pthread_t thread_one, thread_two;     // 创建线程一    pthread_create(&thread_one, NULL, func_one, NULL);     // 创建线程二    pthread_create(&thread_two, NULL, func_two, NULL);         // 等待线程一运行结束     pthread_join(thread_one, NULL);     // 等待线程二运行结束    pthread_join(thread_two, NULL);      if(pthread_equal(thread_one,thread_two))    {        printf("thread 1 and thread 2 equaln");    }    else    {        printf("thread 1 and thread 2 not equaln");    }
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# gcc -o pthread_equal_test pthread_equal_test.c -lpthread[root@localhost 77]# ./pthread_equal_test thread 1ID:1515988736thread 1ID:1515988736thread 1ID:1515988736thread 2 ID:1507596032thread 1 and thread 2 not equal

根据上述输出结果可知,这两个线程不相等。

四、显示进程中的线程信息

在Linux中,程序中创建的线程(也称为轻量级进程,LWP)会具有和程序的PID相同的“线程组ID”,该线程即为主线程。然后,各个线程会获得其自身的线程ID(TID)。对于Linux内核调度器而言,线程不过是恰好共享特定资源的标准的进程而已。经典的命令行工具,如ps或top,都可以用来显示线程级别的信息,只是默认情况下它们显示进程级别的信息。这里提供了在Linux上显示某个进程的线程的几种方式。

1、ps命令

在ps命令中,“-T”或”-L”选项可以开启线程查看。下面的命令列出了由进程号为的进程创建的所有线程。

方法一:

ps -T -p <pid>

选项说明:

  • -T :显示线程,带有SPID列。

SPID:system process id,Linux下面表示线程。

方法二:

ps -L -p <pid>

选项说明:

  • -L :显示线程,可能有LWP和NLWP列。

LWP:线程ID

NLWP:线程组内线程的个数。

自定义进程pthread_multithread_test,其中起2个线程,源码如下:

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <pthread.h>   void* func_one(void* ptr) {     while(1)    {}    return (void*)1;}   void* func_two(void* ptr) {     while(1);    return 0;}   int main() {     pthread_t thread_one, thread_two;       // 创建线程一    pthread_create(&thread_one, NULL, func_one, NULL);       // 创建线程二    pthread_create(&thread_two, NULL, func_two, NULL); 
// 等待线程一运行结束 pthread_join(thread_one, NULL); // 等待线程二运行结束 pthread_join(thread_two, NULL);
return 0;}

使用gcc编译源文件,运行可执行程序,如下:

[root@localhost 77]# ./pthread_multithread_test 
[master@localhost 77]$ ps -ef | grep pthread_multithread_testroot 47359 3835 99 03:12 pts/0 00:00:38 ./pthread_multithread_testmaster 47422 47370 0 03:12 pts/1 00:00:00 grep --color=auto pthread_multithread_test
[master@localhost 77]$ ps -T -p 47359 PID SPID TTY TIME CMD 47359 47359 pts/0 00:00:00 pthread_multith 47359 47360 pts/0 00:00:45 pthread_multith 47359 47361 pts/0 00:00:46 pthread_multith

“SPID”栏表示线程ID,其中SPID为47359的线程为主线程,而“CMD”栏则显示了线程名称。

我们再重新运行一下pthread_multithread_test进程,使用方法二查看如下:

[master@localhost 77]$ ps -ef | grep pthread_multithread_testroot      47777   3835 99 03:18 pts/0    00:00:22 ./pthread_multithread_testmaster    47789  47370  0 03:19 pts/1    00:00:00 grep --color=auto pthread_multithread_test[master@localhost 77]$ ps -L -p 47777   PID    LWP TTY          TIME CMD 47777  47777 pts/0    00:00:00 pthread_multith 47777  47778 pts/0    00:00:37 pthread_multith 47777  47779 pts/0    00:00:37 pthread_multith

“LWP”栏表示线程ID,LWP即Light Weight Process(轻量级进程),其中LWP为47777的线程为主线程,同样“CMD”栏则显示了线程名称。

2、Top命令

top命令可以实时显示各个线程情况。要在top输出中开启线程查看,请调用top命令的“-H”选项,该选项会列出所有Linux线程。在top运行时,你也可以通过按“H”键将线程查看模式切换为开或关。

要让top输出某个特定进程并检查该进程内运行的线程状况,命令如下:

top -H -p <pid>

Linux多线程编程【基础 | 实战】解析

top -Hp $pid  可以查看进程pid下所有的线程列表,Shift+H (进程和线程切换显示)

Linux多线程编程【基础 | 实战】解析

Shift+H切换至只显示进程

Linux多线程编程【基础 | 实战】解析

Shift+H切换至显示所有线程

3、Htop

一个对用户更加友好的方式是,通过htop查看单个进程的线程,它是一个基于ncurses的交互进程查看器。该程序允许你在树状视图中监控单个独立线程。

要在htop中启用线程查看,请开启htop,然后按来进入htop的设置菜单。选择“设置”栏下面的“显示选项”,然后开启“树状视图”和“显示自定义线程名”选项。按退出设置。

Linux多线程编程【基础 | 实战】解析

现在,你就会看到下面这样单个进程的线程视图。

Linux多线程编程【基础 | 实战】解析

可以看到pthread_multithread_test进程以及其创建的2个线程。

4、/proc/pid/task/tid/status

Linux下面procfs文件系统下面的/proc/pid/status文件内记录进程的详细信息,在使用时,其中pid替换成相应进程的进程号。

[root@localhost 77]# cat /proc/51174/statusName:	pthread_multithState:	S (sleeping)Tgid:	51174Ngid:	0Pid:	51174PPid:	3835TracerPid:	0Uid:	0	0	0	0Gid:	0	0	0	0FDSize:	256Groups:	0 VmPeak:	   22840 kBVmSize:	   22840 kBVmLck:	       0 kBVmPin:	       0 kBVmHWM:	     376 kBVmRSS:	     376 kBRssAnon:	      80 kBRssFile:	     296 kBRssShmem:	       0 kBVmData:	   16588 kBVmStk:	     136 kBVmExe:	       4 kBVmLib:	    1972 kBVmPTE:	      36 kBVmSwap:	       0 kBThreads:	3SigQ:	0/7179SigPnd:	0000000000000000ShdPnd:	0000000000000000SigBlk:	0000000000000000SigIgn:	0000000000000000SigCgt:	0000000180000000CapInh:	0000000000000000CapPrm:	0000001fffffffffCapEff:	0000001fffffffffCapBnd:	0000001fffffffffCapAmb:	0000000000000000Seccomp:	0Cpus_allowed:	ffffffff,ffffffff,ffffffff,ffffffffCpus_allowed_list:	0-127Mems_allowed:	00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001Mems_allowed_list:	0voluntary_ctxt_switches:	2nonvoluntary_ctxt_switches:	1

而/proc/pid/task/tid/status文件内记录进程中的线程的详细信息,其中pid为进程号,tid为线程号,需要查看哪一个线程,该tid的值就切换成其相应的值。/proc/pid/task/目录下则保存着进程内的线程,每个线程对应task下的一个目录。具体如下:

[root@localhost 77]# ps -ef | grep pthreadroot      51174   3835 99 05:31 pts/0    00:00:26 ./pthread_multithread_testroot      51226  48895  0 05:31 pts/2    00:00:00 grep --color=auto pthread
//查看进程pthread_multithread_tes内有几个线程,task下的每个目录表示一个线程[root@localhost 77]# ls -l /proc/51174/task/总用量 0dr-xr-xr-x 6 root root 0 7月 9 05:31 51174dr-xr-xr-x 6 root root 0 7月 9 05:31 51175dr-xr-xr-x 6 root root 0 7月 9 05:31 51176
//查看线程51175的详细信息[root@localhost 77]# cat /proc/51174/task/51175/statusName: pthread_multithState: R (running)Tgid: 51174Ngid: 0Pid: 51175PPid: 3835TracerPid: 0Uid: 0 0 0 0Gid: 0 0 0 0FDSize: 256Groups: 0 VmPeak: 22840 kBVmSize: 22840 kBVmLck: 0 kBVmPin: 0 kBVmHWM: 376 kBVmRSS: 376 kBRssAnon: 80 kBRssFile: 296 kBRssShmem: 0 kBVmData: 16588 kBVmStk: 136 kBVmExe: 4 kBVmLib: 1972 kBVmPTE: 36 kBVmSwap: 0 kBThreads: 3SigQ: 0/7179SigPnd: 0000000000000000ShdPnd: 0000000000000000SigBlk: 0000000000000000SigIgn: 0000000000000000SigCgt: 0000000180000000CapInh: 0000000000000000CapPrm: 0000001fffffffffCapEff: 0000001fffffffffCapBnd: 0000001fffffffffCapAmb: 0000000000000000Seccomp: 0Cpus_allowed: ffffffff,ffffffff,ffffffff,ffffffffCpus_allowed_list: 0-127Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001Mems_allowed_list: 0voluntary_ctxt_switches: 0nonvoluntary_ctxt_switches: 36154


原文始发于微信公众号(Linux二进制):Linux多线程编程【基础 | 实战】解析

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/302246.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!