Linux 内核只为线程的支持提供了底层原语,比如 clone() 系统调用。多线程库在用户空间。POSIX(Portable Operating System Interface,可移植操作系统接口)对线程库进行了标准化。开发人员称之为 POSIX 线程,简称为 Pthreads。

Pthread API 提供了非常多的接口,但是对于开发人员,需要熟练掌握的主要有下面三类:

  • 线程的创建、等待及结束。
  • Mutex 的创建、销毁、加锁、解锁。
  • 条件变量的创建、销毁、等待、通知、广播。

Anyway,一切以 man page 为准。

一、线程

Pthread_create

1.函数原型

#include <pthread.h>

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

2.参数说明

  • pthread_t *thread: 值-结果参数。调用成功,被赋值为 线程 ID。调用失败,结果未定义。
  • pthread_attr_t *attr: 线程创建时的属性信息,通过 pthread_attr_init(3) 来初始化。传入NULL代表使用默认属性。
  • void (start_routine) (void*) : 线程启动后将执行 start_routine.
  • void *arg: 将作为 start_routine 的入参传入。
  • 调用成功将返回 0,否则,会返回一个错误码。

3.More

  • 调用失败的原因可能有:资源不足、达到线程最大数目、 传入属性错误等。
  • pthread_t 结构灭有明确规定是哪种类型,只应该通过 pthread 函数使用。
  • thread id 只保证在单进程内唯一。线程销毁后,其ID可能被重复使用,这个ID是由Pthread库来分配的,可以通过pthread_self()来获取。
  • TID 是通过 gettid()系统调用来获取的,对于单线程而言TID = PID(通过 getpid()来获取),对于多线程而言,所有线程有同一个PID,但是TID都是唯一的。
  • 线程终止可能有如下四种可能:1. 主动调用 pthread_exit; 2. 从 start_routine() 中返回; 3. 通过 pthread_cancel 被取消了; 4. 进程中任一线程调用了 exit(),所有线程终止。

Pthread_join

1.函数原型

#include <pthread.h>

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

pthread_join 等待由 thread 参数指定的线程终止。如果该线程已经终止了,该函数将立即返回。

2.参数说明

  • pthread_t thread: 指定被join的线程(必须是 joinable)
  • void **retval:值-结果参数,如果非 NULL,会复制线程的退出状态到该变量上。
  • 调用成功返回0,否则返回一个错误码。

3.More

  • 多个线程同时join一个线程,结果未定义。
  • 错误码可能是:死锁、非joinable线程、已被join、未找到线程。
  • 没有join一个joinable线程,将会产生僵尸线程(zombie thread)。这种行为应该被避免。

Pthread_detach

1.函数原型

#include <pthread.h>

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

2.参数说明

  • pthread_t thread: 指定需detach的线程
  • 调用成功返回0,否则返回错误码。

3.More

  • 试图detach一个已经是detach状态的线程,结果未定义。
  • 错误码可能是:非joinable线程、未找到线程
  • detach只是指定线程终止后,系统对该线程资源的行为。进程终止了,该线程依旧会终止。

二、Mutex

Pthread_mutex_init / Pthread_mutex_destory

1.函数原型

#include <pthread.h>


int pthread_mutex_destroy(pthread_mutex_t *mutex);

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

2.参数说明

  • pthread_mutex_t *restrict mutex:初始化/销毁的 mutex对象
  • pthread_mutexattr_t *restrict attr:mutex对象的属性,传入NULL代表默认初始化
  • PTHREAD_MUTEX_INITIALIZER:等同于传入NULL attr 的pthread_mutex_init
  • 调用成功返回0,否则返回错误码

3.More

  • 被 pthread_mutex_destory 销毁的 mutex 可以由 pthread_mutex_init 重新初始化。
  • destroy 一个locked 或者被引用(如pthread_cond_wait)的mutex,结果未定义。
  • 对 mutex 拷贝的操作(lock/unlock..)结果未定义。
  • 对已初始化的mutex进行初始化结果未定义。

Pthread_mutex_lock / Pthread_mutex_unlock

1.函数原型

#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);

2.参数说明

  • pthread_mutex_t *mutex: 操作的 mutex 对象
  • 调用成功返回0,否则返回错误码

3.More

  • 同一个线程对一个已lock的Mutex,再次lock,结果依赖于mutex type
  • 一个线程对一个已unlock的Mutex,再次unlock,结果依赖于mutex type,具体参考man
  • trylock会在该Mutex已经lock的情况,立即返回
  • 如果多个线程阻塞在Mutex 的Lock上,具体哪个线程获得 Lock 由系统调度策略来决定

三、Cond

Pthread_cond_init / Pthread_cond_destroy

1.函数原型

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);

int pthread_cond_init(pthread_cond_t *restrict cond,
	 const pthread_condattr_t *restrict attr);
	 
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2.More

  • destory一个正在block其他线程的cv,结果未定义
  • 拷贝cv,产生的结果未定义
  • 重复init一个已init的cv,结果未定义

Pthread_cond_wait / Pthread_cond_timedwait

1.函数原型

#include <pthread.h>

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
	 pthread_mutex_t *restrict mutex,
	 const struct timespec *restrict abstime);
	 
int pthread_cond_wait(pthread_cond_t *restrict cond,
	 pthread_mutex_t *restrict mutex);

2.More

  • timewait / wait 将会block在cv上,代码需要确保传入的mutex由调用线程 locked,否则发生错误或者结果未定义。
  • wait 是原子性地释放 Mutex,并使当前线程在cv上阻塞。具体:如果B线程能够获取到由A线程释放的锁,那么B线程随后的signal/boardcast调用的表现行为就像是在A线程阻塞之后发出的。什么意思呢?意思就是原子性保证了signal/boardcast的调用一定表现上像是唤醒了在该线程之前获取mutex的线程的cv。如果不是原子的话,就会出现,B线程是否lock,A线程获取lock,唤醒CV,但是此时B线程未阻塞在CV上,无法唤醒。(TODO:关于为什么总和mutex关联在一起,知乎有个回答,review下)
  • 如果成功返回,mutex将会处于lock状态返回给当前线程
  • 通常来讲使用cv涉及到一个Bool谓词来判断这个cv所关联的等待条件是否满足。但是虚假唤醒不可避免,所以当wait返回时,需要再次判断是否满足条件:意思就是得有个while循环内来使用cv,而不是if。(TODO:为什么虚假唤醒不可避免)
  • 这一段有点复杂,大概意思是一个cv和一个mutex绑定。。。
  • 如果一个信号被发给一个阻塞在cv上的线程,当信号handler处理完毕后,线程将会恢复在等待cv上,或者因虚假唤醒而返回0.
  • 有人建议将互斥量的获取和释放与状态等待分开。这被拒绝了,因为这是操作的组合性质,事实上,这有助于实时实现。那些实现可以以对调用者透明的方式原子地移动条件变量和互斥锁之间的高优先级线程。这可以防止额外的上下文切换,并在等待线程发送信号时提供更多确定性的互斥量采集。因此,公平性和优先级问题可以由调度规则直接处理。此外,当前的条件等待操作符合现有的做法。(TODO)
  • 由调度策略来决定哪个线程会被唤醒,而不是某种固定的顺序

Pthread_cond_broadcast/Pthread_cond_signal

1.函数原型

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

2.More

  • broadcast 会唤醒所有阻塞在cv上的线程;signal会唤醒至少一个线程。
  • 由调度机制来决定唤醒顺序。
  • 线程没有获取到wait的lock,也可以使用broadcast/signal进行唤醒操作,不过如果需要可预测的调度行为,还是使用锁吧
  • 一种可能的实现:

      // wait
     pthread_cond_wait(mutex, cond):
     	   value = cond->value; /* 1 */
         pthread_mutex_unlock(mutex); /* 2 */
         pthread_mutex_lock(cond->mutex); /* 10 */
         if (value == cond->value) { /* 11 */
             me->next_cond = cond->waiter;
             cond->waiter = me;
             pthread_mutex_unlock(cond->mutex);
             unable_to_run(me);
          } else
             pthread_mutex_unlock(cond->mutex); /* 12 */
          pthread_mutex_lock(mutex); /* 13 */
     
      // signal
      pthread_cond_signal(cond):
          pthread_mutex_lock(cond->mutex); /* 3 */
          cond->value++; /* 4 */
          if (cond->waiter) { /* 5 */
              sleeper = cond->waiter; /* 6 */
              cond->waiter = sleeper->next_cond; /* 7 */
              able_to_run(sleeper); /* 8 */
          }
          pthread_mutex_unlock(cond->mutex); /* 9 */