注意:在Linux上线程函数位于libpthread共享库中,因此在编译时要加上-lpthread选项。
1.线程概念
1)线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
2)线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
2.线程的特点
在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
1)轻型实体
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
2)独立调度和分派的基本单位。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
3)可并发执行。
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
4)共享进程资源。
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
(1)⽂件描述符表
(2)每种信号的处理⽅式(SIG_IGN、 SIG_DFL或者自定义的信号处理函数)
(3)当前⼯作目录
(4)用户id和组id
但有些资源是每个线程各有⼀份的:
(1)线程id
(2)上下文,包括各种寄存器的值、程序计数器和栈指针
(3)栈空间
(4)errno变量
(5)信号屏蔽字
(6)调度优先级
3.线程与进程比较
进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。
另外,进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。
与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源。
线程只由相关堆栈(系统栈或用户栈)寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其他线程的相关变量。
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。因而近年来推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。
线程与进程的区别可以归纳为以下4点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程OS中,进程不是一个可执行的实体。
4.线程控制
①创建线程
int pthread_create(pthread_t tidp, const pthread_attr_t *attr, (void)(start_rtn)(void), void *arg);
参数解释
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
返回值:成功返回0,失败返回错误号,pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有⼀个errno,但这是为了兼容其它函数接⼜⽽提供的,pthread库本⾝并不使⽤它,通过返回值返回错误码更加清晰。
代码:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
pthread_t tid;
void* thread_run(void* _val)
{
printf("%s : pid is : %d,tid is : %u\n",(char*)_val,(int)getpid(),(unsigned long long)pthread_self());
return NULL;
}
int main()
{
int err=pthread_create((&tid),NULL,thread_run,"other thread run");
if(err!=0){
printf("create thread error! info is :%s\n",strerror(err));
exit(err);
}
printf("main thread run : pid is : %d,tid is : %u\n",(int)getpid(),(unsigned long long)pthread_self());
sleep(1);
return 0;
}
运行结果:
可知在Linux上,pthread_ t 类型是⼀个地址值,属于同⼀进程的多个线程调⽤getpid(2)可以得到相同的进程号,而调用pthread_self(3)得到的线程号各不相同。
由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。
如果任意⼀个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是⼀种权宜之计,即使主线程等待1秒,内核也不⼀定会调度新创建的线程执行。
②线程等待
pthread_join
int pthread_join(pthread_t thread, void **retval);
参数 :thread: 线程标识符,即线程ID,标识唯一线程。retval: 用户定义的指针,用来存储被等待线程的返回值。
返回值:0代表成功。失败,返回的则是错误号。
代码:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<pthread.h>
#include<time.h>
#include<stdlib.h>
pthread_t ttid;sigset_t set;
void myfunc()
{
printf("hello\n");
}
static void *mythread(void* p)
{
int signum;
while(1){
sigwait(&set,&signum);
if(SIGUSR1==signum)
myfunc();
if(SIGUSR2==signum){
printf("I will sleep 2 second and exit\n");
sleep(2);
break;
}
}
}
int main()
{
char tmp;
void *status;
sigemptyset(&set);
sigaddset(&set,SIGUSR1);
sigaddset(&set,SIGUSR2);
sigprocmask(SIG_SETMASK,&set,NULL);
pthread_create(&ttid,NULL,mythread,NULL);
while(1){
printf(":");
scanf("%c",&tmp);
if('a'==tmp){
pthread_kill(ttid,SIGUSR1);//发送SIGUSR1,打印字符串。
}
else if('q'==tmp){
//发出SIGUSR2信号,让线程退出,如果发送SIGKILL,线程将直接退出。
pthread_kill(ttid,SIGUSR2);
//等待线程tid执行完毕,这里阻塞。
pthread_join(ttid,&status);
printf("finish\n");
break;
}
else
continue;
}
return 0;
}
运行结果:
如果输入a,子线程打印”hello”,主程序继续等待输入;
如果输入q,主程序等待子程序结束。子线程打印”I will sleep 2 second and exit”,并延时两秒后结束。主线程随之打印”finish”,程序结束。
在前面我们提到,可以通过pthread_join()函数来使主线程阻塞等待其他线程退出,这样主线程可以清理其他线程的环境。但是还有一些线程,更喜欢自己来清理退出的状态,他们也不愿意主线程调用pthread_join来等待他们。我们将这一类线程的属性称为detached。如果我们在调用pthread_create()函数的时候将属性设置为NULL,则表明我们希望所创建的线程采用默认的属性,也就是joinable。
③终止线程
如果需要只终止某个线程而不终止整个进程,可以有三种⽅法:
1.从线程函数return。这种⽅法对主线程不适用,从main函数return相当于调⽤exit。
2.⼀个线程可以调用pthread_cancel终止同⼀进程中的另⼀个线程。
3.线程可以调用pthread_exit终止自己。
void pthread_exit(void* retval);
线程通过调用pthread_ exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。retval是void *类型,和线程函数返回值的用法⼀样,其它线程可以调⽤pthread_join获得这个指针。
注意:
pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函树的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
总结:
1.如果thread线程通过return返回,value_ptr所指向的单元⾥存放的是thread线程函数的返回值。
2.如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
3.如果thread线程是自己调用pthread_ exit终止的,value_ ptr所指向的单元存放的是传给pthread_ exit的参数。 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。
5.线程分离与结合
在任何⼀个时间点上,线程是可结合的(joinable)或者是分离的(detached)。⼀个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反,⼀个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统⾃动释放。
默认情况下,线程被创建成可结合的。 为了避免存储器泄漏,每个可结合线程都应该要么被显示地回收,即调⽤pthread_ join;要么通过调用pthread_detach函数被分离。
如果⼀个可结合线程结束运行但没有被join,则它的状态类似于进程中的Zombie Process,即还有⼀部分资源没有被回收,所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出码,回收其资源。
由于调⽤pthread_ join后,如果该线程没有运⾏结束,调⽤者会被阻塞,在有些情况下我们并不希望如此。例如,在Web服务器中当主线程为每个新来的连接请求创建⼀个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的连接请求),这时可以在子线程中加⼊代码
pthread_detach(pthread_self())或者父线程调用pthread_detach(thread_id)(⾮阻塞,可立即返回),这将该子线程的状态设置为分离的(detached),如此⼀来,该线程运行结束后会自动释放所有资源。