在 *nix 上写 multi-thread 程序,通常会用到 pthreads。

1. 预备

开发是需要在源代码前加上 #include <pthread.h>。链接时需要加上 -lpthread

2. 创建与取消 thread

每一个 thread 都有一个 thread ID。在 C/C++ 中,thread ID 用 pthread_t 表示。

可使用 pthread_create() 创建新进程。

 int pthread_create(pthread_t *thread, 
                    const pthread_attr_t *attr,
                    void *(*start_routine) (void *), 
                    void *arg);

其中,thread 是 thread ID。Thread 在创建后会开始运行 start_routinearg 是传递给 start_routine 的数据。

attr 定义了 thread 的属性。在 Linux 中通常在将 thread 设置为 detached 的时候才会用到 attr。Thread 默认为 joinable,运行完后自动退出。但是,detached thread 运行结束后会挂起,等待 main thread 用 pthread_join() 获取其返回值。

创建 attr 需要用到如下函数:

int pthread_attr_init(pthread_attr_t *attr);

可以用如下函数设置 detach state。

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

detachstate 可以为 PTHREAD_CREATE_DETACHED 或者 PTHREAD_CREATE_JOINABLE

pthread_attr_t 在 thread 创建完后即可销毁。

int pthread_attr_destroy(pthread_attr_t *attr);

如果需要 main thread 等待某一个子 thread。可以使用 pthread_join()

int pthread_join(pthread_t thread, void **retval);

retval 指向 start_routine 运行完后的返回值。

Thread 在运行中随时可以调用 pthread_exit() 退出。某个 thread 也可以调用 pthread_cancel() 取消其它 thread 的运行。有的 thread 可被取消,有的 thread 不可被取消,分为三种情况。

  • Asynchronously cancelable: 可被取消
  • Synchronously cancelable: 只能在特定位置被取消(默认)
  • Uncancelable: 不可被取消

可用如下函数设置 thread 是否可被取消。

int pthread_setcancelstate(int state, int *oldstate)

state 取值为 PTHREAD_CANCEL_ENABLE 或者 PTHREAD_CANCEL_DISABLE

可用如下函数设置 thread 的取消方式。

int pthread_setcanceltype(int type, int *oldtype)

typePTHREAD_CANCEL_DEFERRED 表示 thread 为 synchronously cancelable,只在用 pthread_testcancel() 标记的 cancellation point 处才可被取消。typePTHREAD_CANCEL_ASYNCHRONOUS 是表示 thread 可在任意时刻被取消。

3. Cleanup

线程可能会因为 pthread_exit() 或者 pthread_cancel() 而退出,此时需要对资源进行 cleanup 以避免资源泄漏。因此,pthreads 提供了 cleanup handler。

可用如下函数注册 cleanup handler。

void pthread_cleanup_push(void (*routine)(void *), void *arg);

如果线程正常结束,可以从 stack 中弹出 cleanup handler。

void pthread_cleanup_pop(int execute);

如果 execute 为非零值,此函数会隐式同时调用 routine

4. thread 间的同步

Thread 间需要一定的同步措施以避免 race condition,也即,因为多个 threads 同时访问一样的数据而造成 bug。

4.1 Mutex

Mutex 是互斥锁的缩写。对于一个 mutex,在某个时刻,只有一个 thread 可以获得它,否则只能等待。

可用如下函数创建 mutex。

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                       const pthread_mutexattr_t *restrict attr);

attr 可设为 NULL,此时会按照默认方式创建 mutex。

可用如下两个函数获得或者释放 mutex。

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

若在无法获得 mutex 时不想让 thread 等待,可使用如下函数

int pthread_mutex_trylock(pthread_mutex_t *mutex);

Mutex 默认为 fast mutex。若同一线程试图两次 lock 同一个 mutex,则会陷入 deadlock。

还有一种 deadlock。若 thread A 获得了 mutex A 并请求 mutex B,而thread B获得了 mutex B 并请求 mutex A,也会发生 deadlock。因此,若多个 threads 都需要获得同一组 mutexes,需要按照相同的顺序调用 pthread_mutex_lock()

4.2 Semaphore

Semaphore 可用于多个 threads 处理同一个队列的情形。使用 Semaphore 前,需要在 C/C++ 代码中加入 #include <semaphore.h> 并用如下函数初始化一个 sem_t 类型的变量:

int sem_init(sem_t *sem, int pshared, unsigned int value);

pshared 若为 0,则表示该 semaphore 只用于当前 thread。value 则是该 semaphore 的初始值,通常为 0。

对 semaphore,有两种操作,分别是 wait 和 post。函数如下:

int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);

sem_post() 会使 semaphore 的值加一。sem_wait() 会在 semaphore 为 0 时等待,否则会使 semaphore 的值减 1。因此,在 post 时,如果有多个 threads 同时 wait,那么会有一个 thread 被 unblock。Semaphore 通常和 mutex 组合使用。用 ` sem_getvalue()` 可以获得 semaphore 的当前值。

此外,还有 sem_trywait() ,其作用和上面的 pthread_mutex_trylock() 类似。

4.3 Conditional Variable

Semaphore 只是一个计数器,而利用 conditional variable,可以写出更复杂的同步逻辑。Contitional variable 用 pthread_cond_t 类型的变量表示,变量使用前需要用 pthread_cond_init() 函数初始化,同时,还需要搭配一个 mutex。pthread_cond_wait() 可以使当前 thread 被 block,然后当该 conditional variable 被另一个线程 pthread_cond_signal() 的时候,某一个正在 wait 的 thread 就会被唤醒。如果使用 pthread_cond_broadcast() 则所有正在 wait 的thread 都会被唤醒。Conditional variable 需要和自定义的一个 flag 变量搭配使用。例如,当 flag 被 set 的时候,处理一些任务,否则继续 wait。而且,需要对该 flag 变量使用 mutex。在 wait 的同时,需要 unblock 这个 mutex,否则就会发生 race condition,导致 signal 丢失。因此,使用 pthread_cond_wait() 是,需要同时传入一个 mutex 参数,pthreads 库会保证 mutex 的 unlock 和 wait 是同时的。

4.4 Lock-free

这个部分比较复杂,根据我在 StackOverflow 上看到的一些回答,似乎是说在创建对象的开销较小的时候,使用 lock-free data structure 相比 mutex 效率更高。实现 lock-free 需要用到 atomic operation,gcc 提供了相关的编译器扩展。这部分我尚未学习,日后学完会回来填坑。

EOF