30 August 2014

概念


每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。一个进程的所有信息对该进程的所有线程都时共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。

线程的标识和创建


  • pthread_equal():比较两个线程ID;
  • pthread_self():获得自身的线程ID;
  • pthread_create():创建线程,新线程继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集(尚未递送的信号)会被清除。

线程的终止


  1. 线程可以简单地从启动例程中返回,返回值是线程的退出码;
  2. 线程可以被同一进程中的其他线程取消;
  3. 线程调用pthread_exit。

pthread_jon将阻塞等待指定线程终止。

线程可以通过调用pthread_cancel函数请求取消同一进程中的其他线程。

在默认情况下,线程的终止状态会保存直到对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。调用pthread_detach分离线程。

线程清理程序


当线程执行以下动作时,清理函数是由pthread_cleanup_push函数调度的,调用时只有一个参数:

  • 调用pthread_exit时;
  • 响应取消请求;
  • 用非零execute参数调用pthread_cleanup_pop时。

如果execute参数设置为0,清理函数将不被调用。不管发生上述哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理程序。

以上两个函数必须成对匹配调用。

如果线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。

线程同步


互斥量

用pthread_mutex_t数据类型表示,在使用之前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态分配互斥量(例如,通过调用malloc函数),在释放内存前需要调用pthread_mutex_destroy。

对互斥量的锁操作:pthread_mutex_init()、pthread_mutex_destroy()、pthread_mutex_lock()、pthread_mutex_trylock()、pthread_mutex_unlock()、pthread_mutex_timedlock()

如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁,这可能并不能改善并发性。如果锁的粒度太细,那么过多的锁开销会使系统性能受到影响,而且代码变得复杂。

读写锁

读写锁可以有3种状态:读模式、写模式、不加锁。

用pthread_rwlock_t数据类型表示,在使用之前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_RWLOCK_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_rwlock_init函数进行初始化。如果动态分配互斥量(例如,通过调用malloc函数),在释放内存前需要调用pthread_rwlock_destroy。

对读写锁的锁操作:pthread_rwlock_init()、pthread_rwlock_destroy()、pthread_rwlock_rdlock()、pthread_rwlock_wrlock()、pthread_rwlock_unlock()、pthread_rwlock_tryrdlock()、pthread_rwlock_trywrlock()、pthread_rwlock_timedrdlock()、pthread_rwlock_timedwrlock()

条件变量

用pthread_cond_t数据类型表示,在使用之前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_COND_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_cond_init函数进行初始化。如果动态分配互斥量(例如,通过调用malloc函数),在释放内存前需要调用pthread_cond_destroy。

对条件变量的锁操作:pthread_cond_init()、pthread_cond_destroy()、pthread_cond_wait()、pthread_cond_timedwait()、pthread_cond_signal()、pthread_cond_broadcast()

自旋锁

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

对自旋锁的锁操作:pthread_spin_init()、pthread_spin_destroy()、pthread_spin_lock()、pthread_spin_trylock()、pthread_spin_unlock()

屏障

屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。我们已经看到一种屏障,pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。但是屏障对象的概念更广,它们允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出,所有线程达到屏障后可以接着工作。

对屏障的操作:pthread_barrier_init()、pthread_barrier_destroy()、pthread_barrier_wait()

如果线程调用了最后一个pthread_barrier_wait,就满足了屏障计数,所有的线程都被唤醒。对于任意一个线程,pthread_barrier_wait函数返回了PTHREAD_BARRIER_SERIAL_THREAD。剩下的线程看到的返回是0.这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上。

一旦达到屏障计数值,而且线程处于非阻塞状态,屏障可以被重用。但是除非调用了pthread_barrier_destroy函数之后,又调用了pthread_barrier_init函数对计数用另外的数进行初始化,否则屏障计数不会改变。

线程控制


线程限制

线程属性

结构:pthread_attr_t

  • detachstate
  • guardsize
  • stackaddr
  • stacksize

pthread_attr_init()的实现对属性对象的内存空间是动态分配的,pthread_attr_destroy()就会释放该内存空间。

如果对现有的某个线程的终止状态不感兴趣的话,可以使用pthread_detach函数让操作系统在线程退出时收回它占用的资源。函数:pthread_attr_getdetachstate()、pthread_attr_setdetachstate()

同步属性

1、互斥量属性 进程共享属性(多个进程是否共享)、健壮属性(与在多个进程间共享的互斥量有关)、类型属性(控制着互斥量的锁定特性)。pthread_mutexattr_init()、pthread_mutexattr_destroy()、pthread_mutexattr_getpshared()、pthread_mutexattr_setpshared()、pthread_mutexattr_getrobust()、pthread_mutexattr_setrobust()、pthread_mutex_consistent()、pthread_mutexattr_gettype()、pthread_mutexattr_settype()

2、读写锁属性 进程共享属性。pthread_rwlockattr_init()、pthread_rwlockattr_destroy()、pthread_rwlockattr_getpshared()、pthread_rwlockattr_setpshared()

3、条件变量属性 进程共享属性、时钟属性(控制计算pthread_cond_timewait函数的超时参数采用的是哪个时钟)。pthread_condattr_init()、pthread_condattr_destroy()、pthread_condattr_getpshared()、pthread_condattr_setpshared()、pthread_condattr_getclock()、pthread_condattr_setclock()

4、屏障属性 进程共享属性。pthread_barrierattr_init()、pthread_barrierattr_destroy()、pthread_barrierattr_getpshared()、pthread_barrierattr_setpshared()

重入

如果一个函数在相同时间点可以被多个线程安全地调用,就称该函数是线程安全的。但这并不能说明对信号处理程序来说该函数也是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。

线程特定数据

存储和查询某个特定线程相关数据的一种机制。

在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于对线程特定数据的访问。pthread_key_create(pthread_key_t *keyp, void (*destructor) (void *))函数创建的键存储在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程特定数据地址进行关联。创建新键时,每个线程地址设为空值。

除了创建键以外,pthread_key_create可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经被设置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。如果传入的析构函数为空,就表明没有析构函数与这个键关联。当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用。同样,线程取消时,只有在最后的清理处理程序返回之后,析构函数才会被调用。如果线程调用了exit、_exit、_Exit或abort,或者出现其他非正常的退出时,就不会调用析构函数。

线程通常使用malloc为线程特定数据分配内存。析构函数通常释放已分配的内存。如果线程在没有释放内存之前就退出了,那么这块内存就会丢失,即线程所属进程就出现了内存泄露。

线程可以为线程特定数据分配多个键,每个键都可以有一个析构函数与它关联。每个键的析构函数可以互不相同,当然所有键也可以使用相同的析构函数。每个操作系统实现可以对进程可分配的键的数量进行限制(PTHREAD_KEYS_MAX)。

线程退出时,线程特定数据的析构函数将按照操作系统视线中定义的顺序被调用。析构函数可能会调用另一个函数,该函数可能会创建新的线程特定数据,并且把这个数据与当前的键关联起来。当所有的析构函数都调用完以后,系统会检查是否还有非空的线程特定数据值与键关联,如果有的话,再次调用析构函数。这个过程会一直持续到线程所有的键都为空线程特定数据值,或者已经做了PTHREAD_DESTRUCTOR_ITERATIONS中定义的最大次数的尝试。

对所有线程,我们都可以通过调用pthread_key_delete来取消键与线程特定数据值之间的关联关系。注意,调用pthread_key_delete并不会激活与键关联的析构函数。要释放任何与键关联的线程特定数据值的内存,需要在应用程序中采取额外的步骤。

pthread_once确保函数只被调用一次。

键一旦被创建,就可以通过调用pthread_setspecific函数把键和线程特定数据关联起来。可以通过pthread_getspecific函数获得线程特定数据的地址。

取消选项

有两个线程属性并没有包含在pthread_attr_t结构中,它们是可取消状态(PTHREAD_CANCEL_ENABLE、PTHREAD_CANCEL_DISABLE)和可取消类型。这两个属性影响着线程在响应pthread_cancel函数调用时所呈现的行为。

pthread_setcancelstate设置可取消状态。

在默认情况下,线程在取消请求发出以后还是继续执行,直到线程到达某个取消点。取消点是线程检查它是否被取消的一个位置,如果取消了,则按照请求行事。(如果把取消disable,那么是不是线程就省去了检查取消点的耗费)

如果线程的状态为PTHREAD_CANCEL_DISABLE时,对pthread_cancel的调用并不会杀死线程。相反,取消请求对这个线程来说还处于挂起状态,当取消状态再次变为PTHREAD_CANCEL_ENABLE时,线程将在下一个取消点上对所有挂起的取消请求进行处理。

POSIX.1定义了一些可选的取消点:P363。线程也可以添加自己的取消点,调用pthread_testcancel函数。

可以通过调用pthread_setcanceltype来修改取消类型(推迟取消、异步取消)。

异步取消使得线程可以在任意时间撤销,而不是非得遇到取消点才被取消。

线程与信号

每个线程都有自己的信号屏蔽字,但是信号的处理是进程中的所有线程共享的。这意味着单个线程可以阻止某些信号,但当某个线程修改了与某个给定信号相关的处理行为后,所有的线程都必须共享这个处理行为的改变。

进程中的信号是递送到单个线程的。如果一个信号与硬件故障有关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。

线程使用pthread_sigmask设置信号屏蔽字,sigwait等待一个或多个信号的出现。

如果多个线程在sigwait的调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从sigwait中返回。如果一个信号被捕获,而且一个线程正在sigwait调用中等待同一信号,那么这是将由操作系统实现决定以何种方式递送信号。操作系统实现可以让sigwait返回,也可以激活信号处理程序,但这两种情况不会同时发生。

调用pthread_kill可以把信号发送给线程。

注意,闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以,进程中的多个线程不能互不干扰地使用闹钟定时器。

线程和fork

在子进程的内部,只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程将同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法直到它占有了哪些锁、需要释放哪些锁。

如果子进程从fork返回以后马上调用其中一个exec函数,就可以避免这样的问题。这种情况下,旧的地址空间就被丢弃,所以锁的状态无关紧要。但如果子进程需要继续作处理工作的话,这种策略就行不通,还需要使用其他的策略。

在多线程的进程中,为了避免不一致状态的问题,POSIX.1声明,在fork返回和子进程调用其中一个exec函数之间,子进程只能调用异步信号安全的函数。这就限制了在调用exec之间子进程能做什么,但不涉及子进程中锁状态的问题。

要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程序。

线程和IO

pread()、pwrite()