一、线程的概念
进程在各个独立的地址空间中运行,进程之间共享数据需要mmap或者进程间通信机制,本节我们学习如何在一个进程的地址空间中执行多个线程。
有些情况需要在一个进程中同时执行多个控制流程,这时候线程就派上了场,如实现一个图形界面的下载软件,需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个文件,等待和处理从多个网络主机发来的数据,这些任务都需要一个“ 等待-处理”的循环,可以用多线程实现,一个线程专门 负责与用户交互,另外几个线程每个线程负责和一个网络主机通信。
main 函数和信号处理函数是同一个进程地址空间中的多个控制流程,多线程也是如此,但是比信号处理函数更加灵活,信号处理函数的控制流程只是在信号递达时产生,在处理完信号之后就结束,而多线程的控制流程可以长期并存,操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。由于同一进程的多个线程共享同一地址空间,因此Text Segment(代码段)、Data Segment(数据段)都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
1. 文件描述符表
2. 每种信号的处理方式(SIG_IGN 、SIG_DFL或者自定义的信号处理函数)
3. 当前工作目录
4. 用户id 和组id
但是有些资源是每个线程各自一份的:
1.线程id(由库函数自己生成 pthread_t( unsigned long long))
2.上下文,包括各种寄存器的值。程序计数器和栈指针
3.栈空间
4.errno变量
5.信号屏蔽字
6.调度优先级
我们将要学习的线程库函数是有POSIX标准定义的,称为POSIXthread或者pthread。 在Linux上 线程函数位于libpthread共享库中,因此编译时要加上 -lpthread选项。
二、线程控制
A.创建线程
返回值:成功返回0, 失败返回错误号。以前学过的系统函数都是成功返回0, 失败返回-1,错误号保存在全局变量errno中,pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno, 但这是为了兼容其它函数接口提供的,pthread 库本身并不使用它,通过返回值返回错误码更加清晰。
在一个线程中调pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决 定。start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为
void *, 这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *, 这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程 可以调用pthread_join()得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态。
pthread_create成功返回后,新创建的线程的id 被填写到thread参数所指向的内存单元。我们知道进程id 的类型是pid_t,每个进程的id在整个系统中是唯一的,调getpid(2) 可以获得当前进程的id,是一个正整数值。线程id 的类型是thread_t, 它只在当前进程中保证是唯一的,在不同的系统中thread_t 这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数printf打印,调用pthread_self(3) 可以获得当前线程的id 。
attr参数表示线程属性,再次不深入讨论线程属性,所有代码例子都传NULL给attr参数,表示线程属性取缺省值。
如下是实例:
#include
#include
#include
#include
void* pthread_test(void* arg)
{
int i=0;
while(i++<10)
{
printf("this is thread! pid:%d,tid: %u\n",(int)getpid(),(unsigned long)pthread_self());
sleep(1);
}
return NULL;
}
pthread_t ptr;
int main()
{
int pthread_ret=pthread_create(&ptr,NULL,pthread_test,NULL);
if(pthread_ret!=0)
{
printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));
exit(pthread_ret);
}
int i=0;
while(i++<10)
{
printf("this is man thread pid:%d,tid: %u!\n",(int )getpid(),(unsigned long long)pthread_self());
sleep(1);
}
return 0;
}
运行结果:
可知在Linux上,thread_t类型是一个地址值, 属于同一进程的多个线程调getpid(2)可以得到相同的进程号,调用pthread_self(3)得到的线程号不相同各。由于pthread_create的错误码不保存在errno中,因此不能直perror(3) 打印错误信息,可以先strerror(3) 把错误码转换成错误信息再打印。
如果任意一个线程调用了exit 或_exit, 则整个进程的所有线程都终止,由于从main函数return也相当于调用exit, 为了防止新创建的线程还没有得到执行就终止,我们在main函数return 之前延 时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下面我们会看到更好的办法。
思考题:主线程在一个全局变量ntid 中保存了新创建的线程的id,如果新创建的线程不调用pthread_self 而是直接打印这个ntid, 能不能达到同样的效果?
解答:通过测试得到 线程创建成功时 新建线程的tid确实被保存在thread 地址 所指向的空间(图 1)。但有时我们为了快捷可能用同一个全局变量ntid 创建多个子进程 这时ntid肯定为最后创建的ntid值(图 2),因此只有pthread_self可以达到目的。(自己拙见)
图 1:
图 2:
B. 终止线程
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1.从线程函数return。这种方法对主线程不适和,从main函数return 相当于调用exit 。
2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3. 线程可以调pthread_exit终止。
pthread_cancel终止一个线程分同步和异步两种情况,比较复杂,自此不介绍。
下面介绍pthread_exit的和pthread_join的用法。