引言:
我们先在学的线程都是用户级库线程(POSIX),我们通过基本的学习认识到了与线程有关的函数构成了一个完整的系列,绝大多数的函数的名字都是以“pthread”打头的,在使用这些函数的时候引入的头文件是
一、创建线程
pthread_create函数
错误检查:
- 常见的函数,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误
-pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthread函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销小。
代码实现:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <pthread.h>
6
7 void *rout(void *arg)
8 {
9 (void)arg;
10 for(;;)
11 {
12 printf("I am thread 1\n");
13 sleep(1);
14 }
15 }
16
17
18
19 int main(void)
20 {
21 pthread_t tid;
22 //创建成功返回0,失败返回错误码
23 int ret=pthread_create(&tid,NULL,rout,NULL);
24 if(ret!=0)
25 {
26 //创建失败
27 fprintf(stderr,"pthread_create:%s\n",strerror(ret));
28 exit(EXIT_FAILURE);
29 }
30 //创建成功
31 for(;;)
32 {
33 printf("I am main thread\n");
34 sleep(1);
35 }
36 }
~
代码执行的结果:
我们可以看到我们已经创建好了一个新的线程。
二、进程ID和线程ID
- 在Linux下,目前线程实现的是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
- 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程的概念之后,一个用户级线程下对应N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子都编程了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?
- linux内核引入了线程组的概念
struct task_struct
{
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
- 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应,进程描述符结构体中的pid,表面上看对应的是进程ID,其实它对应的是线程ID,进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID。
用户态 | 系统调用 | 内核进程描述符中对应的结构 |
---|---|---|
线程ID | pid_t gettpid(void); | pid_t pid |
线程ID | pid_t getpid(void); | pid_t tgid |
现在介绍的线程ID,不同于pthread_t类型的线程ID,和进程ID一样,线程ID是pid_t类型的变量,而且是唯一标识线程的一个整形变量,如何查看一个线程的ID呢?
我们可以看出a.out是多线程的,进程ID为3873,进程中有两个线程为3873和3874
ps命令中的-L选项,会显示以下信息
LWP:线程ID,即gettid()系统调用的返回值
NLWP:线程组内线程的个数
linux下提供了getpid系统调用来返回其线程ID,如果需要获得线程ID,则可采用:
#include <sys/syscall.h> pid_t tid; tid=syscall(sys_gettid);
- 我们从上面的例子中可以看出,a.out进程id为3873,下面有一个进程的ID也为3873,显然,这不是巧合,线程组内的第一个线程,在用户级态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID值设置成第一个线程的线程ID,group leader指针指向自身,即主线程的进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。
线程组ID等于线程ID,group_leader指向自身
p->tgid=p->pid;
p->group_leader=p;
INIT_LIST_HEAD(&P->thread_group);
- 线程组其他线程的ID则由内核分配,其线程组ID总是和主线程的线程组ID一致,无论主线程直接创建线程还是创建出来的线程再次创建线程,都一样。
注意:线程和进程是不一样的,进程有父进程的概念,但是在线程组里面,所有的线程是对等关系的。
三、 线程ID及进程地址空间布局
- pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和前面说的线程ID不一样。
- 线程ID属于进程调度范畴,因为进程是轻量级进程,是操作系统调度器的最小的单位,所以需要一个数值来唯一表示该线程。
- pthread_create函数产生并标记在第一个参数指向的地址中的线程ID当中,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 线程库NPTL提供了提供了pthread_self函数,可以获得线程自身的ID
对于Linux目前实现的NPTL实现而言,pthread_t 类型的线程ID,本质上就是一个进程地址空间上的一个地址。
四、线程终止
如果需要只终止某个线程而不终止整个线程
- 从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_exit来终止自己。
- 一个线程可以调用pthread_cancel终止同一个进程中的另一个线程。
pthread_exit函数
注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。
pthread_cancel函数