一、线程概念
1、引入
我们知道,进程在各自独立的地址空间中运行,进程之间共享数据需要用mmap(将一个文件或者其它对象映射进内存)或者进程间通信机制,本篇我们将学习如何在一个进程的地址空间中执行多个线程。有些情况需要在一个进程中同时执行多个控制流程,这时候线程就派上了用场,比如实现一个图形界面的下载软件, 一方面需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个文件, 等待和处理从多个网络主机发来的数据,这些任务都需要一个“等待-处理”的循环,可以用多线程实现,一个线程专门负责与用户交互,另外一个线程负责和一个网络主机通信。
2、什么叫线程
在一个程序里的多个执行路线就叫做线程。更准确的定义是:线程是“一个进程内部的一个控制序列”。典型的unix进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程以后,在程序设计时可以把进程设计成在同一时刻能够做不止一件事,每个线程处理各只独立的任务。线程可以看作是轻量级进程,它是操作系统调度的基本单位。main函数和信号处理函数是同一个进程地址空间中的多个控制流程,多线程也是如此,但是比信号处理函数更加灵活,信号处理函数的控制流程只是在信号递达时产生,在 处理完信号之后就结束,而多线程的控制流程可以长期并存,操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。
3、线程特性
同一进程的多个线程共享同一地址空间。其中Text Segment、 Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:(1)文件描述符表;(2)每种信号的处理方式(SIG_IGN、 SIG_DFL或者自定义的信号处理函数);(3)当前工作目录;(4)用户id和组id。但有些资源是每个线程独有一分的:(1)线程id;(2)上下文,包括各种寄存器的值、程序计数器和栈指针;(3)栈空间;(4) errno变量;(5)信号屏蔽字;(6)调度优先级。
4、线程的优缺点
优点:(1)通过为每种事件类型的处理分配单独的线程,能够简化处理异步时间的代码;
(2)多个线程可以自动共享相同的存储地址空间和文件描述符;
(3)有些问题可以通过将其分解从而改善整个程序的吞吐量;
(4)交互的程序可以通过使用多线程实现相应时间的改善,多线程可以把程序中处理用户输入输出的部分与其它部分分开。
缺点:线程也有不足之处。编写多线程程序需要更全面更深入的思考。在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的。调试一个多线程程序也比调试一个单线程程序困难得多。
二、线程控制
1、线程的创建
名称:pthread_create
功能:创建线程
头文件:#include <pthread.h>
函数原形:int pthread_create(pthread_t *thread,const pthread _attr_t *attr,void *(*start_routine)(void*),void *restrict arg);
参数:第一个参数thread指id
返回值:若成功返回则返回0,否则返回错误编号(返回错误码更加清晰。)
当pthread_creat()成功返回时, thread指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性。可以把它设置为NULL,创建默认的线程属性。新创建的线程从start_routine函数的地址开始运行,该函数只有一个无类型指针参数arg,如果需要向start_routine函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。
关于进程的编译我们都要加上参数 –lpthread 否则提示找不到函数的错误。
具体编译方法是 gcc –lpthread –o pthread pthread.c
运行结果为
以前学过的系统函数都是成功返回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_join。
新创建的线程的id被填写到thread参数所指向的内存单元。我们知 道进程id的类型是pid_t,每个进程的id在整个系统中是唯⼀的,调⽤getpid(2)可以获得当前进程 的id,是⼀个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯⼀的,在不同的系 统中thread_t这个类型有不同的实现,它可能是⼀个整数值,也可能是⼀个结构体,也可能是⼀ 个 地址,所以不能简单地当成整数⽤printf打印,调⽤pthread_self(3)可以获得当前线程的id。
2、名称:pthread_self
功能:获取自身线程的id
头文件:#include <pthread.h>
函数原形:pthread_t pthread_self(void);
参数:无
返回值:调用线程的线程id
上例中就已经用到了这个函数。
3、线程的终止
线程是依进程而存在的,当进程终止时,线程也就终止了。当然也有在不终止整个进程的情况下停止它的控制流。
1)从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2) 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3)线程可以调用pthread_exit终止自己。 用pthread_cancel终止一个线程分同步和异步两种情况,比较复杂,后面再详细介绍。
(1)名称:pthread_exit
功能:终止一个线程
头文件:#include <pthread.h>
函数原形:void pthread_exit(void *rval_ptr);
参数:rval_prt是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以调用pthread_join函数访问到这个指针。
返回值:无
retval是void *类型,和线程函数返回值的用法一样,其它线程可以调用pthread_join获得这个指针。 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
(2)线程等待
名称:pthread_join
功能:调用该函数的线程将挂起等待,直到id为thread的线程终止。
头文件:#include <pthread.h>
函数原形:int pthread_join(pthread_t thread,void **rval_ptr);
参数:
返回值:若成功返回0,否则返回错误编号。
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
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 参数。当一个线程通过调用pthread_exit退出或者简单地从启动历程中返回时,进程中的其他线程可以通过调用pthread_join函数获得进程的退出状态。调用pthread_join进程将一直阻塞,直到指定的线程调用pthread_exit,从启动例程中或者被取消。
如果线程只是从它的启动历程返回,rval_ptr将包含返回码。
输出结果如下:
可见在Linux的pthread库中常数PTHREAD_CANCELED的值是-1。可以在头文件pthread.h 中找到它的定义。
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。 但是线程也可以被置为detach 状态,这样的线程一旦终止就立刻回收它占用的所有资源, 而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。 对一个尚未detach的线程调用pthread_join或pthread_detach都可以把该线程 置为detach状态,也 就是说,不能对同一线程调用两次pthread_join,或者如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
4、线程分离
原型:int pthread_detach(pthread_t thread);
功能:线程分离;成功返回0,失败返回错误号。
在任何⼀个时间点上, 线程是可结合的(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),如此⼀来,该线程运⾏结束后会⾃动释
放所有资源。
三、线程与进程
1、各自的定义
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
2、关系
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
3、区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
4、优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。