POSIX 线程,常被缩写为 Pthread,是 POSIX 的线程标准,定义了与线程相关的一套 API。

POSIX 指的是 IEEE 发布的 UNIX 系统接口标准,标准提供了软件开发需要的通用操作系统 API,按照此标准开发的软件就可以方便地在不同的 UNIX 系统之间进行代码移植,所以称为 Portable

Linux 是 GNU 项目下基于 POSIX 标准开发的完全自由且兼容 UNIX 的操作系统,也是今天发展最为活跃的类 UNIX 操作系统。

关于 UNIX、GNU 和 Linux 有很多故事,这里不一一展开。

线程创建

使用 pthread_create 函数创建线程,并运行 start_routine 函数。

#include <pthread.h>
int pthread_create (pthread_t *thread,
                    pthread_attr_t *attr,
                    void *(*start_routine)(void *),
                    void *arg)

线程终止

线程在下面几种情况下会终止:

#include <pthread.h>
void pthread_exit(void *retval);

pthread_exit 函数终止调用线程,并将返回值放入 retval,其他线程可以通过 retval 获得线程返回状态。

如果在主函数(主线程)中调用该函数,那么主线程将会结束,但进程不会结束。当进程中的其他线程都结束时,进程才会结束。

#include <pthread.h>
int pthread_cancel(pthread_t thread);

一个进程中的线程可以通过调用 pthread_cancel,来请求结束另一个线程。该函数参数为要结束的线程的标识符。如果结束成功返回 0,否则返回错误代码。

线程阻塞和分离

阻塞

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

一个线程通过调用 pthread_join 函数等待 thread 线程结束,这个过程称为阻塞( join )。thread 线程运行结束后,继续当前线程。通过 retval 获得另一个线程的返回值。被阻塞的 thread 线程结束后,其所占用的资源将被释放。

分离

#include <pthread.h>
int pthread_detach(pthread_t thread);

调用 pthread_join 后,当前线程将等待被阻塞线程。使用 pthread_detach 函数可以使当前线程不等待目标线程而继续后续任务,并且在目标线程结束后,目标线程的资源将会自动释放。

线程属性

线程创建时,可以通过 pthread_attr_t 结构来设置线程属性,其中就有与阻塞和分离相关的属性。

pthread_attr_t attr;
//初始化并设置 detached 属性
//可设置为 PTHREAD_CREATE_DETACHED 属性
//或 PTHREAD_CREATE_JOINABLE 属性
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
pthread_create(thread, &attr, foo, (void *)arg);
//释放
pthread_attr_destroy(&attr);

当线程需要阻塞时,考虑显式设置其可阻塞属性。因为并非所有情况下,线程都默认被设置为可阻塞。

线程资源释放

线程运行完毕后,就需要考虑资源释放。使用 pthread_create 创建线程后,线程会立即开始运行。一般地,线程与线程之间不会通信,线程运行结束后不会将资源释放,而是仍由其所在的进程持有。如果不使用 pthread_joinpthread_detach 设置线程,那么线程在结束后不会会释放资源。

互斥锁

多线程编程时,多个线程常常需要访问同一个数据源,为确保访问的数据有效,必须使用锁。互斥锁( Mutex )可以用来保护被多个线程访问的资源,防止多个线程同时更新一个数据时出现同步错误。

使用互斥锁的一般步骤是:

  1. 创建互斥锁。
  2. 多个线程尝试锁定互斥锁。
  3. 成功锁定互斥锁的线程成为互斥锁的拥有者,并在这之后执行一些代码。
  4. 互斥锁拥有者解锁。
  5. 其他线程尝试锁定互斥锁,并重复 3-5 步。
  6. 不再需要互斥锁时释放互斥锁资源。

初始化互斥锁有两种方式:

  1. 使用常量 PTHREAD_MUTEX_INITIALIZER 来初始化互斥锁变量。
    • 例如 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    • 定义和初始化需要写在一起,不能分开。
  2. 使用函数 pthread_mutex_init(mutex,attr) 初始化互斥锁变量,这种方式可以设置互斥锁属性。
    • 互斥锁属性初始化函数 pthread_mutexattr_init (attr)
    • 互斥锁属性释放函数 pthread_mutexattr_destroy (attr)

互斥锁初始化后,默认为解锁状态。当不再需要互斥锁时,使用 pthread_mutex_destroy (pthread_mutex_t *mutex) 释放互斥锁资源。

线程可以使用三种方式尝试锁定互斥锁:

  1. pthread_mutex_lock(mutex)。当互斥锁已经被其他线程锁定时,该函数会阻塞,直到互斥锁被解锁。
  2. pthread_mutex_trylock(mutex)。当互斥锁已经被其他线程锁定时,该函数会立即返回一个表示“忙碌 busy”的错误码。

线程执行完需要的代码后,需要使用 pthread_mutex_unlock (mutex) 函数解锁。

互斥锁的使用没有什么特别的地方,实践中更像是线程间的“君子协定”。程序员需要保证所有访问同一数据源的线程都使用了互斥锁机制,否则可能产生非预期结果。

条件变量

互斥锁通过控制线程访问数据的方式来实现同步,而条件变量能够在特定条件下进行数据同步。在使用互斥锁时,没有获得锁的线程会持续查询锁的状态,这种方式显然会浪费 CPU 资源。通过使用条件变量,一个线程可以在数据达到某个条件时通知另一个线程。

条件变量需要与互斥锁同时使用,使用条件变量的同时也需要初始化互斥锁。

初始化条件变量有两种方式:

  1. 使用常量初始化条件变量。 例如: pthread_cond_t myconvar = PTHREAD_COND_INITIALIZER;
  2. 使用函数 pthread_cond_init(cond,attr) 初始化条件变量,这种方式可以设置条件变量属性。
    • 条件变量属性初始化函数 pthread_condattr_init (attr)
    • 条件变量属性释放函数 pthread_condattr_destroy (attr)

释放条件变量函数:

使用函数 pthread_cond_wait (condition,mutex) 来等待条件变量成立的通知。这个函数需要在已经获得互斥锁的前提下使用,所以在使用这个函数前必须有一个 pthread_mutex_lock(mutex) 存在。

条件变量使用的一般步骤:

  1. 线程 t 通过 pthread_mutex_lock(mutex) 获得互斥锁。
  2. 线程 t 调用 pthread_cond_wait(condition,mutex) 等待条件成立。
  3. 条件成立通知尚未到来时,pthread_cond_wait(condition,mutex) 函数将会阻塞,同时自动解锁互斥锁。
  4. 其他线程获得互斥锁。
  5. 其他线程通过 pthread_cond_signal(condition) 函数发送条件成立通知,随后使用 pthread_mutex_unlock(mutex) 函数解锁互斥锁。
  6. 线程 t 收到条件变量通知被唤醒,同时自动锁定互斥锁。
  7. 线程 t 执行后续代码,完成后使用 pthread_mutex_unlock(mutex) 函数解锁互斥锁。

在使用条件变量时,线程 t 往往需要和其他线程访问同一数据。但在开始时该数据尚未达到线程 t 的执行条件,在判断这个执行条件时,推荐使用 while 循环而非 if 以避免下面的问题。

  • 当有多个线程等待条件变量时,多个等待线程会先后获得互斥锁,一些线程可能会修改共同访问的数据使得条件判断条件不成立,所以在获得通知后要用 while 再进行一次判断。
  • 线程接收条件通知时可能会出现错误,使用 while 能再次尝试。

如果需要通知多个处于等待条件状态的线程,可以使用 pthread_cond_broadcast(condition)

未完待续。

参考链接