当前位置: 首页 > news >正文

技术支持 长沙网站建设-创研科技外贸网站建设公司

技术支持 长沙网站建设-创研科技,外贸网站建设公司,页面设计span,医院网站开发违法吗目录 早期 Linux 不支持线程#xff0c;直到 1996 年#xff0c;Xavier Leroy 等人才开发出第一个基本符合 POSIX 标准的线程库 LinuxThreads 。但 LinuxThreads 效率低而且问题很多。自内核 2.6 开始#xff0c;Linux 才真正提供内核级的线程支持#xff0c;并有两个组织…目录 早期 Linux 不支持线程直到 1996 年Xavier Leroy 等人才开发出第一个基本符合 POSIX 标准的线程库 LinuxThreads 。但 LinuxThreads 效率低而且问题很多。自内核 2.6 开始Linux 才真正提供内核级的线程支持并有两个组织致力于编写新的线程库NGPTNext Generation POSIX Threads和NPTLNative POSIX Thread Library。不过前者在 2003 年就放弃了因此新的线程库就称为 NPTL 。NPTL 比 LinuxThreads 效率高且更符合 POSIX 规范所以它已经成为 glibc 的一部分。本书所有线程相关的例程使用的线程库都是 NPTL 。本章要讨论的线程相关的内容都属于 POSIX 线程简称 pthread标准而不局限于 NPTL 实现具体包括 创建线程和结束线程读取和设置线程属性POSIX 线程同步方式POSIX信号量、互斥锁和条件变量。还将介绍在 Linux 环境下库函数、进程、信号与多线程程序之间的相互影响。 一、Linux 线程概述 1. 线程模型 线程是程序中完成一个独立任务的完整执行序列即一个可调度的实体。根据运行环境和调度者的身份线程可分为内核线程和用户线程。内核线程在有的系统上也称为 LWPLight Weight Process轻量级进程运行在内核空间由内核来调度用户线程运行在用户空间由线程库来调度。当进程的一个内核线程获得 CPU 的使用权时它就加载并运行一个用户线程。可见内核线程相当于用户线程运行的“容器”。一个进程可以拥有 M 个内核线程和 N 个用户线程其中 M ≤ N 。并且在一个系统的所有进程中M 和 N 的比值都是固定的。按照 M : N 的取值线程的实现方式可分为三种模式完全在用户空间实现、完全由内核调度和双层调度two level scheduler。 完全在用户空间实现的线程无须内核的支持内核甚至根本不知道这些线程的存在。线程库负责管理所有执行线程比如线程的优先级、时间片等。线程库利用 longjmp 来切换线程的执行使它们看起来像是“并发”执行的。但实际上内核仍然是把整个进程作为最小单位来调度的。换句话说一个进程的所有执行线程共享该进程的时间片它们对外表现出相同的优先级。因此对这种实现方式而言N 1 即 M 个用户空间线程对应 1 个内核线程而该内核线程实际上就是进程本身。完全在用户空间实现的线程的优点是创建和调度线程都无须内核的干预因此速度相当快。并且由于它不占用额外的内核资源 所以即使一个进程创建了很多线程也不会对系统性能造成明显的影响。其缺点是对于多处理器系统一个进程的多个线程无法运行在不同的 CPU 上因为内核是按照其最小调度单位来分配 CPU 的。此外线程的优先级只对同一个进程中的线程有效比较不同进程中的线程的优先级没有意义。早期的伯克利 UNIX 线程就是采用这种方式实现的。完全由内核调度的模式将创建、调度线程的任务都交给了内核运行在用户空间的线程库无须执行管理任务这与完全在用户空间实现的线程恰恰相反。二者的优缺点也正好互换。较早的 Linux 内核对内核线程的控制能力有限线程库通常还要提供额外的控制能力尤其是线程同步机制不过现代 Linux 内核已经大大增强了对线程的支持。完全由内核调度的这种线程实现方式满足 M : N 1 : 1 即 1 个用户空间线程被映射为 1 个内核线程。双层调度模式是前两种实现模式的混合体内核调度 M 个内核线程线程库调度 N 个用户线程。这种线程实现方式结合了前两种方式的优点不但不会消耗过多的内核资源而且线程切换速度也较快同时它可以充分利用多处理器的优势。 2. Linux 线程库 Linux 上两个最有名的线程库是 LinuxThreads 和 NPTL 它们都是采用 1 : 1 的方式实现的。由于 LinuxThreads 在开发的时候Linux 内核对线程的支持还非常有限所以其可用性、稳定性以及 POSIX 兼容性都远远不及 NPTL 。现代 Linux 上默认使用的线程库是 NPTL 。用户可以使用如下命令来查看当前系统上所使用的线程库 $getconf GNU_LIBPTHREAD_VERSION NPTL 2.14.90LinuxThreads 线程库的内核线程是用 clone 系统调用创建的进程模拟的。clone 系统调用和 fork 系统调用的作用类似创建调用进程的子进程。不过我们可以为 clone 系统调用指定 CLONE_THREAD 标志这种情况下它创建的子进程与调用进程共享相同的虚拟地址空间、文件描述符和信号处理函数这些都是线程的特点。不过用进程来模拟内核线程会导致很多语义问题比如 每个线程拥有不同的 PID 因此不符合 POSIX 规范。Linux 信号处理本来是基于进程的但现在一个进程内部的所有线程都能而且必须处理信号。用户 ID 、组 ID 对一个进程中的不同线程来说可能是不一样的。程序产生的核心转储文件不会包含所有线程的信息而只包含产生该核心转储文件的线程的信息。由于每个线程都是一个进程因此系统允许的最大进程数也就是最大线程数。 LinuxThreads 线程库一个有名的特性是所谓的管理线程。它是进程中专门用于管理其他工作线程的线程。其作用包括 系统发送给进程的终止信号先由管理线程接收管理线程再给其他工作线程发送同样的信号以终止它们。当终止工作线程或者工作线程主动退出时管理线程必须等待它们结束以避免僵尸进程。如果主线程先于其他工作线程退出则管理线程将阻塞它直到所有其他工作线程都结束之后才唤醒它。回收每个线程堆栈使用的内存。 管理线程的引入增加了额外的系统开销。并且由于它只能运行在一个 CPU 上所以 LinuxThreads 线程库也不能充分利用多处理器系统的优势。 要解决 LinuxThreads 线程库的一系列问题不仅需要改进线程库最主要的是需要内核提供更完善的线程支持。因此Linux 内核从 2.6 版本开始提供了真正的内核线程。新的 NPTL 线程库也应运而生。相比 LinuxThreads NPTL 的主要优势在于 内核线程不再是一个进程因此避免了很多用进程模拟内核线程导致的语义问题。摒弃了管理线程终止线程、回收线程堆栈等工作都可以由内核来完成。由于不存在管理线程所以一个进程的线程可以运行在不同的 CPU 上从而充分利用了多处理器系统的优势。线程的同步由内核来完成。隶属于不同进程的线程之间也能共享互斥锁因此可实现跨进程的线程同步。 二、创建线程和结束线程 下面我们讨论创建和结束线程的基础 API 。Linux 系统上它们都定义在pthread.h头文件中 1. pthread_create 创建线程 #includepthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void*(*start_routine)(void*), void *arg);thread 参数是新线程的标识符后续 pthread_* 函数通过它来引用新线程。其类型 pthread_t 的定义如下 #includebits/pthreadtypes.h typedef unsigned long int pthread_t;可见pthread_t 是一个整型类型。实际上Linux 上几乎所有的资源标识符都是一个整型数比如 socket 、各种 System V IPC 标识符等。 attr 参数用于设置新线程的属性。给它传递 NULL 表示使用默认线程属性。线程拥有众多属性。start_routine 和 arg 参数分别指定新线程将运行的函数及其参数。 pthread_create 成功时返回 0 失败时返回错误码。一个用户可以打开的线程数量不能超过 RLIMIT_NPROC 软资源限制。此外系统上所有用户能创建的线程总数也不得超过 /proc/sys/kernel/threads-max 内核参数所定义的值。 2. pthread_exit 线程一旦被创建好内核就可以调度内核线程来执行 start_routine 函数指针所指向的函数了。线程函数在结束时最好调用如下函数以确保安全、干净地退出 #includepthread.h void pthread_exit(void*retval);pthread_exit 函数通过 retval 参数向线程的回收者传递其退出信息。它执行完之后不会返回到调用者而且永远不会失败。 3. pthread_join 一个进程中的所有线程都可以调用 pthread_join 函数来回收其他线程前提是目标线程是可回收的即等待其他线程结束这类似于回收进程的 wait 和 waitpid 系统调用。pthread_join 的定义如下 #includepthread.h int pthread_join(pthread_t thread,void**retval);thread 参数是目标线程的标识符retval 参数则是目标线程返回的退出信息。该函数会一直阻塞直到被回收的线程结束为止。该函数成功时返回 0 失败则返回错误码 错误码描述EDEADLK可能引起死锁比如两个线程互相针对对方调用 pthread_ioin 或者线程对自身调用 pthread_joinEINVAL目标线程是不可回收的或者已经有其他线程在回收该目标线程ESRCH目标线程不存在 4. pthread_cancel 有时候希望一场终止一个线程即取消线程 #include pthread.h int pthread_cancel(pthread_t thread);thread 参数是目标线程的标识符。该函数成功时返回 0 失败则返回错误码。不过接收到取消请求的目标线程可以决定是否允许被取消以及如何取消这分别由如下两个函数完成 #includepthread.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 。根据 POSIX 标准其他可能阻塞的系统调用比如 read 、wait也可以成为取消点。不过为了安全起见我们最好在可能会被取消的代码中调用 pthread_testcancel 函数以设置取消点。 pthread_setcancelstate 和 pthread_setcanceltype 成功时返回 0 失败则返回错误码。 三、线程属性 pthread_attr_t 结构体定义了一套完整的线程属性如下所示 #includebits/pthreadtypes.h #define __SIZEOF_PTHREAD_ATTR_T 36 typedef union { char__size[__SIZEOF_PTHREAD_ATTR_T]; long int__align; }pthread_attr_t;可见各种线程属性全部包含在一个字符数组中。线程库定义了一系列函数来操作 pthread_attr_t 类型的变量以方便我们获取和设置线程属性。这些函数包括 #includepthread.h /*初始化线程属性对象*/ int pthread_attr_init(pthread_attr_t*attr); /*销毁线程属性对象。被销毁的线程属性对象只有再次初始化之后才能继续使用*/ int pthread_attr_destroy(pthread_attr_t*attr); /*下面这些函数用于获取和设置线程属性对象的某个属性*/ int pthread_attr_getdetachstate(const pthread_attr_t*attr,int*detachstate); int pthread_attr_setdetachstate(pthread_attr_t*attr,int detachstate); int pthread_attr_getstackaddr(const pthread_attr_t*attr,void**stackaddr); int pthread_attr_setstackaddr(pthread_attr_t*attr,void*stackaddr); int pthread_attr_getstacksize(const pthread_attr_t*attr,size_t*stacksize); int pthread_attr_setstacksize(pthread_attr_t*attr,size_t stacksize); int pthread_attr_getstack(const pthread_attr_t*attr,void**stackaddr,size_t*stacksize); int pthread_attr_setstack(pthread_attr_t*attr,void*stackaddr,size_t stacksize); int pthread_attr_getguardsize(const pthread_attr_t*__attr,size_t*guardsize); int pthread_attr_setguardsize(pthread_attr_t*attr,size_t guardsize); int pthread_attr_getschedparam(const pthread_attr_t*attr,struct sched_param*param); int pthread_attr_setschedparam(pthread_attr_t*attr,const struct sched_param*param); int pthread_attr_getschedpolicy(const pthread_attr_t*attr,int*policy); int pthread_attr_setschedpolicy(pthread_attr_t*attr,int policy); int pthread_attr_getinheritsched(const pthread_attr_t*attr,int*inherit); int pthread_attr_setinheritsched(pthread_attr_t*attr,int inherit); int pthread_attr_getscope(const pthread_attr_t*attr,int*scope); int pthread_attr_setscope(pthread_attr_t*attr,int scope);detachstate线程的脱离状态。它有 PTHREAD_CREATE_JOINABLE 和 PTHREAD_CREATE_DETACH 两个可选值。前者指定线程是可以被回收的后者使调用线程脱离与进程中其他线程的同步。脱离了与其他线程同步的线程称为“脱离线程”。 脱离线程在退出时将自行释放其占用的系统资源。线程创建时该属性的默认值是 PTHREAD_CREATE_JOINABLE 。此外我们也可以使用 pthread_detach 函数直接将线程设置为脱离线程。stackaddr 和 stacksize线程堆栈的起始地址和大小。一般来说我们不需要自己来管理线程堆栈因为 Linux 默认为每个线程分配了足够的堆栈空间一般是 8 MB。我们可以使用ulimt-s命令来查看或修改这个默认值。guardsize保护区域大小。如果 guardsize 大于 0 则系统创建线程的时候会在其堆栈的尾部额外分配 guardsize 字节的空间作为保护堆栈不被错误地覆盖的区域。如果 guardsize 等于 0 则系统不为新创建的线程设置堆栈保护区。如果使用者通过 pthread_attr_setstackaddr 或 pthread_attr_setstack 函数手动设置线程的堆栈则 guardsize 属性将被忽略。 schedparam线程调度参数。其类型是 sched_param 结构体。该结构体目前还只有一个整型类型的成员 sched_priority 该成员表示线程的运行优先级。schedpolicy线程调度策略。该属性有 SCHED_FIFO 、SCHED_RR 和 SCHED_OTHER 三个可选值其中 SCHED_OTHER 是默认值。SCHED_RR 表示采用轮转算法round-robin调度SCHED_FIFO 表示使用先进先出的方法调度这两种调度方法都具备实时调度功能但只能用于以超级用户身份运行的进程。inheritsched是否继承调用线程的调度属性。该属性有 PTHREAD_INHERIT_SCHED 和 PTHREAD_EXPLICIT_SCHED 两个可选值。前者表示新线程沿用其创建者的线程调度参数这种情况下再设置新线程的调度参数属性将没有任何效果。后者表示调用者要明确地指定新线程的调度参数。scope线程间竞争 CPU 的范围即线程优先级的有效范围。POSIX 标准定义了该属性的 PTHREAD_SCOPE_SYSTEM 和 PTHREAD_SCOPE_PROCESS 两个可选值前者表示目标线程与系统中所有线程一起竞争 CPU 的使用后者表示目标线程仅与其他隶属于同一进程的线程竞争 CPU 的使用。目前 Linux 只支持 PTHREAD_SCOPE_SYSTEM 这一种取值。 四、POSIX 信号量 和多进程程序一样多线程程序也必须考虑同步问题。pthread_join 可以看作一种简单的线程同步方式不过很显然它无法 高效地实现复杂的同步需求比如控制对共享资源的独占式访问又抑或是在某个条件满足之后唤醒一个线程。接下来我们讨论 3 种专门用于线程同步的机制POSIX 信号量、互斥量和条件变量。 在 Linux上 信号量 API 有两组。一组是第 13 章讨论过的 System V IPC 信号量另外一组是我们现在要讨论的 POSIX 信号量。这两组接口很相似但不保证能互换。由于这两种信号量的语义完全相同因此不再赘述信号量的原理。 POSIX 信号量函数的名字都以 sem_ 开头并不像大多数线程函数那样以 pthread_ 开头。常用的 POSIX 信号量函数是下面 5 个 #includesemaphore.h int sem_init(sem_t*sem,int pshared,unsigned int value); int sem_destroy(sem_t*sem); int sem_wait(sem_t*sem); int sem_trywait(sem_t*sem); int sem_post(sem_t*sem);这些函数的第一个参数 sem 指向被操作的信号量 sem_init用于初始化一个未命名的信号量POSIX 信号量 API 支持命名信号量。pshared 参数指定信号量的类型。如果其值为 0 就表示这个信号量是当前进程的局部信号量否则该信号量就可以在多个进程之间共享。value 参数指定信号量的初始值。此外初始化一个已经被初始化的信号量将导致不可预期的结果。sem_destroy用于销毁信号量以释放其占用的内核资源。如果销毁一个正被其他线程等待的信号量则将导致不可预期的结果。sem_wait以原子操作的方式将信号量的值减 1 。如果信号量的值为 0 则 sem_wait 将被阻塞直到这个信号量具有非 0 值。sem_trywait 与 sem_wait 函数相似不过它始终立即返回而不论被操作的信号量是否具有非 0 值相当于 sem_wait 的非阻塞版本。当信号量的值非 0 时sem_trywait 对信号量执行减 1 操作。当信号量的值为 0 时它将返回 -1 并设置 errno 为 EAGAIN 。sem_post以原子操作的方式将信号量的值加 1 。当信号量的值大于 0 时其他正在调用 sem_wait 等待信号量的线程将被唤醒。 上面这些函数成功时返回 0 失败则返回 -1 并设置 errno 。 五、互斥锁 互斥锁也称互斥量可以用于保护关键代码段以确保其独占式的访问这有点像一个二进制信号量。当进入关键代码段时我们需要获得互斥锁并将其加锁这等价于二进制信号量的 P 操作当离开关键代码段时我们需要对互斥锁解锁以唤醒其他等待该互斥锁的线程这等价于二进制信号量的 V 操作。 1. 互斥锁基础 API POSIX 互斥锁的相关函数主要有如下 5 个 #includepthread.h int pthread_mutex_init(pthread_mutex_t*mutex,const pthread_mutexattr_t*mutexattr); int pthread_mutex_destroy(pthread_mutex_t*mutex); int pthread_mutex_lock(pthread_mutex_t*mutex); int pthread_mutex_trylock(pthread_mutex_t*mutex); int pthread_mutex_unlock(pthread_mutex_t*mutex);这些函数的第一个参数 mutex 指向要操作的目标互斥锁互斥锁的类型是 pthread_mutex_t 结构体。pthread_mutex_init 函数用于初始化互斥锁。mutexattr 参数指定互斥锁的属性。如果将它设置为 NULL 则表示使用默认属性。我们将在下一小节讨论互斥锁的属性。除了这个函数外我们还可以使用如下方式来初始化一个互斥锁 pthread_mutex_t mutexPTHREAD_MUTEX_INITIALIZER;宏 PTHREAD_MUTEX_INITIALIZER 实际上只是把互斥锁的各个字段都初始化为 0 。 pthread_mutex_destroy 函数用于销毁互斥锁以释放其占用的内核资源。销毁一个已经加锁的互斥锁将导致不可预期的后果。 pthread_mutex_lock 函数以原子操作的方式给一个互斥锁加锁。如果目标互斥锁已经被锁上则 pthread_mutex_lock 调用将阻塞直到该互斥锁的占有者将其解锁。 pthread_mutex_trylock 与 pthread_mutex_lock 函数类似不过它始终立即返回而不论被操作的互斥锁是否已经被加锁相当于 pthread_mutex_lock 的非阻塞版本。当目标互斥锁未被加锁时pthread_mutex_trylock 对互斥锁执行加锁操作。当互斥锁已经被加锁时pthread_mutex_trylock 将返回错误码 EBUSY 。需要注意的是这里讨论的 pthread_mutex_lock 和 pthread_mutex_trylock 的行为是针对普通锁而言的。后面我们将看到对于其他类型的锁而言这两个加锁函数会有不同的行为。 pthread_mutex_unlock 函数以原子操作的方式给一个互斥锁解锁。如果此时有其他线程正在等待这个互斥锁则这些线程中的某一个将获得它。 上面这些函数成功时返回 0 失败则返回错误码。 2. 互斥锁属性 pthread_mutexattr_t 结构体定义了一套完整的互斥锁属性。线程库提供了一系列函数来操作 pthread_mutexattr_t 类型的变量以方便我们获取和设置互斥锁属性。这里我们列出其中一些主要的函数 #includepthread.h /*初始化互斥锁属性对象*/ int pthread_mutexattr_init(pthread_mutexattr_t*attr); /*销毁互斥锁属性对象*/ int pthread_mutexattr_destroy(pthread_mutexattr_t*attr); /*获取和设置互斥锁的pshared属性*/ int pthread_mutexattr_getpshared(const pthread_mutexattr_t*attr,int*pshared); int pthread_mutexattr_setpshared(pthread_mutexattr_t*attr,int pshared); /*获取和设置互斥锁的type属性*/ int pthread_mutexattr_gettype(const pthread_mutexattr_t*attr,int*type); int pthread_mutexattr_settype(pthread_mutexattr_t*attr,int type);本文只讨论互斥锁的两种常用属性pshared 和 type。互斥锁属性 pshared 指定是否允许跨进程共享互斥锁其可选值有两个 PTHREAD_PROCESS_SHARED互斥锁可以被跨进程共享PTHREAD_PROCESS_PRIVATE互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。 互斥锁属性 type 指定互斥锁的类型。Linux 支持如下 4 种类型的互斥锁 PTHREAD_MUTEX_NORMAL普通锁。这是互斥锁默认的类型。当一个线程对一个普通锁加锁以后其余请求该锁的线程将形成一个等待队列并在该锁解锁后按优先级获得它。这种锁类型保证了资源分配的公平性。但这种锁也很容易引发问题一个线程如果对一个已经加锁的普通锁再次加锁将引发死锁对一个已经被其他线程加锁的普通锁解锁或者对一个已经解锁的普通锁再次解锁将导致不可预期的后果。PTHREAD_MUTEX_ERRORCHECK检错锁。一个线程如果对一个已经加锁的检错锁再次加锁则加锁操作返回 EDEADLK 。对 一个已经被其他线程加锁的检错锁解锁或者对一个已经解锁的检错锁再次解锁则解锁操作返回 EPERM 。PTHREAD_MUTEX_RECURSIVE嵌套锁。这种锁允许一个线程在释放锁之前多次对它加锁而不发生死锁。不过其他线程如果要获得这个锁则当前锁的拥有者必须执行相应次数的解锁操作。对一个已经被其他线程加锁的嵌套锁解锁或者对一个已经解锁的嵌套锁再次解锁则解锁操作返回 EPERM 。PTHREAD_MUTEX_DEFAULT默认锁。一个线程如果对一个已经加锁的默认锁再次加锁或者对一个已经被其他线程加锁的默认锁解锁或者对一个已经解锁的默认锁再次解锁将导致不可预期的后果。这种锁在实现的时候可能被映射为上面三种锁之一。 3. 死锁举例 使用互斥锁的一个噩耗是死锁。死锁使得一个或多个线程被挂起而无法继续执行而且这种情况还不容易被发现。前文提到在一个线程中对一个已经加锁的普通锁再次加锁将导致死锁。这种情况可能出现在设计得不够仔细的递归函数中。另外如果两个线程按照不同的顺序来申请两个互斥锁也容易产生死锁 #include pthread.h #include unistd.h #include stdio.h int a 0; int b 0; pthread_mutex_t mutex_a; pthread_mutex_t mutex_b; void *another(void *arg) {pthread_mutex_lock(mutex_b);printf(in child thread,got mutex b,waiting for mutex a\n);sleep(5);b;pthread_mutex_lock(mutex_a);b a;pthread_mutex_unlock(mutex_a);pthread_mutex_unlock(mutex_b);pthread_exit(NULL); } int main() {pthread_t id;pthread_mutex_init(mutex_a, NULL);pthread_mutex_init(mutex_b, NULL);pthread_create(id, NULL, another, NULL);pthread_mutex_lock(mutex_a);printf(in parent thread,got mutex a,waiting for mutex b\n);sleep(5);a;pthread_mutex_lock(mutex_b);a b;pthread_mutex_unlock(mutex_b);pthread_mutex_unlock(mutex_a);pthread_join(id, NULL);pthread_mutex_destroy(mutex_a);pthread_mutex_destroy(mutex_b);return 0; }主线程试图先占有互斥锁 mutex_a 然后操作被该锁保护的变量 a 但操作完毕之后主线程并没有立即释放互斥锁 mutex_a 而是又申请互斥锁 mutex_b 并在两个互斥锁的保护下操作变量 a 和 b 最后才一起释放这两个互斥锁与此同时子线程则按照相反的顺序来申请互斥锁 mutex_a 和 mutex_b 并在两个锁的保护下操作变量 a 和 b 。我们用 sleep 函数来模拟连续两次调用 pthread_mutex_lock 之间的时间差以确保代码中的两个线程各自先占有一个互斥锁主线程占有 mutex_a 子线程占有 mutex_b 然后等待另外一个互斥锁主线程等待 mutex_b 子线程等待 mutex_a。这样两个线程就僵持住了谁都不能继续往下执行从而形成死锁。如果代码中不加入 sleep 函数则这段代码或许总能成功地运行从而为程序留下了一个潜在的 BUG 。 六、条件变量 如果说互斥锁是用于同步线程对共享数据的访问的话那么条件变量则是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通知机制当某个共享数据达到某个值的时候唤醒等待这个共享数据的线程。条件变量的相关函数主要有如下 5 个 #includepthread.h int pthread_cond_init(pthread_cond_t*cond,const pthread_condattr_t*cond_attr); int pthread_cond_destroy(pthread_cond_t*cond); int pthread_cond_broadcast(pthread_cond_t*cond); int pthread_cond_signal(pthread_cond_t*cond); int pthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);这些函数的第一个参数 cond 指向要操作的目标条件变量条件变量的类型是 pthread_cond_t 结构体。pthread_cond_init 函数用于初始化条件变量。cond_attr 参数指定条 件变量的属性。如果将它设置为 NULL 则表示使用默认属性。条件变量的属性不多而且和互斥锁的属性类型相似所以不再赘述。除了 pthread_cond_init 函数外还可以使用如下方式来初始化一个条件变量 pthread_cond_t condPTHREAD_COND_INITIALIZER;宏 PTHREAD_COND_INITIALIZER 实际上只是把条件变量的各个字段都初始化为0。 pthread_cond_destroy 函数用于销毁条件变量以释放其占用的内核资源。销毁一个正在被等待的条件变量将失败并返回 EBUSY 。 pthread_cond_broadcast 函数以广播的方式唤醒所有等待目标条件变量的线程。pthread_cond_signal 函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒则取决于线程的优先级和调度策略。有时候我们可能想唤醒一个指定的线程但 pthread 没有对该需求提供解决方法。不过我们可以间接地实现该需求定义一个能够唯一表示目标线程的全局变量在唤醒等待条件变量的线程前先设置该变量为目标线程然后采用广播方式唤醒所有等待条件变量的线程这些线程被唤醒后都检查该变量以判断被唤醒的是否是自己如果是就开始执行后续代码如果不是则返回继续等待。 pthread_cond_wait 函数用于等待目标条件变量。mutex 参数是用于保护条件变量的互斥锁以确保 pthread_cond_wait 操作的原子性。在调用 pthread_cond_wait 前必须确保互斥锁 mutex 已经加锁否则将导致不可预期的结果。pthread_cond_wait 函数执行时首先把调用线程放入条件变量的等待队列中然后将互斥锁 mutex 解锁。可见从pthread_cond_wait 开始执行到其调用线程被放入条件变量的等待队列之间的这段时间内pthread_cond_signal 和 pthread_cond_broadcast 等函数不会修改条件变量。换言之pthread_cond_wait 函数不会错过目标条件变量的任何变化。当 pthread_cond_wait 函数成功返回时互斥锁 mutex 将再次被锁上。 上面这些函数成功时返回 0 失败则返回错误码。 七、线程同步机制包装类 为了充分复用代码同时由于后文的需要我们将前面讨论的 3 种线程同步机制分别封装成 3 个类实现在 locker.h 文件中 #ifndef LOCKER_H #define LOCKER_H #include exception #include pthread.h #include semaphore.h /*封装信号量的类*/ class sem { public: /*创建并初始化信号量*/sem(){if (sem_init(m_sem, 0, 0) ! 0){ /*构造函数没有返回值可以通过抛出异常来报告错误*/throw std::exception();}} /*销毁信号量*/~sem() { sem_destroy(m_sem); } /*等待信号量*/bool wait() { return sem_wait(m_sem) 0; } /*增加信号量*/bool post() { return sem_post(m_sem) 0; }private:sem_t m_sem; }; /*封装互斥锁的类*/ class locker { public: /*创建并初始化互斥锁*/locker(){if (pthread_mutex_init(m_mutex, NULL) ! 0){throw std::exception();}} /*销毁互斥锁*/~locker() { pthread_mutex_destroy(m_mutex); } /*获取互斥锁*/bool lock() { return pthread_mutex_lock(m_mutex) 0; } /*释放互斥锁*/bool unlock() { return pthread_mutex_unlock(m_mutex) 0; }private:pthread_mutex_t m_mutex; }; /*封装条件变量的类*/ class cond { public: /*创建并初始化条件变量*/cond(){if (pthread_mutex_init(m_mutex, NULL) ! 0){throw std::exception();}if (pthread_cond_init(m_cond, NULL) ! 0){ /*构造函数中一旦出现问题就应该立即释放已经成功分配了的资源*/pthread_mutex_destroy(m_mutex);throw std::exception();}} /*销毁条件变量*/~cond(){pthread_mutex_destroy(m_mutex);pthread_cond_destroy(m_cond);} /*等待条件变量*/bool wait(){int ret 0;pthread_mutex_lock(m_mutex);ret pthread_cond_wait(m_cond, m_mutex);pthread_mutex_unlock(m_mutex);return ret 0;} /*唤醒等待条件变量的线程*/bool signal() { return pthread_cond_signal(m_cond) 0; }private:pthread_mutex_t m_mutex;pthread_cond_t m_cond; }; #endif八、多线程环境 1. 可重入函数 如果一个函数能被多个线程同时调用且不发生竞态条件则我们称它是线程安全的thread safe或者说它是可重入函数。Linux 库函数只有一小部分是不可重入的比如 inet_ntoa 函数以及 getservbyname 和 getservbyport 函数。这些库函数之所以不可重入主要是因为其内部使用了静态变量。不过 Linux 对很多不可重入的库函数提供了对应的可重入版本这些可重入版本的函数名是在原函数名尾部加上 _r 。比如函数 localtime 对应的可重入函数是 localtime_r 。在多线程程序中调用库函数一定要使用其可重入版本否则可能导致预想不到的结果。 2. 线程和进程 思考这样一个问题如果一个多线程程序的某个线程调用了 fork 函数那么新创建的子进程是否将自动创建和父进程相同数量的线程呢答案是“否”正如我们期望的那样。子进程只拥有一个执行线程它是调用 fork 的那个线程的完整复制。并且子进程将自动继承父进程中互斥锁条件变量与之类似的状态。也就是说父进程中已经被加锁的互斥锁在子进程中也是被锁住的。这就引起了一个问题子进程可能不清楚从父进程继承而来的互斥锁的具体状态是加锁状态还是解锁状态。这个互斥锁可能被加锁了但并不是由调用 fork 函数的那个线程锁住的而是由其他线程锁住的。如果是这种情况则子进程若再次对该互斥锁执行加锁操作就会导致死锁 #include pthread.h #include unistd.h #include stdio.h #include stdlib.h #include wait.h pthread_mutex_t mutex; /*子线程运行的函数。它首先获得互斥锁mutex然后暂停5 s再释放该互斥锁*/ void *another(void *arg) {printf(in child thread,lock the mutex\n);pthread_mutex_lock(mutex);sleep(5);pthread_mutex_unlock(mutex); } int main() {pthread_mutex_init(mutex, NULL);pthread_t id;pthread_create(id, NULL, another, NULL); /*父进程中的主线程暂停1 s以确保在执行fork操作之前子线程已经开始运行并获 得了互斥变量mutex*/sleep(1);int pid fork();if (pid 0){pthread_join(id, NULL);pthread_mutex_destroy(mutex);return 1;}else if (pid 0){printf(I am in the child,want to get the lock\n); /*子进程从父进程继承了互斥锁mutex的状态该互斥锁处于锁住的状态这是由父进 程中的子线程执行pthread_mutex_lock引起的因此下面这句加锁操作会一直阻塞 尽管从逻辑上来说它是不应该阻塞的*/pthread_mutex_lock(mutex);printf(I can not run to here,oop...\n);pthread_mutex_unlock(mutex);exit(0);}else{wait(NULL);}pthread_join(id, NULL);pthread_mutex_destroy(mutex);return 0; }不过pthread 提供了一个专门的函数 pthread_atfork 以确保 fork 调用后父进程和子进程都拥有一个清楚的锁状态。该函数的定义如下 #includepthread.h int pthread_atfork(void(*prepare)(void),void(*parent) (void),void(*child)(void));该函数将建立 3 个 fork 句柄来帮助我们清理互斥锁的状态。prepare 句柄将在 fork 调用创建出子进程之前被执行。它可以用来锁住所有父进程中的互斥锁。parent 句柄则是 fork 调用创建出子进程之后而 fork 返回之前在父进程中被执行。它的作用是释放所有在 prepare 句柄中被锁住的互斥锁。child 句柄是 fork 返回之前在子进程中被执行。和 parent 句柄一样child 句柄也是用于释放所有在 prepare 句柄中被锁住的互斥锁。该函数成功时返回 0 失败则返回错误码。 因此如果要让上述代码正常工作就应该在其中的 fork 调用前加入它 void prepare() {pthread_mutex_lock(mutex); } void infork() { pthread_mutex_unlock(mutex); } pthread_atfork(prepare, infork,infork);3. 线程和信号 每个线程都可以独立地设置信号掩码。在多线程环境下应该使用如下所示的 pthread 版本的 sigprocmask 函数来设置线程信号掩码 #includepthread.h #includesignal.h int pthread_sigmask(int how,const sigset_t*newmask,sigset_t*oldmask);该函数的参数的含义与 sigprocmask 的参数完全相同因此不再赘述。pthread_sigmask 成功时返回 0 失败则返回错误码。 由于进程中的所有线程共享该进程的信号所以线程库将根据线 程掩码决定把信号发送给哪个具体的线程。因此如果我们在每个子线程中都单独设置信号掩码就很容易导致逻辑错误。此外所有线程共享信号处理函数。也就是说当我们在一个线程中设置了某个信号的信号处理函数后它将覆盖其他线程为同一个信号设置的信号处理函数。这两点都说明我们应该定义一个专门的线程来处理所有的信号。这可以通过如下两个步骤来实现 在主线程创建出其他子线程之前就调用 pthread_sigmask 来设置好信号掩码所有新创建的子线程都将自动继承这个信号掩码。这样做之后实际上所有线程都不会响应被屏蔽的信号了。在某个线程中调用如下函数来等待信号并处理之#includesignal.h int sigwait(const sigset_t*set,int*sig);set 参数指定需要等待的信号的集合。我们可以简单地将其指定为在第 1 步中创建的信号掩码表示在该线程中等待所有被屏蔽的信号。参数 sig 指向的整数用于存储该函数返回的信号值。sigwait 成功时返回 0 失败则返回错误码。一旦 sigwait 正确返回我们就可以对接收到的信号做处理了。很显然如果我们使用了 sigwait 就不应该再为信号设置信号处理函数了。这是因为当程序接收到信号时二者中只能有一个起作用。 以下代码取自 pthread_sigmask 函数的 man 手册。展示了如何通过上述两个步骤实现在一个线程中统一处理所有信号 #include pthread.h #include stdio.h #include stdlib.h #include unistd.h #include signal.h #include errno.h #define handle_error_en(en, msg) \do \{ \errno en; \perror(msg); \exit(EXIT_FAILURE); \} while (0) static void *sig_thread(void *arg) {sigset_t *set (sigset_t *)arg;int s, sig;for (;;){ /*第二个步骤调用sigwait等待信号*/s sigwait(set, sig);if (s ! 0)handle_error_en(s, sigwait);printf(Signal handling thread got signal%d\n, sig);} } int main(int argc, char *argv[]) {pthread_t thread;sigset_t set;int s; /*第一个步骤在主线程中设置信号掩码*/sigemptyset(set);sigaddset(set, SIGQUIT);sigaddset(set, SIGUSR1);s pthread_sigmask(SIG_BLOCK, set, NULL);if (s ! 0)handle_error_en(s, pthread_sigmask);s pthread_create(thread, NULL, sig_thread, (void *)set);if (s ! 0)handle_error_en(s, pthread_create);pause(); }最后pthread 还提供了下面的方法使得我们可以明确地将一个信号发送给指定的线程 #includesignal.h int pthread_kill(pthread_t thread,int sig);其中thread 参数指定目标线程sig 参数指定待发送的信号。如果 sig 为 0 则 pthread_kill 不发送信号但它任然会执行错误检查。我们可以利用这种方式来检测目标线程是否存在。pthread_kill 成功时返回 0 失败则返回错误码。
http://www.dnsts.com.cn/news/151243.html

相关文章:

  • 手机网站建立教程wordpress使用html界面
  • 中小企业为什么要建网站在线答题网站怎么做
  • 做网站的关键词是指中文网站建设小组
  • 遵义外国网站制作海南省建设考试网站
  • 11号在线 网站开发如何建立自己的网站步骤
  • 私人网站免费观看网站建设栏目流程
  • 百度网站排名搜行者seo重庆微信企业网站
  • 网站建设写网站前台的实现
  • 网站建设有哪些费用嘉兴高端网站
  • 学做网站需要学哪些软件商城网站建设需求
  • 工程行业做的好的网站有哪些内容卡片式网站模板下载
  • 网站建设项目软件开发招标文件建网站金坛哪家强?
  • 诚信网站建设的意义wordpress说说加分类
  • 如何修改网站模版怎么做网页才能
  • 有哪些网站可以学做糕点的建设婚纱摄影网站的重要性
  • 昆明做网站方案网络公关公司排名
  • 旺道seo网站优化大师网络营销推广方式案例
  • 公司建网站空间网站建站流程图
  • 关于网站建设的调研报告成都网站设计费用
  • 建设网站公司 优帮云福永外贸网站建设公司
  • 先做网站后台还是前台东莞市企业信息查询网
  • 佛山外贸网站建设平台个人备案的网站能做什么
  • 先做网站还是先解析百度app下载官方免费下载最新版
  • 这么建立com的网站wordpress添加下载
  • 网站建设都需要买什么东西安徽p2p网站建设
  • 做非法网站要多少钱wordpress黑镜主题2.0下载
  • 网站语音转写怎么做物流公司招聘
  • 网站开发需求表wordpress企业咨询模板
  • 哪个网站可以帮忙做简历seo企业网站模板
  • 社区网站建设方案网站设计与制