在线程的概念一文中,有提到在Linux下的线程的实现是通过NPTL线程库实现的。即每一个用户态的线程对应于内核中的一个调度实体(一个PCB/一个内核级线程)。可以说,一个Linux下的线程包含两部分组成:一个用户级的线程,一个内核级的线程(PCB)。
之前有提到过,操作系统并不知道用户级线程的存在。而Linux中的线程又包含一个用户级的线程。所以该用户级线程的实现是通过用户级线程库POSIX来完成的。有关线程的一些函数实现也是封装在该用户级别的库中。因此:
(1)与线程有关的大多数函数都是以“pthread”打头的
(2)要使用这些函数,就要引入头文件“<pthread.h>”
(3)链接这些线程函数库时要加上“-pthread”选项,指定要链接的库名。可以通过以下方式来查找该库:
find /usr/lib -name libpthread.*
创建线程
int pthread_create(pthread_t* thread,const pthread_attr_t* attr,void*(*start_routine)(void*),void* arg);
参数:
thread:引用该函数创建的线程ID存放在thread指针指向的空间中,关于用户级线程id类型可以通过以下方式查找:
grep -ER "pthread_t" /usr/include
/usr/include/bits/pthreadtypes.h:typedef unsigned long int pthread_t;结果显示,pthread_t类型是一个无符号长整型。
attr:设置线程的属性,一般设置为BULL(默认属性)
start_routine:是一函数指针,该函数的返回值和参数都是void*类型的。当新线程创建好后,就去执行该函数指针指向的函数
arg:上述start_routine函数指针指向的函数的参数
返回值:成功返回0,失败返回错误码
注意:
(1)传统的一些函数,成功返回0,失败返回-1,错误码赋给errno以指示错误。比如对于open函数,文件成功打开,返回值为0。打开失败,返回值为-1。我们一般会通过perror或strerror函数先显示错误信息,而perror函数就是通过errno的取值(错误码)来显示错误信息。strerror的参数也是errno(错误码)。
(2)大多数POSIX函数(包含pthreads类型)不会设置全局变量errno,而是将错误码以返回值的形式给出。
(3)pthreads函数也设置了线程内得errno变量,以支持其他使用errno的代码。但是建议通过返回值来判断错误码,因为读取返回值比读取errno的开销更小。因此,可以通过将返回值传参给strerror函数,来显示错误信息。
下面通过该函数来创建一个新线程:
#include<stdio.h> #include<pthread.h> #include<unistd.h> //新线程创建成功后执行的函数 void* run(void* arg) { while(1) { printf("%s\n",(char*)arg); sleep(1); } } int main() { pthread_t tid; //新线程创建成功后去执行run指向的函数,主线程接着执行后面的代码 pthread_create(&tid,NULL,run,"i am new pthread");//主线程创建新线程 while(1)//主线程要执行的代码 { printf("i am main pthread\n"); sleep(1); } return 0; }
运行结果:
[admin@localhost pthread]$ ./a.out i am main pthread i am new pthread i am main pthread i am new pthread ^C
结果显示,主线程和新线程交替执行,说明程序中有两个执行流。如果只有一个执行流,对于有两个死循环的程序,永远只会执行第一个死循环,而不会两个同时运行。所以说,线程是调度的基本单位。
注意:程序执行后,那个线程先运行是由调度器决定的。
线程ID和进程ID
在上面有提到,Linux下的线程实现是通过POSIX线程库实现的:一个用户级线程对应于内核中的一个调度实体。
1. 用户级线程ID
对于每一个用户级线程,都有唯一的线程id来标识,即上述所提到过的pthread_t类型的变量。该类型我们已经查看过,他是一个无符号长整型。那它本质上又代表的是什么呢?对于Linux的NPTL实现而言,pthread_t类型的线程ID,本质上是一个进程空间中的一个地址。如下图:
可以通过以下函数来查看用户级线程自身的ID:
pthread_t pthread_self(void);
下面,来查看用户级线程的ID:
#include<stdio.h> #include<pthread.h> #include<unistd.h> //新线程创建成功后执行的函数 void* run(void* arg) { printf("new pthread id:%d\n",pthread_self()); } int main() { pthread_t tid; printf("main pthread id:%d\n",pthread_self()); //新线程创建成功后去执行run指向的函数,主线程接着执行后面的代码 pthread_create(&tid,NULL,run,"i am new pthread");//主线程创建新线程 printf("main:new pthread id:%d\n",tid); sleep(1); return 0; }
结果如下:
可以看到,在主函数中通过pthread_create的参数返回的线程id和pthread_self返回的线程id相同。
2. 调度实体的线程ID(内核级线程ID)和进程ID
在Linux中,线程是通过NPTL实现的。在这种实现下,线程又被称为轻量级进程。一个用户态线程对应于内核中的一个调度实体。也拥有自己的进程描述符(PCB)。
因此,在一个用户进程中可能对应多个用户态线程,进而对应内核中的多个调度实体即多个PCB。所以进程与PCB的关系变成了1:N的关系。所以就需要一个标识符ID来唯一的标识一个调度实体。
多个调度实体对应一个进程。所以多个线程调用getpid时应返回相同的进程PID。
为了解决上述两个ID,Linux内核引入了线程组的概念:
多线程的进程又被称为线程组。线程组内的每一个线程对应于内核中的调度实体,每个调度实体都有一个唯一的进程标识符PCB,在该PCB中存放的ID,表面上是进程ID,其实是该线程即调度实体的ID。而PCB中的tgid才是真正意义上的进程ID,该进程ID其实是进程中第一个线程即调度实体的ID。
也就是说,对于进程中的第一个线程,它的PCB中存放的pid和tgid是相同的。对于其他线程来说,各自拥有自己的pid,但tgid与第一个线程的pid相同。
将进程中的第一个线程(主线程)称为该线程租的组长。在上图中的第三个参数均指向组长线程的PCB,组长线程指向自身。
(1)通过命令查看调度实体(轻量级进程)的ID和进程ID
此处的ID与进程的PID类型相同,都是pid_t类型的变量。
(2)通过函数调用查看轻量级进程ID和进程ID
进程ID的查看方式之前有说过,是通过getpid来进行查看的。
轻量级进程ID通过以下方式查看:
#include<sys.syscall.h> pid_t tid; yid = syscall(SYS_gettid);
#include<stdio.h> #include<pthread.h> #include<unistd.h> #include<sys/syscall.h> //新线程创建成功后执行的函数 void* run(void* arg) { pid_t new_tid; new_tid = syscall(SYS_gettid); printf("new: tgid = %d, tid:%d\n",getpid(),new_tid); } int main() { pid_t main_tid; main_tid = syscall(SYS_gettid); printf("main:tgid = %d, tid = %d\n",getpid(),main_tid); pthread_t tid; //新线程创建成功后去执行run指向的函数,主线程接着执行后面的代码 pthread_create(&tid,NULL,run,"i am new pthread");//主线程创建新线程 sleep(1); return 0; }
运行结果:
与用命令查看的结果类似。
注意:进程之间存在着父子关系,而线程之间都是对等的。
线程终止
终止某个线程而不终止整个进程有下面三种方法:
(1)在线程函数中return。
(2)线程自己调用pthread_exit来终止自己
int pthread_exit(void* value_ptr);
该函数的参数即为该线程退出时的退出码即该线程调用的函数start_routine的返回值。
注意:函数的参数不要指向一个局部变量,因为当线程的调用函数退出时,局部变量也就销毁了。
该函数无返回值。
(3)一个线程调用pthread_cancel来终止另外一个线程
int pthread_cancel(pthread_t thread);
参数:要终止线程的线程ID。
返回值:成功返回0,失败返回错误码
注意:以上三种方式的退出方式均用于新线程。如果是主线程退出,应以进程的方式退出(在main中调用return或在任意位置调用exit)。所以说,主线程退出相当于进程退出。
线程等待
当一个线程退出时,如果空间没有被释放,新创建的线程也不会利用退出线程的资源,所以会发生内存泄漏。因此新线程在退出时主线程要通过等待的方式回收退出现成的资源并获取新线程退出时的状态。
int pthread_join(pthread_t pthread,void** value_ptr);
参数:pthread为主线程要等待的进程
value_ptr为一指针,指向新线程退出码所在的空间,即为新线程退出码的地址
返回值:成功返回0,失败返回错误码
注意:该函数是以阻塞的方式进行等待的
thread线程以不同的方式终止,得到的线程终止状态也是不同的:
(1)线程以return的方式终止,value_ptr指向的空间中保存return返回的值
(2)线程以pthread_exit的方式终止,value_ptr指向的空间中保存该函数的参数
(3)线程以pthread_cancel的方式终止,value_ptr指向的空间中保存常数PTHREAD_CANCELED其实为-1.即
#define PTHREAD_CANCELED (void*)-1;
(4)如果不关心新线程的退出状态,直接将value_ptr设置为NULL即可。
代码演示:
#include<stdio.h> #include<pthread.h> #include<unistd.h> void* pthread1(void* arg) { printf("i am phread1\n"); return (void*)1; } void* pthread2(void* arg) { printf("i am phread2\n"); pthread_exit((void*)2); } void* pthread3(void* arg) { printf("i am phread3\n"); while(1) { sleep(1); } return (void*)5; } int main() { pthread_t tid; void* ret; printf("main tid : %x\n",pthread_self()); //以return方式退出 pthread_create(&tid,NULL,pthread1,NULL); pthread_join(tid,&ret); printf("pthread1 tid:%x return code: %d\n",tid,(int)ret); //以pthread_exit方式退出 pthread_create(&tid,NULL,pthread2,NULL); pthread_join(tid,&ret); printf("pthread1 tid:%x return code: %d\n",tid,(int)ret); //以pthread_cancel方式退出 pthread_create(&tid,NULL,pthread3,NULL); pthread_cancel(tid); pthread_join(tid,&ret); printf("pthread3 id:%x return code: %d\n",tid,(int)ret); return 0; }
运行结果:
线程的分离
如果主线程不关心新线程的退出状态。这时可以对线程进行分离,使新线程退出时,自动释放资源。
int pthread_detach(pthread_t thread);
参数为要分离的线程id。成功返回0,失败返回错误码。
可以是线程组内的其他线程对目标线程进行分离,也可以自己分离。
注意:新创建的线程默认是不可分离的即可结合的。
一个分离的线程异常退出,整个进程也会异常退出。
#include<stdio.h> #include<pthread.h> #include<unistd.h> void* run(void* arg) { pthread_detach(pthread_self()); printf("hello\n"); return (void*)1; } int main() { pthread_t tid; void *ret; pthread_create(&tid,NULL,run,NULL); //pthread_detach(tid); sleep(1); int r = pthread_join(tid,&ret); if(r == 0) { printf("wait success\n"); printf("%d\n",(int)ret); } else { printf("wait fail\n"); } return 0; }
上面红字标注的两种分离方式结果相同: