线程
1. 引言
Q: 相关的进程间存在一定的共享?
进程与线程的区别
进程的执行实际上是内核按顺序调用系统调用指令集。指令的执行必须要有其操作对象和操作环境(控制作用)。事实上,内核的总是在不同的进程间切换执行进程,因为CPU的处理速度远远高于总线上挂载的设备(RAM)的处理速度,所以在执行完指令块等待其他设备操作时,CPU切换进程执行,避免时间的浪费。其执行过程可以视为读取之前保存的进程环境(从何处继续执行,调用栈),执行指令,保存当前进程环境(写入变量,记录执行停止指令位置),切换另一进程执行。线程是在进程执行指令时执行的某一部分指令集合,它在线程自己的环境下执行,共享进程环境,在执行时仅切换线程的环境来执行不同的线程,线程环境的切换的时间开销要远小于进程执行环境的切换。
2. 线程概念
进程用于实现在单进程环境中执行多个任务。一个进程中的所有线程都可以访问该进程的组成部件,如文件描述符和内存。
- 为每种事件类型分配单独的处理线程,可简化处理异步事件的代码。每个线程在进行事件处理时可以采用同步编程的方式。
- 多个进程必须使用操作系统提供的复杂机制才能够实现内存和文件描述符的共享。而多个线程自动地可以访问相同的存储地址空间和文件描述符。
- 有些问题可以分解从而提高整个程序的吞吐量。在只有一个控制线程的情况下,一个单线程进程要完成多个任务,只需要把这些任务串行化。但有多个控制线程时,相互独立的任务的处理就可以交叉进行,此时只需要为每个任务分配一个单独的线程。当然只有在两个任务的处理过程不相互依赖的情况下,两个任务才可以交叉进行。
- 交互的程序同样可以使用多线程来改善响应时间,多线程可以把程序中处理用户输入输出的部分和其他部分分开。
有些人把多线程的程序设计和多处理器或多核系统联系起来。但是即使程序运行在单处理器上,也能得到多线程编程模型的好处。处理器的数量并不影响程序结构,所以不管处理器的个数多少,程序都可以通过使用线程得以简化。而且,即使多线程程序在串行化任务时不得不阻塞,由于某些线程在阻塞时其他线程还有另外一些线程可以运行,所以多线程程序在单核处理器上运行还是可以改善响应时间和吞吐量的。
每个线程都包含有表示执行环境所必须的信息,其中包括进程中的标识线程的线程ID、一组寄存器的值,栈、调度优先级和策略、信号屏蔽字,errno
变量以及线程私有数据。一个进程的所有信息对该进程的所有线程都是共享的,包括正文段(可执行程序的代码),数据段(程序的全局内存),堆,栈以及文件描述符。
3. 线程标识
就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID仅在它所属进程的上下文中才有意义。
进程ID是由一个非负整数表示的,其数据类型为pid_t
。线程ID和进程ID不同,其数据类型为pthread_t
,具体的类型取决于实现,在Linux 3.2.0
上表现为无符号长整形,在Solaris 10
上表现为无符号整形,在FreeBSD 8.0
和Mac OS X 10.6.8
上表现为一个结构体指针。
由于可能存在由机构体实现的pthread_t
,所以需要一个函数来判断两个线程的线程ID的=是否相同。
#include <pthread.h>
int pthread_equal(pthread_t pid1, pthread_t pid2);
// 返回值,若相等返回非0数值,否则返回0;
线程可以通过调用pthread_self
来获取自身的线程ID。
pthread_t pthread_self();
// 调用线程的线程ID
Q:调用线程为主线程是如何?
当线程需要识别以线程ID作为标识的数据结构时,需要用到pthread_equal
,pthread_self
函数。例如,主线程进行任务分发,以任务中的线程ID作为标识分发到相同线程ID的线程中,此时,调用pthread_self
获取线程ID,调用pthread_equal
判断是否应分发到该线程。
4. 线程创建
在传统的UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程只包含一个线程是相同的。在POSIX线程(pthread
)的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个线程以前,程序的行为与传统的进程并没有什么区别。新增的线程可以通过调用pthread_create
函数创建。
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr,
void *(*start_run)(void *), void *restrict arg);
// 返回值:若成功返回0,若失败,返回错误编码。
当pthread_create
成功返回时,tidp
所指向的内存单元会存储创建线程的线程ID,attr
为创建的线程设置线程属性,start_run
为线程开始执行的函数入口(函数地址),arg
为线程入口函数所接收的传入参数,其类型为无类型指针参数void *
,表示当有多个参数数时,可以传入一个参数结构体。
线程创建时并不能保证是调用线程先执行还是被创建线程先执行。被创建线程可以访问进程的内存空间,继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。
注意,pthread_create
函数调用失败时,会返回错误码,而不会设置errno
,这与其他POSIX函数的一般行为不同。每个线程都提供errno
的副本,这仅是为了与现使用errno
的函数兼容,在线程中并不推荐使用errno
等其他全局变量,以免受其他线程的影响。
实例:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
void printids(const char *s) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid %lu tid %lu (0x%lx)\n",
s, (unsigned long)pid, (unsigned long)tid, (unsigned long)tid);
}
void *thr_fn(void *arg) {
printids("new thread: ");
return nullptr;
}
pthread_t ntid;
int main(void) {
int err = pthread_create(&ntid, nullptr, thr_fn, nullptr);
if(err != 0) {
printf("error code: %d\n", err);
}
printids("main thread: ");
sleep(1);
return 0;
}
两个不足之处:
- 需要处理主线程和新线程之间的竞争关系,因为主线程和新线程的调用顺序未知,有可能主线程调用
exit
退出进程时(或return),新线程并未执行完毕。主线程调用sleep
等待新线程执行完成。 - 新线程中,不能使用全局变量
n_tid
,虽然可以新线程可以访问进程内存空间,但是无法保证新线程访问此变量时,在主线程中pthread_create
已返回,n_tid
已被赋值。
5. 线程终止
如果进程中的任意线程调用了exit
, _Exit
或_exit
,那么整个进程就会终止。与此相类似,如果默认的动作是终止进程,那么。发送到线程的信号就会终止整个进程。
单个线程可以通过三种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
(1) 线程可以简单的从启动例程中返回,返回值是线程的退出码。
(2) 线程可以被同一进程中的其他线程取消。
(3) 线程调用pthread_exit
。
#include <pthread.h>
void pthread_exit(void *rval_ptr);
rval_ptr
是一个无类型指针,进程中的其他线程可以通过pthread_join
访问该值。
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回值:若成功返回0,若失败,返回错误码
pthread_join
的调用线程将一直阻塞,直到thread
指定的线程调用pthread_exit
。从启动例程中返回,或者被其他线程取消。如果线程简单的从它的启动例程中返回,rval_ptr
就包含返回码,线程主动调用pthread_exit
,rval_ptr
就由pthread_exit
设置,线程被其他线程取消,rval_ptr
所指向的内存单元就设置为PTHREAD_CANCELED
。
可以用pthread_join
将线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,那么
pthread_join
调用就会失败,返回EINVAL
,尽管这种行为是与具体实现有关的。
如果对线程的返回值不感兴趣,那么可以将rval_ptr
设为NULL。这这种情况下,调用pthread_join
可以等待线程结束,但不获取线程终止状态。
实例:获取线程返回码
#include <pthread.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
void * thread_return(void *) {
return (void *)2;
}
void * thread_exit(void *) {
char *str = new char[10];
strncpy(str, "Exit", 5);
pthread_exit(str);
}
int main() {
pthread_t return_tid;
if(pthread_create(&return_tid, nullptr, thread_return, nullptr) != 0) {
printf("thread_return fail");
exit(1);
}
void **return_rval = new (void *);
if(pthread_join(return_tid, return_rval) == 0) {
printf("thread_return return %ld\n", long(*return_rval));
}
pthread_t exit_tid;
if(pthread_create(&exit_tid, nullptr, thread_exit, nullptr) != 0) {
printf("thread_exit fail");
exit(1);
}
void **exit_rval = new (void *);
if(pthread_join(exit_tid, exit_rval) == 0) {
printf("thread_return exit %s\n", *exit_rval);
}
return 0;编号编号
}
可以看到当一个线程通过调用pthread_exit
或者从启动例程返回时,调用线程可以通过调用pthread_join
获得该线程的退出状态。
注意在被调用线程通过传递无类型指针的方式将返回值的地址通过pthread_join
返回到调用线程中,这意味着在调用线程访问该返回值地址时,该返回值应当仍处于自己的生命周期中。
线程通过调用pthread_cancel
来取消同一进程中的其他线程。
int pthread_cancel(pthread_t tid);
// 返回值:若成功返回0,若失败,返回错误码
默认情况下,pthread_cancel
的表现情况好像是调用线程以PTHREAD_CANCELED
为参数调用pthread_exit
函数。但时其实,线程可以决定在被取消时是否调用线程清理处理程序,线程清理处理程序类似于进程终止处理程序,注册的线程清理处理程序存储在栈中。
#include <pthread.h>
void pthread_cleanup_push(void *(*rtn)(void *), void *arg);
void pthread_clean_pop(int execute);
当线程
- 调用
pthread_exit
退出时 - 以非0参数调用
pthread_cleanup_pop
时 - 同进程其他线程对其调用
pthread_cancel
时,响应时
线程会由pthread_cleanup_push
调度执行线程清理处理程序。
pthread_cleanup_pop
中参数execute
为0时表示false
,即不执行线程清理处理程序。不管发生哪种情况,pthread_cleanup_pop
函数都会删除上次pthread_cleanup_push
建立的线程清理处理程序。
这些函数有一个问题,因为它们可以实现为宏,所以必须在与线程相同的作用于中以匹配对的形式使用。pthread_cleanup_push
的宏定义可能包含{
,所以pthread_cleanup_pop
的宏定义中要有对应的匹配字符。
进程原语 | 线程原语 | 描述 |
---|---|---|
fork | pthread_create | 创建新的控制流 |
exit | pthread_exit | 从控制流退出 |
waitpid | pthread_join | 从控制流中得到退出状态 |
atexit | pthread_cleanup_push | 注册在退出控制流时调用的函数 |
getpid | pthread_selt | 获得控制流的id |
abort | pthread_cancel | 从控制流异常退出 |
在默认情况下,线程的终止状态将一直保存到调用线程调用pthread_join
获取终止状态。如果线程已被分离,则线程在终止时,其底层存储资源将会被立即回收。在线程被分离后,调用pthread_join
会产生未定义行为。可以调用pthread_detach
分离线程。
#include <pthread.h>
int pthread_detach(pthread_t tid);
// 返回值:若成功,返回0,否则,返回错误编号
6. 线程同步
当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。
线程同步的本质上是因为线程在进程层面并行执行,不同线程间具有竞争关系,当线程持有共享变量时,只要线程试图修改变量,就需要线程同步。
对变量的读写是否为原子操作取决于对变量的修改时间是否多于一个存储器周期。若等于一个存储器周期,则变量写过程中不会发生其他线程的变量读写操作,所有线程的对变量的读写操作都是顺序进行的,不存在交叉操作。若多于一个存储器周期,则变量的写过程有可能会在其他控制线程发生对同一共享变量的读写操作,那么就有可能发生以下情况:
(1) 线程1从内存读取变量值至寄存器中;
(2) 线程1对变量值做运算;
(3) 线程2从内存读取变量值至寄存器中;
(4) 线程2对变量值做运算;
(5) 线程1将变量值写回内存;
(6) 线程2将变量值写回内存。
上述情况期望得到变量值在线程1和线程2上分别进行运算,得到运算结果之和。但是,实际上线程2并没有读到线程1写回的数据,导致线程1的写回数据被线程2的写回数据覆盖。
不显式的使用线程同步的方法,我们就无法确定线程在读写共享变量的操作是否发生交叉,导致读写操作不符合期望。因为读写操作并非是原子操作,在多线程环境下,就无法确定变量的最终值,因为线程操作间的竞争关系。
为了解决线程同步的问题,线程需使用锁,在线程对变量加锁期间,仅允许持有锁的线程访问该共享变量,这使的线程对变量的操作相对于其他线程来说变成了一个原子操作。
6.1 互斥量
可以使用pthread
的互斥接口来保护数据,确保同一时间只有一个线程访问数据。当线程期望访问数据时,先对互斥量进行设置(加锁),然后访问数据,在数据操作完成后,释放(解锁)互斥量。在线程对数据持有互斥量(加锁)期间,其它试图获取互斥量的线程都会阻塞,等待互斥量被释放。当互斥量被释放时,
其他被阻塞线程恢复运行状态,并试图获得互斥量,第一个尝试获得互斥量的线程获得互斥量,其他线程被阻塞。在这种情况下,每次只有一个线程向前执行。
只有将所有线程设计成满足相同的数据访问方式,即所有线程对共享数据进行访问前都必须获得互斥量(加锁),在访问结束后,都需要释放互斥量(解锁),使得同一时间,仅有唯一线程可以访问共享数据。
互斥量是用pthread_mutex_t
数据类型表示的。在使用互斥变量以前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER
(只适用于静态分配的互斥量),也可以通过调用pthread_mutex_init
函数进行初始化。如果动态分配互斥量(例如,通过调用malloc
函数),在释放内存前需要调用pthread_mutex_destory
。
#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;否则,返回错误编号
要使用默认的属性初始化互斥量,仅需将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,否则,尝试锁住互斥量失败,不阻塞直接返回,返回值为EBUSY
。
实例:
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
};
foo * foo_alloc(int id) {
foo *fp;
if((fp = (foo *)malloc(sizeof(foo))) == NULL) {
return NULL;
}
fp->f_count = 0;
pthread_mutex_init(&fp->f_lock, NULL);
fp->f_id = id;
return fp;
}
void *foo_hold(void *ptr) {
foo *fp = (foo *)ptr;
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
printf("f_count++ = %d.\n", fp->f_count);
pthread_mutex_unlock(&fp->f_lock);
return nullptr;
}
void *foo_rela(void *ptr) {
foo *fp = (foo *)ptr;
pthread_mutex_lock(&fp->f_lock);
fp->f_count--;
printf("f_count-- = %d.\n", fp->f_count);
pthread_mutex_unlock(&fp->f_lock);
if(fp->f_count == 0) {
pthread_mutex_destroy(&fp->f_lock);
free(fp);
}
return nullptr;
}
int main() {
foo *fp = foo_alloc(1);
pthread_t h_tid;
pthread_create(&h_tid, NULL, foo_hold, fp);
pthread_t r_tid;
pthread_create(&r_tid, NULL, foo_rela, fp);
pthread_join(h_tid, NULL);
pthread_join(r_tid, NULL);
return 0;
}
6.2 避免死锁
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。如果两个线程以不一致的顺序同时对多个互斥量加锁,且锁住的变量在两个线程间需要同步,则可能会造成死锁。例如,A线程需要锁住互斥量b_lock以访问b_data,B线程需啊摇锁住互斥量a_lock,以访问a_data。但是b_lock被线程B锁住,a_lock被线程A锁住,对于A线程需要等待B线程释放b_lock,对于B线程需要等待A线程释放a_lock,但是A,B线程分别阻塞,等待b_lock和a_lock的释放。
6.3 函数pthread_mutex_timelock
当线程试图获取一个已加锁的互斥量时,pthread_mutex_timelock
互斥量原语允许绑定线程阻塞时间。当在阻塞超时后,pthread_mutex_timelock
不再阻塞,直接返回错误码ETIMEDOUT
。
#include <pthread.h>
#include <time.h>
int pthread_mutex_timelock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
// 返回值:若成功,返回0;否则,返回错误编号
6.4 读写锁
读写锁较互斥量有更高的并行性,互斥量只有两种状态(锁住,未锁住状态),而读写锁有三种状态:
- 读模式下加锁状态:读模式下加锁状态允许多条线程以读模式同时访问数据,此模式下,会阻塞写模式的加锁请求,直到所用的读模式锁全部被释放,该线程才会以写模式对读写锁加锁。通常,在阻塞至少一个写模式下加锁请求后,随后的读模式加锁请求也会被阻塞,这是为了避免写模式加锁请求长时间被阻塞。
- 写模式下加锁状态:写模式下加锁状态仅允许唯一线程持有读写锁,阻塞其他任何加锁请求。
- 不加锁状态:等待对读写锁加锁请求。
读写锁又叫共享互斥锁(shared-exclusiye lock
),在读模式下允许线程共享锁,在写模式下,线程间加锁互斥。
与互斥量相比,读写锁在使用前必须初始化,在释放它们底层的内存之前必须销毁。
#include <pthread.h>
int pthread_rwlock_int(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号。
读写锁通过调用pthread_rwlock_init
进行初始化,如果希望使用默认属性初始化读写锁,将attr
设为NULL
。
Single UNIX Specification 在XSI拓展中定义了PTHREAD_RWLOCK_INITIALIZER
常量,如果默认属性足够使用的话,可以用它初始化静态分配的读写锁。
在释放读写锁占用的内存之前,应当调用pthread_rwlock_destroy
做清理工作。如果pthread_rwlock_init
为读写锁分配了资源,pthread_rwlock_destroy
将释放这些资源。如果在调用pthread_rwlock_destroy
之前就释放了读写锁占用的内存,那么分配给这个锁的资源就会丢失。
读写模式锁定和解锁:
#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,否则,返回错误编码
尝试加锁,成功返回0,读写锁被其他线程占用,返回EBUSY
。
6.5 带有超时的读写锁
#include <pthread.h>
int pthread_rwlock_timerdlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr);
int pthread_rwlock_timewrlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr);
// 两个函数的返回值:若成功,返回0,若失败返回错误编号。
在时限tsptr
内,未能获得锁的所有权,则返回ETIMEDOUT
。
6.6 条件变量
条件变量是线程可用的另一种同步机制。条件变量给多个线程提供一个会合的场所。条件变量和互斥变量一起使用时,允许线程以无竞争的方式等待某特定条件发生。
互斥变量保证多线程拥有相同的共享数据视图,以保证线程如期望串行处理共享数据。
条件变量提供一种机制,使得线程间能进行交互,协作处理任务。
条件变量使用前需要进行初始化:
- 静态分配:使用
PTHREAD_COND_INITIALIZER
初始化静态条件变量。 - 动态分配:使用
pthread_cond_init
对动态分配的条件变量进行初始化。
在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy
函数对条件变量进行反初始化(deinitialize
)。
int pthread_cond_int(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cont_t *restrict cond);
// 两个函数的返回值:若成功,返回0,否则,返回错误编号
pthread_cond_int
的 attr
参数设置为NULL
,将以默认的条件变量属性初始化cond
。
线程使用下列函数等待条件变量:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timewait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
// 两个函数的返回值:若成功,返回0,若失败返回错误码,若超时,返回`ETIMEDOUT`
其中,cond
为调用线程等待其改变的条件变量,mutex
是用于给条件变量加锁的互斥变量。线程首先获得mutex
的持有权,然后调用pthread_cond_wait
将调用线程放到等待条件变量cond
唤醒的线程列表上,然后释放互斥变量mutex
,线程进入阻塞等待状态。之所以要先获得mutex
的所有权,是为了对条件变量cond
加锁,当调用线程持有cond
时,其他线程无法获得cond
的所有权,无法修改cond
的状态,这样就关闭了条件检查与线程进入休眠等待条件改变这两个操作间的时间通道,线程就不会错过任何的条件改变。当从pthread_cond_wait
返回时,调用线程再次获得mutex
的持有权,互斥量再次被锁住。
tsptr
是绝对时间,不是相对时间,例如,需要等待3分钟,则将tsptr
设置为当前时间加上3分钟。
如果超时到期时,条件还是没有出现,pthread_cond_timewait
将重新为互斥量加锁,然后返回错误ETIMEDOUT
。从pthread_cond_wait
或者pthread_cond_timewait
调用成功返回时,线程需要重新计算条件,因为另一个线程可能已经在运行并改变了条件。
当有多个等待线程将被唤醒,第一个被唤醒的线程获得互斥量所有权,继续运行,当其释放互斥量时,下一个等待线程被唤醒,但由于早先被唤醒的线程可能修改了条件,所以需要再进行条件检查,而由于第一个唤醒线程有内核调度,我们无法确定,所以所有线程都需要进行条件检查。
有两个函数用于通知线程条件已经满足。pthread_cond_signal
函数至少唤醒一个等待该条件的线程,而pthread_cond_broadcast
函数则能唤醒等待该条件的所有线程。
POSIX 规范为了简化
pthread_cond_signal
的实现,允许它在实现的时侯唤醒一个以上的线程。
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// 两个函数的返回值:若成功,返回0;若失败,返回错误编码。
6.7 自旋锁
自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。
6.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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
初始化屏障时,可以使用count
参数指定,在允许所有线程继续运行之前,必须到达屏障的线程数目。使用attr
参数指定屏障对象的属性。设置attr
为NULL,用默认属性初始化屏障。如果使用pthread_barrier_init
函数为屏障分配资源,那么在反初始化屏障可以调用pthread_barrier_destroy
函数释放相应的资源。
可以使用pthread_barrier_wait
函数来表明,线程已经完成工作,准备等待其他所有线程赶上来。
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
// 返回值:若成功,返回0或者`PTHREAD_BARRIER_SERIAL_THREAD`;否则,返回错误编号。
调用pthread_barrier_wait
的线程在屏障计数未满足条件时会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait
的线程,就满足了屏障计数,所有休眠线程都将被唤醒。
对于一个任意线程,pthread_barrier_wait
函数返回了PTHREAD_BARRIER_SERIAL_THREAD
。剩下的线程返回值都是0。这使一个线程可以作为主线程,它可以工作在其他所有线程已完成工作的基础上。
一旦达到屏障计数值,而且线程处于非阻塞状态,屏障就可以被重用。但是除非在调用了pthread_barrier_destroy
函数之后,又调用pthread_barrier_init
函数对计数用另外的数进行初始化,否则屏障计数不会改变。