Linux线程概述
1、线程模型
线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体。根据运行环境和调度者的身份,线程可分为用户线程和内核线程。内核线程在有的系统上也称为LWP(Light Weight Process ,轻量级进程),运行在内核空间,由内核来调度;用户线程,运行在用户空间,由线程库来调度。当一个进程的内核线程获得CPU的使用权时,它就加载并运行一个用户线程,可见内核线程相当于用户线程运行的一个容器。一个进程可以拥有M个内核线程和N个用户线程,其中M<=N。并且在一个系统的所有进程中,M和N的比值都是确定的。按照M:N的取值,线程的实现方式可以分为三种模式:完全在用户空间实现、完全由内核调度和双层调度。
完全在用户空间实现的线程无须内核的支持,内核甚至根本不知道这些线程的存在。线程库负责管理所有执行的线程,比如线程的优先级、时间片等。线程库利用longjmp来切换线程的执行,使他们看起来像是“并发”执行的。但实际上内核仍然是把整个进程作为最小单位来调度的。换句话说,一个进程的所有执行线程共享该进程的时间片。他们对外表现出相同的优先级。因此,对这种实现方式而言,N=1,即M个用户空间线程对应一个内核线程,而该内核线程实际上就是进程本身。完全在用户空间实现的线程的优点是:创建和调度线程无需内核的干预,因此速度相当快。并且由于它不占用额外的内核资源,所以即使一个进程创建了很多线程,也不会对系统性能造成明显影响。其缺点是:对于多处理器系统,一个进程的多个线程无法运行在不同的CPU上,因为内核是按照其最小调度单位来分配CPU的。此外,线程的优先级只对同一个进程中的线程有效,比较不同进程中的线程的优先级没有意义。
完全由内核调度的模式将创建和调度的任务都交给了内核,运行在用户空间的线程库无须执行管理任务,这与完全在用户空间按实现的线程恰恰相反。二者的优缺点也正好互换。
双层调度模式是前两种实现模式的混合体:内核调度M个内核线程,线程库调度N个用户线程。这种线程实现方式结合了前两种方式的优点:不但不会消耗过多的内核资源,而且线程切换速度较快,同时他可以充分利用多处理器的优势。
2、Linux线程库
LinuxThreads线程库的内核线程是用clone系统调用创建的进程模拟的。clone系统调用和fork系统调用的作用类似:创建调用进程的子进程。不过我们可以为clone系统调用指定CLONE_THREAD标志,这种情况下它创建的子进程与调用进程共享相同的虚拟地址空间、文件描述符和信号处理函数。这些都是线程的特点。不过用进程来模拟内核线程会出现很多语义问题,比如:
- 每个线程拥有不同 的PID,因此不符合POSIX规范;
- Linux信号处理本来就是基于进程的,但现在每一个进程内部的所有线程都能而且必须处理信号;
- 用户ID、组ID对于同一个进程中的不同线程来说可能是不一样的;
- 程序产生的核心转储文件不会包含所有线程信息,而只包含产生该核心转储文件的线程的信息;
- 由于每个线程都是一个进程,因此系统允许的最大进程数就是最大线程数。
LinuxThread线程库一个有名的特性是所谓的管理线程。它是进程中专门管理其他工作线程的线程,其作用包括:
- 系统发送给进程的终止信号先由管理线程接收,管理线程在给其他工作线程发送同样的信号以终止他们;
- 当终止工作线程或工作线程主动退出时,管理线程必须等待他们结束,避免僵尸进程;
- 如果主线程先于其他工作线程主退出,则管理线程将阻塞它,直到所有工作线程都结束才唤醒它;
- 回收每个线程堆栈使用的内存。
3、创建线程和结束线程
下面讨论创建线程和结束线程的基础API。Linux系统上,它们都定义在pthread.h头文件中。
3.1 pthread_creat
创建一个线程的函数是pthread_creat。其定义如下:
#include <pthread.h>
int pthraed_creat(pthread_t* thread,const pthread_attr_t* attr,
void*(*start_routine)(void*),void* arg)
thread参数是新线程的标识符,后续pthread_*函数通过它来引用新线程。其类型pthread_t的定义如下:
#include <bits/pthreadtypes.h>
typedef unsigned long int pthread_t;
可见,pthread_t是一个整型类型。实际上,Linux上几乎所有的资源标识符都是一个整数,比如:socket、各种System V IPC标识符等。
attr参数用于设置新线程的属性。给他传递NULL表示使用默认线程属性。线程拥有众多属性,start_routine和arg参数分别指定新线程将运行的函数及其属性。
pthread_creat成功时返回0,失败时返回错误码。
3.2 pthread_exit
线程一旦被创建好,内核就可以调度内核线程来执行start_routine函数指针所指向的函数了。线程函数在结束时最好调用如下函数,以确保安全、干净的退出:
#include <pthread.h>
void pthread_exit(void * retval);
pthread_exit函数通过retval参数向线程的回收者传递退出信息。执行完后不会返回调用者,而且永远不会失败。
3.3 pthread_join
一个进程中的所有线程都可以调用pthread_join函数来回收其他线程(前提是目标线程时可回收的),即等待其他线程结束,这类似于回收线程的wait和waitpid系统调用。pthread_join定义如下:
#include <pthread.h>
int pthread_join(pthread_t pthread, void** retval);
thread参数是目标线程的标识符,retval参数则是目标线程返回的退出信息。该函数会一直堵塞,直到被回收的线程结束为止。该函数成功时返回0,失败则返回错误码。可能的错误码如下:
pthread_join函数可能引发的错误码
错误码 描述 EDEADLK 可能引起死锁。比如两个线程互相针对对方调用pthread_join,或者线程自身调用pthread_join EINVAL 目标线程是不可回收的,或者已经有了其他线程在回收该目标线程 ESRCH 目标线程不存在3.4 pthread_cancel
有时候我们需要异常终止一个线程,即取消线程,它是通过如下函数实现的:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
thread参数是目标线程的标识符。该函数成功时返回0,失败时则返回错误码。不过,接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,这分别由如下两个函数完成:
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
这两个函数的第一个参数分别用于设置线程的取消状态(是否允许取消)和取消类型(如何取消),第二个参数则分别记录线程原来的取消状态和取消类型。state参数有两个可选值:
PTHREAD_CANCEL_ENABLE; //允许线程被取消。它是线程被创建时的默认取消状态。
PTHREAD_CANCEL_DISABLE;//禁止线程被取消。在这种情况下,如果一个线程收到取消请求,则他会将请求挂起,直到该线程允许被取消。
type参数也有两个可选值:
PTHREAD_CANCEL_ASYNCHRONOUS; //线程随时可以被取消。它将使得接收到取消请求的目标线程立即采取行动
PTHREAD_CANCEL_DEFERRED;//允许目标线程推迟行动,直到它调用了下面几个所谓的取消点函数中的一个:pthread_join、pthread_testcancel、pthread_cond_wait、pthread_cond_timedwait、sem_wait和sigwait。