Unix环境高级编程 读书笔记 第十一章 线程

线程概念

典型的UNIX进程可以看成只有一个控制进程,即一个进程在某个时刻只能处理一件事情。如果引入线程的概念,则可以将进程设计成在某个时刻能够处理不止一件事情,每个线程可以处理各自独立的任务。

采用线程实现程序的优点包括:

  1. 通过为每种事件类型分配单独的线程进行处理,可以简化处理异步事件的代码;
  2. 多个进程之间必须通过操作系统提供的机制进行内存与文件描述符的共享,但是同一进程下的多个线程之间自动实现内存与文件描述符的共享;
  3. 针对问题进行分解,分配各自的线程进行处理,可以提高程序的吞吐量;
  4. 类似交互类型的程序可以通过多线程来改善程序的响应时间。

每个线程都包含有标识线程的线程ID、线程对应的寄存器值,栈,调度优先级,信号屏蔽字,errno变量,线程私有数据等。
进程的所有信息对于该进程的所有线程都是共享的,包括可执行程序的代码,程序的全局内存与堆内存,栈,文件描述符等。

线程标识

每个线程都有其线程ID,线程ID只有在其所属的进程上下文中才有意义。
线程ID是用数据类型pthread_t来表示的。对于不同的实现,数据类型pthread_t是不同的,可能是整数类型,也可能是指向数据结构的指针。因此对于比较两个线程ID,需要通过一个函数来实现:

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
/*若相等,返回非0,若不相等,返回0*/

线程可以通过调用函数pthread_self来获取自身的线程ID:

#include <pthread.h>
pthread_t pthread_self(void);
/*返回调用线程的线程ID*/

创建线程

新增的线程可以通过调用函数pthread_create来创建:

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
                   const pthread_attr_t *restrict attr,
                   void *(*start_rtn)(void *),
                   void *restrict arg);
/*若成功,返回0,否则,返回错误编号*/

当函数pthread_create成功返回时,新创建的线程的线程ID将被设置在tidp指向的内存单元。
参数attr用于定制各种不同的线程属性,设置为NULL则创建一个具有默认属性的线程。
新创建的线程将从start_rtn函数的地址开始执行,该函数只有一个无类型指针参数arg。若需要向start_rtn函数传递多个参数,则需要将这些参数放在一个结构中,将结构的地址作为arg参数传入。
线程创建时不能保证哪个线程先运行,可能是新创建的线程,也可能是调用线程。
新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。

线程终止

如果进程中的任意线程调用了exit系列函数,则整个进程将会终止。而单个线程可通过3种方式,在不终止整个进程的情况下退出:

  1. 线程可简单地从启动例程中返回,返回值就是线程的退出码;
  2. 线程可以被同一个进程中的其他线程取消;
  3. 线程调用pthread_exit。
    函数pthread_exit的原型为:
#include <pthread.h>
void pthread_exit(void *rval_ptr);

如果线程简单地从它的启动例程返回,则rval_ptr将包含返回码。如果线程是被取消,则rval_ptr指向的内存单元被设置为PTHREAD_CANCELED。

线程可以通过调用函数pthread_cancel来请求取消同一个进程中的所有其他线程:

#include <pthread.h>
int pthread_cancel(pthread_t tid);
/*若成功,返回0,若出错,返回错误编码*/

默认情况下,函数pthread_cancel会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数。但是函数pthread_cancel并不等待线程终止,仅仅是提出请求,被请求的线程可以选择忽略这个取消请求。

线程在退出时候,可以注册退出时需要调用的函数,即线程清理处理程序,这个与进程退出时使用的atexit函数类似。一个线程可以建立多个清理处理程序,执行顺序与注册顺序相反。

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);

线程同步与互斥量

当多个线程,在没有数据保护的情况下,对同一个内存区进行读写操作,会产生竞争情况,导致内存区数据不一致。需要通过某种手段实现线程同步,避免竞争。

可以使用pthread的互斥接口来实现保护数据,避免竞争。互斥量(mutex)本质上是锁。
在对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程都会被阻塞,直到当前持有锁的线程释放该互斥锁。如果释放互斥量时,有多个线程阻塞等待,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程只能阻塞继续等待。

互斥量使用pthread_mutex_t数据类型表示,在使用互斥量之前,必须首先对其进行初始化。
对于静态分配的互斥量,初始化将其设定为常量PTHREAD_MUTEX_INITIALIZER,对于静态分配或者动态分配的互斥量,初始化时调用函数pthread_mutex_init进行初始化。
对于动态分配的互斥量,释放时调用函数pthread_mutex_destroy来释放内存。

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                       const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/*若成功,返回0,若出错,返回错误编码*/

对于函数pthread_mutex_init,要是需要使用默认的属性初始化互斥量,则只需将参数attr设置为NULL。

线程对互斥量进行加锁,解锁使用的函数为:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/*若成功,返回0,若出错,返回错误编码*/

线程对互斥量进行加锁,调用函数pthread_mutex_lock,若互斥量已经被上锁,则调用线程将被阻塞直到互斥量解锁。
线程对互斥量进行解锁,调用函数pthread_mutex_unlock。
线程若加锁失败时不希望被阻塞,则调用函数pthread_mutex_trylock尝试对互斥量进行加锁操作。若加锁失败,不会阻塞,直接返回EBUSY,若加锁成功,则返回0。

避免死锁

出现死锁的情况包括:

  1. 线程试图对同一个互斥量加锁两次,在第二次加锁时,由于互斥量已经被自身加锁,永远也等不到解锁,产生死锁;
  2. 两个线程在互相请求另一个线程所拥有的互斥量资源,互相在等待对方释放,产生死锁。

避免死锁的方式:

  1. 通过控制互斥量加锁的顺序来避免死锁的发生。即所有的线程申请多个互斥量的顺序是相同的;
  2. 线程申请多个互斥量时失败,则释放所有已经占有的互斥量,过一段时间再试。

函数pthread_mutex_timedlock

函数pthread_mutex_timedlock与函数pthread_mutex_lock基本等价,但是函数pthread_mutex_timedlock互斥量原语允许绑定线程阻塞时间,当线程阻塞的时间达到超时的时间值时,返回错误码ETIMEDOUT。使用该函数可以避免线程永久阻塞。

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
                            const struct timespec *restrict tsptr);
/*若成功,返回0,若出错,返回错误编码*/

读写锁

读写锁的概念与互斥量类似,但是读写锁具有更高的并行性。
读写锁可以有3种状态:

  1. 读模式下加锁状态;
  2. 写模式下加锁状态;
  3. 不加锁状态。

一次只能有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占用读模式的读写锁。
当读写锁是写模式加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁是读模式加锁状态时,所有试图以读模式对其进行加锁的线程都可以得到访问权,但是任何以写模式对此锁就行加锁的线程都会被阻塞,直到所有线程释放了其持有的读模式加锁状态。

读写锁也被称为共享互斥锁(shared-exclusive-lock),适合于数据结构读的次数远大于写的情况。当读写锁是读模式锁住,可以称为以共享模式锁住,当其是写模式锁住时,可以称为以互斥模式锁住。

读写锁在使用之前必须初始化,在释放其底层内存之前必须销毁:

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/*若成功,返回0,若出错,返回错误编码*/

若attr为NULL,则初始化的读写锁具有默认属性。可以使用常量PTHREAD_RWLOCK_INITIALIZER对静态分配的读写锁以默认属性初始化。

以读模式或者写模式加锁,以及解锁的接口函数为:

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
/*若成功,返回0,若出错,返回错误编码*/

带有超时的读写锁加锁函数为:

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
                               const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
                               const struct timespec *restrict tsptr);
/*若成功,返回0,若出错,返回错误编码*/

条件变量

条件变量是在多线程程序中实现“等待–>唤醒”逻辑的常用方法。条件变量是利用线程间共享的全局变量进行同步的一种机制。
消费者线程等待条件变量的条件成立而挂起,生成者线程使得该条件成立。
条件变量的使用总是和一个互斥量绑定在一起。

对条件变量进行初始化与销毁的函数为:

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
                      const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
/*若成功,返回0,若出错,返回错误编码*/

等待条件变量成立的函数为:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                           pthread_mutex_t *restrict mutex,
                           const struct timespec *restrict tsptr);
/*若成功,返回0,若出错,返回错误编码*/

传递给pthread_cond_wait的互斥量mutex对条件进行保护。调用者把锁住的互斥量传递给函数,函数然后自动将调用线程放在等待条件的线程列表上,然后对互斥量解锁。当pthread_cond_wait返回时,互斥量再次被锁住。

对于生产者线程,用来通知消费者线程条件已经成立的函数为:

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
/*若成功,返回0,若出错,返回错误编码*/

使用条件变量需要注意spurious wakeup,对于spurious wakeup的的行为描述是:“a thread might be awoken from its waiting state even though no thread signaled the condition variable”,可以这样理解,pthread_cond_wait 返回后,并不一定就真的是因为别的地方调用了pthread_cond_signal(),有可能是因为别的原因而返回了,因此这个 wakeup 是假的(spurious)。

因此需要遵循使用的条件变量的正确套路。即条件变量应该始终结合一个 bool 变量来使用,这个 bool 变量用来指示是否真的线程调用了pthread_cond_signal,从而解决 spurious wakeup 的问题。

对于消费者线程:

  1. 必须与mutex一起使用,且相应的bool变量要受该mutex的保护;
  2. 先lock mutex,再wait;
  3. wait要放到循环中,直到bool变量已改变。

对于生产者线程:

  1. pthread_cond_signal()调用可以不用mutex保护;
  2. 要先修改bool变量再进行pthread_cond_signal();
  3. 修改该bool变量需要用mutex进行保护。

以下是一个范例:

 bool g_signaled= false;
 pthread_mutex_t g_mutex;
 pthread_cond_t g_cond;
 
 /* 消费者线程 */
 void wait() 
 {  
    pthread_mutex_lock(&g_mutex);    
    while (!g_signaled)    
    {      
        pthread_cond_wait(&g_cond, &g_mutex);
    }
    /* 这里已经等待条件变量发生 */
    /* 可进行相关的处理,或者重新设置g_signaled */ 
    pthread_mutex_unlock(&g_mutex);  
 }  
 
 /* 生产者线程 */
 void signal() 
 {    
    pthread_mutex_lock(&g_mutex);   
    g_signaled = true;    
    pthread_mutex_unlock(&g_mutex);    
    pthread_cond_signal(&g_cond);  
 }

自旋锁

自旋锁也是一种锁,与互斥量类似,但是与互斥量的区别是,自旋锁不是通过休眠使得进程阻塞,而是在获取锁之前一直处于忙等(自旋)的阻塞状态。
自旋锁可用于以下情况:锁被持有的时间短,且线程不希望在重新调度上花费太多的成本。

在用户层面,自旋锁并不是非常的有用,除非运行在不允许抢占的实时调度类中。

自选锁的接口与互斥量非常的类似:

#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
/*若成功,返回0,若出错,返回错误编码*/

其中pshared参数表示进程共享属性,表明自旋锁是如何获取的,如果pshared参数被设定为PTHREAD_PROCESS_SHARED,则自旋锁能被可以访问锁底层内存的线程所获取,即使那些线程属于不同的进程。
若pshared参数被设定为PTHREAD_PROCESS_PRIVATE,自旋锁就只能被初始化该锁的进程内部的线程所访问。

猜你喜欢

转载自blog.csdn.net/jiangzhangha/article/details/86506447