第十一章:线程
11.1:引言
本章介绍多线程的使用
11.2:线程概念
典型的Unix进程可以看作只有一个控制线程:一个进程在同一时刻只做一件事情。
11.3:线程标识
每个线程跟进程一样,都有一个线程ID来标识。
#include <pthread.h> int pthread_equal(pthread_t tid1, pthread_t tid2); // 判断两个线程ID是否相等,若相等,返回非0,否则返回0 pthread_t pthread_self(void); // 返回调用线程的线程ID
11.4:线程创建
默认一个进程只包含有个线程,如果要实现多线程,首先得创建新的线程,可以使用函数pthread_create来实现。
#include <pthread.h> // 若成功返回0,否则返回错误编号 int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void*(*start_rtn)(void), void *restrict arg);
如果成功,tidp指向的内存保存线程ID,attr是线程属性,可以为NULL,start_rtn是线程函数,arg是传递给线程函数的参数。
实例:11.1 测试线程ID
#include <pthread.h> #include <iostream> #include <cstdio> #include <cstdlib> #include <cerrno> using namespace std; pthread_t ntid; void printids(const char *s) { pid_t pid; pthread_t tid; pid = getpid(); tid = pthread_self(); printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid); } void* thr_fn(void *arg) { printids("new thread: "); return ((void*)0); } int main(int argc, char **argv) { int err; err = pthread_create(&ntid, NULL, thr_fn, NULL); if (err != 0) { perror("create thread failed!"); return 0; } printids("main thread:"); sleep(100); exit(0); }
11.5:线程终止
如果进程中任一线程调用了exit、_exit、_Exit,那么整个进程就会终止。与此类似,如果信号的默认动作是终止进程,那么信号发送到线程会终止整个进程。
单个线程可以通过下列三种方式退出,在不终止整个进程的情况下停止它的控制流:
1.线程只是从启动例程中返回,返回值是线程的退出码。
2.线程可以被同一个进程中的其他线程取消。
3.线程调用pthread_exit()。
#include <pthread.h> void pthread_exit(void *rval_ptr);
rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以调用pthread_join来访问这个指针。
#include <pthread.h> int pthread_join(pthread_t thread, void **rval_ptr); // 若成功返回0,否则返回错误编号
调用线程将一直阻塞,直到指定的线程调用pthread_exit()、从启动例程返回或被取消。
实例:11-2 获得线程退出状态
实例:11-3 pthread_exit参数的不正确使用
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他进程。
#include <pthread.h> int pthread_cancel(pthread_t tid); // 若成功则返回0,否则返回错误编号
注意:pthread_cancel并不等待线程终止,它仅仅提出请求。
线程可以安排它退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说它们的执行顺序与它们注册时顺序相反。
#include <pthread.h> void pthread_cleanup_push(void (*rtn)(void*), void *arg); void pthread_cleanup_pop(int execute);
当线程执行以下动作时调用清理函数,调用参数为arg,清理函数rtn的调用顺序是由pthread_cleanup_push函数来安排的。
1.调用pthread_exit时。
2.相应取消请求时。
3.用非零execute参数调用pthread_cleanup_pop时。
如果execute参数置为0,清理函数将不被调用。无论哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理程序。
实例:11-4 线程清理处理程序
默认情况下,线程的终止状态会保存到对该线程调用pthread_join,如果线程已经处于分离状态,线程的底层存储资源可以在线程终止时被立即回收。当线程分离时,并不能用pthread_join函数等待它的终止状态。对分离状态的线程进行pthread_join的调用会产生失败,返回EINVAL。pthread_detach调用可以用于使线程进入分离状态。
#include <pthread.h>
int pthread_detach(pthread_t tid);
分离状态--即该线程在终止时就可以回收其资源。
11.6:线程同步
当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果一个线程使用的变量其他线程是不会读取和修改的,就不会存在一致性问题。同样,如果变量是只读的,也不会有一致性问题。
1.互斥量
互斥量用pthread_mutex_t数据类型来表示,在使用互斥量前,必须对其进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量使用),也可以调用pthread_mutex_init函数进行初始化。如果是动态分配的互斥量(malloc),在释放内存之前,需要调用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_mutext_t *mutex);
// 返回值:若成功返回0,否则返回错误编号
要用默认的属性初始化互斥量,只需要把attr设置为NULL即可。
对互斥量进行加锁需要调用pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_mutex_unlock。
#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_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,而返回EBUSY。
实例 11-5:使用互斥量保护数据结构
2.避免死锁
如果线程对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。如果允许一个线程一直占用第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,就会发生死锁。
只有在一个线程试图以与另一个线程相反的顺序锁住互斥量时,才可能出现死锁。
3.读写锁
读写锁与互斥量类似,但是读写锁允许更高的并发性。互斥量是要么是加锁状态,要么是不加锁状态,而且一次只能有一个线程对其加锁。
读写锁有三种状态:加读锁、加写锁、不加锁。一次只能有一个线程占有写状态的读写锁,但是多个线程可以同时拥有读状态的读写锁。
当读写锁是写状态加锁是,所有试图对这个读写锁加锁的线程都被阻塞。当读写锁是读状态加锁时,所有以读状态加锁的线程都可以获得访问权,而所有以写状态加锁的线程都会被阻塞直到所有读锁被释放。
当读写锁处于读状态加锁时,如果有另外的线程以写状态加锁而被阻塞,读写锁通常会阻塞随后的读状态加锁请求。这样可以避免读模式长期占用,而等待的写模式锁请求一直得不到满足。
与互斥量一样,读写锁在使用之前必须初始化,在销毁它们底层的内存之前必须销毁。
#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,否则返回错误编号
要在读模式下锁定读写锁,需要调用pthread_rwlock_rdlock;要在写模式下锁定读写锁需要调用pthread_rwlock_wrlock。不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock解锁。
#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,否则返回错误编号
Single Unix Specification同样定义了有条件的读写锁原语的版本。
#include <pthread.h> int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); // 返回值:若成功返回0,否则返回错误编号
实例 11-8:使用读写锁
4.条件变量
条件变量是线程可用的另一种同步机制,条件变量给多线程提供了一个会合的场所。条件变量与互斥量一起使用,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。线程在改变条件状态前,必须先锁住互斥量,其他线程在获取互斥量之前,不会察觉到这种改变,因为必须锁住互斥量后才能计算条件。
条件变量使用之前必须进行初始化,pthread_cond_t数据类型代表的条件变量可以有两种初始化方式,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,可以使用pthread_cond_init函数进行初始化。
在释放底层内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行去初始化。
#include <pthread.h> int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr); int pthread_cond_destroy(pthread_cond_t *cond); // 返回值:若成功返回0,否则返回错误编号
使用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 timeout); // 返回值:若成功返回0,否则返回错误编号
传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。
pthread_cond_timedwait函数的工作方式与pthread_cond_wait类似,只是多了一个timeout。timeout值指定等待的时间,它是通过timespec结构指定的。时间值用秒数或分秒数表示,分秒数的单位是纳秒。
struct timespec { time_t tv_sec; // seconds long tv_nsec; // nanoseconds };
使用这个结构时,需要指定愿意等待多长时间,时间值是一个绝对数而不是相对数。例如,如果能等待三分钟,就需要把当前时间加上三分钟再转换到timespec结构,而不是把三分钟转换成timespec结构。
如果时间值到了但是条件还是没有出现,pthread_cond_timedwait将重新获取互斥量然后返回错误ETIMEDOUT。从pthread_cond_wait或者pthread_cond_timewait调用成功返回时,线程需要重新计算条件,因为其他线程可能已经在运行并改变了条件。
有两个函数可以用于通知线程条件已经满足。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); // 返回值:若成功返回0,否则返回错误编号
调用pthread_cond_signal或pthread_cond_broadcast,也称为向线程或条件发送信号。必须注意一定要在条件状态改变后再给线程发送信号。
11.7:小结
在本章中介绍了线程的概念,讨论了现有的创建线程和销毁线程的POSIX原语;此外,还介绍了线程同步的问题,讨论了三种基本的同步机制:互斥、读写锁以及条件变量,了解如何使用它们来保护共享资源。