【 Linux 】多线程编程

                                              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 
int pthraed_creat(pthread_t* thread,const pthread_attr_t* attr,
                  void*(*start_routine)(void*),void* arg)

thread参数是新线程的标识符,后续pthread_*函数通过它来引用新线程。其类型pthread_t的定义如下:

#include 
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 

void pthread_exit(void * retval);

pthread_exit函数通过retval参数向线程的回收者传递退出信息。执行完后不会返回调用者,而且永远不会失败。

3.3 pthread_join

一个进程中的所有线程都可以调用pthread_join函数来回收其他线程(前提是目标线程时可回收的),即等待其他线程结束,这类似于回收线程的wait和waitpid系统调用。pthread_join定义如下:

#include 
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 

int pthread_cancel(pthread_t thread);

thread参数是目标线程的标识符。该函数成功时返回0,失败时则返回错误码。不过,接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,这分别由如下两个函数完成:

#include 

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。

 

你可能感兴趣的:(【 Linux 】多线程编程)