11 线程
11.1 引言
进一步理解进程,了解在单进程环境中如何使用多个线程执行过个任务。一个进程中的所有线程都可以访问该进程的组成部件,如文件描述符和内存。另外为了在单个进程中多线程的资源共享时出现不一致,本章还讨论了同步机制。
11.2 线程概念
典型的UNIX进程可看成一个控制线程:一个进程在某一时刻只做一件事情。
有了多个控制线程,程序设计时就可以把进程设计成在某一时刻能够做不知一件事情,每个线程可独自处理任务。此方法好处如下:
- 通过为每种事件类型分配单独的处理线程,可以简化处理异步事件的代码。
- 多个进程必须使用操作系统提供的复杂机制才能实现内存共享和文件描述符的共享,而多个线程自动的可以访问相同的存储地址空间和文件描述符。
- 有些问题可以分解从而提高整个程序的吞吐量。
- 交互的程序同样可以通过使用多线程来改善相应时间,多线程可以把程序中处理用户输入输出的部分与其他部分分开。
每个线程都包含表示执行环境所必需的的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号和屏蔽字、errno变量以及线程私有数据。
一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存、栈以及文件描述符。
11.3 线程标识
比较两个线程ID。线程ID只有在它所述的进程上下文中才有意义。
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
return:相等返回非0,不等返回0
线程可以通过调用pthread_self函数来获得自身的线程ID。
#include <pthread.h>
pthread_t pthread_self(void); // 返回调用线程的线程ID
11.4 线程创建
创建一个线程。
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, //指向新创建的线程ID
const pthread_attr_t *restrict attr, // 用于定制各种不同的线程属性
void *(*start_rtn)(void *), // 新创建的线程从start_rtn函数的地址开始运行
void *restrict arg); // start_rtn函数需要传递的参数,注意无类型指针参数多于一个需放到一个结构
return: 0; error: 错误编号
11.5 线程终止
单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流:
- 线程可以简单的从启动例程中返回,返回值是线程大的退出码。
- 线程可以被同一进程的其他线程取消。
- 线程调用pthread_exit函数。
#include <pthread.h>
void pthread_exit(void *rval_ptr);
其中rval_ptr为一个无类型指针参数,如(void *).
进程中的其他线程可以通过下函数访问到这个指针。
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
return: 0,error: 错误编号
调用的线程将一直等待指定线程的终止、从启动例程返回或被取消。rval_ptr
包含返回码。若线程被取消,为PTHREAD_CANCELED
.
线程可通过下函数请求取消同一进程中的其他线程。
#include <pthread.h>
int pthread_cancel(pthread_t tid); // 成功返回0,否则返回错误编码
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数类似。这种被称作线程清理处理程序。
一个线程可以建立多个清理处理程序,处理程序记录在栈中,也就是说执行顺序与注册时相反。
execute为0,清理函数将不被调用。不管是:1,调用pthread_exit时;2,响应取消请求;3,用非零execute时.
pthread_cleanup_pop函数将删除上次pthread_cleanup_push建立的清理处理程序。
在线程被分离后,我们不能用pthread_join函数来等待其终止状态,因为分离状态的线程调用pthread_join会产生未定义行为。 可调用下函数分离线程。
#include <pthread.h>
int pthread_detach(pthread_t tid); //成功返回0,否则返回错误编码
return: 0, error: wrong number.
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
return: 0, error: wrong number.
int pthread_mutex_destroy(pthread_mutex_t *mutex);
return: 0, error: wrong number.
使用互斥变量之前必须首先对它进行初始化,可把它设置成常亮PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量)
也可通过上第一个函数初始化。如果动态分配互斥量,在释放内存前调用第二个函数。
11.6 线程同步
1 互斥量(mutex)
确保同一时间只有一个线程访问数据。
#incldue <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);
return: 0, error: wrong number.
对互斥量加锁和解锁,如果线程不希望被阻塞,可以使用第二个函数尝试对互斥量加锁,即互斥量未上锁时,调完锁上,否则,不能锁住互斥量,并返回EBUSY
2 避免死锁
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态,但是使用互斥量时,还有其他不太明显的方式也能产生死锁。怎样避免死锁呢?
- 通过仔细控制互斥量加锁的顺序来避免死锁的发生。
- 有时候应用程序的结构很难对互斥量进行排序,这种情况下,可先释放占有的锁,过段时间再试。
- 可以使用散列列表锁来保护结构引用次数,使事情大大简化。
注意,两种用途使用相同的锁时,围绕散列列表和引用计数的锁的排序问题就不存在了。设计者应考虑到两者之间的折中,锁的粒度太粗,会出现很多线程阻塞等待相同的锁,不能改善并发性;太细,是系统开销增加,性能下降,而且代码变得复杂。
3 函数pthread_mutex_timedlock
当线程试图获取一个已加锁的互斥量时,此函数互斥量原语允许绑定线程阻塞时间。pthread_mutex_timedlock
函数与pthread_mutex_lock
是基本等价的,但是在达到超时时间值时,上函数不会对互斥量进行加锁,而是返回错误码ETIMEDOUT。
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
return: 0, error: wrong number.
struct timespec
{
time_t tv_sec; /* Seconds. */
long tv_nsec; /* Nanoseconds. */
};
允许绑定线程阻塞时间。
4 读写锁
#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);
return: 0, error: wrong number.
读写锁也叫共享互斥锁(shared-exclusive lock)。写加锁时,都不能访问,读加锁时,他读放行,写不可。上面函数是初始化和释放。
#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);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
return:0; error: 错误编码
5 带有超时的读写锁
与互斥量一样,UNIX提供带有超时的读写锁加锁函数,使应用程序在获取读写锁时避免陷入永久的阻塞状态。两个函数分别如下:
#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr);
int pthread_rwlock_timedwlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr);//指定线程应该停止阻塞的时间
return: 0; error: 错误编号.
6 条件变量
它是线程可用的另一种同步机制。他给多线程提供一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。
在使用条件变量之前,必须先对它进行初始化。用到pthread_cond_init
函数。
在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy
函数对条件变量进行反初始化(deinitialize)。
#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);
return: 0; error: wrong number.
除非要创建一个具有非默认属性的条件变量,否则pthread_cond_init
函数的attr参数可设置为NULL。
我们使用pthread_cond_wait
等待条件变量变为真。如果给定时间内不能满足,那么会生成一个返回错误码的变量。
#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);
return: 0; error: wrong number.
可使用clock_gettime函数(见6.10节)获取timespec结构表示的当前时间。但是并不是所有平台都支撑这个函数,所以可用零个函数gettimeofday获取timeval结构表示的当前时间,然后在转成timespec结构。要得到超时值的绝对时间,见下面函数(假设阻塞最大时间使用分来表示):
#include <sys/time.h>
#include <stdlib.h>
/*
struct timespec
{
time_t tv_sec; /* Seconds. */
long tv_nsec; /* Nanoseconds. */
};
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
*/
void maketimeout(strcut timespec *tsp, long minutes)
{
struct timeval now;
/* get the current time */
gettimeofday(&now, NULL);
tsp->tv_sec = now.tvsec;
tsp->tv_nsec = now.tv_usec * 100;//usec to nsec
/* add the offset to get timeout value */
tsp->tv_sec += minutes * 60;
}
int gettimeofday(struct timeval *tv, struct timezone *tz);
参数:struct timeval *tv 将带回当前的系统时间,从UTC1970-1-1 0:0:0开始计时
struct timezone *tz 带回当前的时区信息,如果不需要刻意设置为0
struct timezone
{
int tz_minuteswest; //和格林威治 时间差了多少分钟
int tz_dsttime; //日光节约时间的状态(夏时制)
};
如果超时到期时条件还是没有出现,pthread_cond_timewait
将重新获取互斥量,然后返回错误ETIMEDOUT。从pthread_cond_wait
或者pthread_cond_timedwait
调用成功返回时,线程需要重新计算条件,因为另一个线程可能已经在运行并改变了条件。
有两个函数可以用于通知线程条件已经满足。pthread_cond_signal
函数至少能唤醒一个等待该条件的线程,而pthread_cond_broadcast
函数可以唤醒等待该条件的所有线程。
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
return: 0; error: wrong number.
在调用上两个函数时,我们说这是再给线程或者条件发信号。必须注意,一定要在改变条件状态以后在给线程发信号。
7 自旋锁
它与互斥量类似,但他不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。
自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。
自旋锁通常作为底层原语实现其他类型的锁。
当自旋锁用在非抢占式内核中时非常有用:除了提供互斥机制以外,他们会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为他需要获取已被加锁的自旋锁(把中断想成是另一种抢占)。在这种类型的内核中,中断处理程序不能休眠,因此他们能用的同步原语只能是自旋锁。
但是在用户层,自旋锁不是非常有用,除非运行在不允许抢占的实时调度类中。
很多互斥量的实现非常高效,以至于应用程序采用互斥锁的性能与曾经采用过自旋锁的性能基本是相同。
自旋锁的结构与互斥量的接口类似,这使得他可以比较容易的从一个替换为另一个。
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
return: 0; error: wrong number.
pshared参数表示进程共享属性,表明自旋锁是如何获取的。如果它设为PTHREAD_PROCESS_SHARED
,则自旋锁能被可以访问所底层内存的线程所获取,即便哪些线程属于不同的进程,情况也是如此。否则它被设为PTHREAD_PROCESS_PRIVATE
,自旋锁就只能被初始化该锁的进程内部的线程所访问。
可用下面函数对自旋锁进行加锁:
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);//在获取锁之前一直自旋
int pthread_spin_trylock(pthread_spinlock_t *lock);//如果不能获取锁,就立即返回EBUSY错误
int pthread_spin_unlock(pthread_spinlock_t *lock);
return: 0; error:wrong number.
8 屏障(barrier)
它是用户协调多个线程并行工作的同步机制。它允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。我们已经看到一种屏障,pthread_join
函数就是一种屏障,允许一个线程等待直到另一个线程退出。
但是屏障对象的概念更广,他们允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。
可以使用下面函数对屏障进行初始化和反初始化:
#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr,
unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
return: 0; error: wrong number.
初始化屏障时,可使用count参数指定允许线程继续运行之前必须到达屏障的线程数目。使用attr指定屏障对象的属性。
可使用下函数来表名线程已完成工作,准备等待所有其他线程赶上来:
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
return: 0 or PTHREAD_BARRIER_SERIAL_THERAD; error: wrong number
调用上函数后,线程在屏障计数未满足条件时,会进入休眠状态。如果该线程是最后一个调用上函数的线程,就满足count条件,所有线程都将被唤醒。
对于任意一个调用上函数返回了PTHREAD_BARRIER_SERIAL_THERAD
的线程可以作为一个主线程,其他调用上函数的返回值都将是0,主线程可以工作在其他所有线程已完成的工作结果上。
一旦达到屏障计数数值,而且线程处于非阻塞状态,屏障可以被重用。但是除非在调用了反初始化函数之后,又调用了初始化函数对计数用另外的书进行初始化,否则屏障计数不会改变。