说在前面:与线程相关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的,要想使用这些函数,要通过引入头文,链接这些线程函数库时要使用编译器命令的“-lpthread”选项
创建线程
//创建一个新的线程
int pthread_create(pthread_t *threade,const pthreade_attr_t *attr,void *(*start_routine)(void*),void *arg);
//参数:thread:返回线程ID
//attr:设置线程的属性,attr为NULL时表示使用默认属性
//start_routine:是个函数地址,线程启动后要执行的函数
//arg:传给线程启动函数的参数
//返回值:成功返回0,失败返回错误码
错误检查:我们常见的一些函数都是成功返回0,失败返回-1。并且对全局变量errno赋值以指示错误。pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误码以返回值的形式返回。pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小。
现在我们可以用这个函数试试创建一个线程:
//Makefile文件内容:
thread:thread.c
gcc -g $^ -o $@ -lpthread
.PHONY:clean
clean:
rm -rf thread
//thread.c文件内容:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
//创建出来一个线程时,该线程做的工作有该函数定义
void *rout(void *arg)
{
(void)arg;
for(;;)
{
printf("I am thread 1\n");
sleep(1);
}
}
int main()
{
pthread_t tid;
//创建成功返回0,失败返回错误码
int ret = pthread_create(&tid,NULL,rout,NULL);
if(ret != 0)
{
//创建失败
fprintf(stderr,"pthread_create:%s\n",strerror(ret));
exit(EXIT_FAILURE);
}
//创建成功
for(;;)
{
printf("I am main thread\n");
sleep(1);
}
return 0;
}
由图可以看出,成功创建了一个线程。
进程ID和线程ID:在Linux中,目前的线程实现是Native POISX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID,但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在求进程内的所有线程调用getpid函数是返回相同的进程ID,为了解决这个问题,由此引入了线程组的概念。
所谓的线程组就是多线程的进程,线程组内的每一个线程在内核之中都存在的一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看是对应的进程ID,其实他对应的是线程ID;进程描述符中的tgid(Thread Group ID),该值对应的是用户层面的进程ID。
现在说的 线程ID不同于pthread_t类型的线程ID,和进程ID一样该线程ID是pid_t类型的变量,而且是用来唯一标识线程的整型变量。
命令查看线程ID:(以上文提到的创建线程的代码为例)
ps命令中的-L选项会显示:LWP(线程ID),即gettid()系统调用的返回值。
NLWP,线程组内线程的个数。
从图中我们可以观察到,thread进程是多线程的,进程的ID是73917。
进程内有两个线程(NLWP=2),线程ID分别为73917和73918(LWP所对应的值)。
当然我们也发现,进程ID与其中一个线程ID相等了,事实上,线程组内的第一个线程,在用户态被称为主线程,在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的ID,group_leader的指针则指向自身,即主线程的进程描述符,所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。对于线程组内的其他线程的ID就有内核具体分配。其中只需要明确一点的就是,主线程的ID总是与线程组的ID保持一致,无论是主线程直接创建线程,还是创建出来的线程再次创建的线程。
线程ID及进程地址空间布局
回顾一下pthread_creat函数,该函数调用成功以后会产生一个线程ID,并且将其存放在第一个参数(pthread_t thread)指向的地址中。然而该线程ID与刚刚前面讲过的线程ID却不是一回事儿。前面讲的线程ID属于进程调度的范畴,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一标识该线程。而我们的pthread_creat函数产生并且标记在第一个参数指向的地址中的线程ID,属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作线程的。对于目前Linux实现的NPTL,pthread_t类型的线程ID本质就是一个进程地址空间上的一个地址。
线程终止
终止线程而不终止进程的方法
1、从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit
2、线程可以调用pthread_exit终止自己
3、一个线程可以调用pthread_cancel终止同一进程中的另一个线程
pthread_exit函数:
//终止线程
void pthread_exit(void *value_ptr);
//参数value_ptr:不要将其指向一个局部变量
//无返回值,和进程一样线程结束的时候无法返回到他的调用者(自身)
注意:该函数或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针的时候该线程已经退出了。
pthread_cancel函数:
//取消一个执行中的线程
int pthread_cancel(pthread_t thread);
//参数:thread为线程ID
//pthread_self()函数可以获取自身的线程ID
//返回值:成功返回0,失败返回错误码