线程标识
每个进程都有ID作为标识,线程也还是一样,但是线程的ID只有在所属进程的上下文才有意义。线程的ID使用pthread_t表示。比较时使用下列函数
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
//返回值:若相等,返回非0数值;否则,返回0
获取当前线程ID
#include <pthread.h>
pthread_t pthread_self(void);
//返回值:调用线程的线程ID
线程创建
#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,创建一个具有默认属性的线程。
线程终止
如果进程中的任意线程调用了exit系列的函数,整个进程就会终止。
单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
- 线程可以简单地从启动例程中返回,返回值是线程的退出码。
- 线程可以被同一进程中的其他线程取消。
- 线程调用pthread_exit。
#include <pthread.h>
// rval_ptr 参数是一个无类型指针,与传给启动例程的单个参数类似。
// 进程中的其他线程也可以通过调用pthread_join函数访问到这个指针。
void pthread_exit(void *rval_ptr);
#include <pthread.h>
// 调用线程将一直阻塞,直到指定的线程调用pthread_exit、
// 从启动例程中返回或者被取消。
int pthread_join(pthread_t thread, void **rval_ptr);
//返回值:若成功,返回0;否则,返回错误编号
注意pthread_exit返回的内存要保证线程停止后任然能使用。
#include <pthread.h>
// 请求取消同一进程中的其他线程。
int pthread_cancel(pthread_t tid);
// 返回值:若成功,返回0;否则,返回错误编号
线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数安排退出是类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
当线程执行以下动作时,清理函数会被调度。
- 调用pthread_exit时
- 响应取消请求时
- 用非零参数调用pthread_cleanup_pop时
如果线程中return结束不会调用清理函数。
进程和线程函数对比
线程分离
在默认情况下,线程的终止状态会保存直到对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用pthread_join函数等待它的终止状态,因为对分离状态的线程调用pthread_join会产生未定义行为。
可以调用pthread_detach分离线程。
#include <pthread.h>
int pthread_detach(pthread_t tid);
// 返回值:若成功,返回0;否则,返回错误编号
线程同步
- 互斥锁
互斥变量是用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_mutex_t *mutex);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
// 在加锁失败时,会阻塞线程
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 加锁失败不会阻塞,返回EBUSY
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 所有函数的返回值:若成功,返回0;否则,返回错误编号
// 超时加锁,也是进行加锁,但是超过多少时间没有加锁成功,就返回ETIMEOUT
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);
// 返回值:若成功,返回0;否则,返回错误编号
- 读写锁
读写锁可以有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;否则,返回错误编号
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;否则,返回错误编号
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;否则,返回错误编号
- 条件变量
在使用条件变量之前,必须先对它进行初始化。由pthread_cond_t数据类型表示的条件变量可以用两种方式进行初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,则需要使用pthread_cond_init函数对它进行初始化。
条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。
#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;否则,返回错误编号
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的互斥量对条件进行保护。调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住
唤醒函数:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
自旋锁
自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。
当自旋锁用在非抢占式内核中时是非常有用的:除了提供互斥机制以外,它们会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它需要获取已被加锁的自旋锁(把中断想成是另一种抢占)。在这种类型的内核中,中断处理程序不能休眠,因此它们能用的同步原语只能是自旋锁。
但是,在用户层,自旋锁并不是非常有用,除非运行在不允许抢占的实时调度类中。运行在分时调度类中的用户层线程在两种情况下可以被取消调度:当它们的时间片到期时,或者具有更高调度优先级的线程就绪变成可运行时。在这些情况下,如果线程拥有自旋锁,它就会进入休眠状态,阻塞在锁上的其他线程自旋的时间可能会比预期的时间更长。
很多互斥量的实现非常高效,以至于应用程序采用互斥锁的性能与曾经采用过自旋锁的性能基本是相同的。事实上,有些互斥量的实现在试图获取互斥量的时候会自旋一小段时间,只有在自旋计数到达某一阈值的时候才会休眠。这些因素,加上现代处理器的进步,使得上下文切换越来越快,也使得自旋锁只在某些特定的情况下有用。
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pshared 参数表示进程共享属性,表明自旋锁是如何获取的。如果它设为 PTHREAD_PROCESS_SHARED,意思是只能能获取到这个锁,就能进行操作,即时是不同进程。否则pshared参数设为 PTHREAD_PROCESS_PRIVATE,自旋锁就只能被初始化该锁的进程内部的线程所访问。
#include <pthread.h>
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;否则,返回错误编号
不要对加锁的的自旋锁重复加锁,对解锁的锁重复解锁,否则出现的错误未知。
屏障
屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。我们已经看到一种屏障,pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。
#include <pthread.h>
// count指定等待的线程数
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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
// 表明线程已完成工作,准备等所有其他线程赶上来
int pthread_barrier_wait(pthread_barrier_t *barrier);
// 返回值:若成功,返回0或者PTHREAD_BARRIER_SERIAL_THREAD;否则,返回错误编号
对于一个任意线程,pthread_barrier_wait函数返回了PTHREAD_BARRIER_SERIAL_THREAD。剩下的线程看到的返回值是0。这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上。
一旦达到屏障计数值,而且线程处于非阻塞状态,屏障就可以被重用。但是除非在调用了pthread_barrier_destroy函数之后,又调用了pthread_barrier_init函数对计数用另外的数进行初始化,否则屏障计数不会改变。