C++多线程编程

Author Avatar
go3k 8月 13, 2014

背景

线程可以支持同一个应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务间通信也更简单。在C++里面,对多线程的支持由具体操作系统提供的函数接口支持。不同的系统中具体实现方法不同。

线程状态

在一个线程的生存期内,可以在多种状态之间转换。不同操作系统可以实现不同的线程模型,定义许多不同的线程状态,每个状态还可以包含多个子状态。但大体说来,如下几种状态是通用的:

  • 就绪:参与调度,等待被执行。一旦被调度选中,立即开始执行。
  • 运行:占用CPU,正在运行中。
  • 休眠:暂不参与调度,等待特定事件发生。
  • 中止:已经运行完毕,等待回收线程资源。

线程、进程资源

线程存在于进程之中。进程内所有全局资源对于内部每个线程均是可见的。进程内典型全局资源有如下几种:

  1. 代码区;这意味着当前进程空间内所有可见的函数代码,对于每个线程来说也是可见的。
  2. 静态存储区。
  3. 全局变量。
  4. 静态变量。
  5. 动态存储区;也就是堆空间。

线程内典型的局部资源有:

  1. 本地栈空间;存放本线程的函数调用栈,函数内部的局部变量等。
  2. 部分寄存器变量;例如本线程下一步要执行代码的指针偏移量。

创建线程

一个进程发起之后,会首先生成一个缺省的线程,通常称这个线程为主线程。C/C++程序中主线程就是通过main函数进入的线程。由主线程衍生的线程称为从线程,从线程也可以有自己的入口函数,作用相当于主线程的main函数, 这个函数由用户指定。

就像main函数有固定的格式要求一样,线程的入口函数一般也有固定的格式要求,参数通常都是void 类型,返回类型在
pthread中是void
, winapi中是unsigned int,而且都需要是全局函数。

pthread

POSIX thread,简称为pthread,是POSIX标准线程,其具有良好的可移植性。

主要接口

  • pthread_create

    int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void), void *restrict arg);
    
    • 参数1:指向线程标识符指针。
    • 参数2:线程属性。
    • 参数3:线程运行函数起始地址。
    • 参数4:运行函数的参数。
    • 创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。
  • pthread_exit():终止当前线程
  • pthread_cancel():中断另外一个线程的运行
  • pthread_join():阻塞当前的线程,直到另外一个线程运行结束
  • pthread_attr_init():初始化线程的属性
  • pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
  • pthread_attr_getdetachstate():获取脱离状态的属性
  • pthread_attr_destroy():删除线程的属性
  • pthread_kill():向线程发送一个信号

与主线程的关系

主线程和子线程的默认关系是:

无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。

需要强调的是,线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态(请回顾上面说的线程状态),但千万要记住的是,进入终止态后,为线程分配的系统资源并不一定已经释放,而且可能在系统重启之前,一直都不能释放。终止态的线程,仍旧作为一个线程实体存在与操作系统中。

主线程和子线程之间通常定义两种关系:

  1. 可会合(joinable);这种关系下,主线程需要明确执行等待操作。在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合。这时主线程继续执行等待操作之后的下一步操作。
  2. 相分离(detached);这表示子线程无需和主线程会合,也就是相分离的。这种情况下,子线程一旦进入终止态,系统立即销毁线程,回收资源。

线程间同步

Mutex锁

Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。

Spin锁

Spin lock它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

参考链接:

1. C++多线程编程入门

2. pthread线程库笔记

3. Mutex与spin对比

其它精彩文章:

并行程序设计原理

Pthread: POSIX多线程程序设计