4.1 线程的简单介绍与同步方法
线程的简单介绍与线程同步方法
线程介绍
这里的线程主要为 POSIX 线程,即 Pthreads,由 Native POSIX Threads Library(NPTL)实现。 线程是允许应用程序并发执行多个任务的一种机制,一个进程可以包含多个线程,线程可以共享同一份全局内存区域。 使用多进程实现并行有以下不足:
进程间的信息难以共享,必须采用进程间通信(IPC)方式
调用 fork 创建进程的代价比较高
线程完美的解决了上面两个问题,线程还可共享:
进程 ID,父进程 ID
进程组 ID 与会话 ID
打开的文件描述符
信号处置
各个线程的独有属性有:
线程 ID
信号掩码
线程特有数据
erron 变量,是一个宏
栈
后续 Pthreads 系列函数均以 0 表示成功,返回一个正值表示失败,这个正值与传统 Unix 的 errno 的值含义一样。在编译调用了 Pthreads 函数的程序需要添加 -lpthread
以链接此库。
创建线程
程序刚启动时,只包含一条线程,称为主线程。
创建新线程后,系统会优先调度哪一个是不确定的,如果对执行顺序有明确要求可采取一些同步技术(见后续的线程同步小节)。
终止线程
可采用以下方式终止线程:
线程 start 函数执行到 return 语句
线程调用 pthread_exit
调用 pthread_cancel 取消线程
任意线程调用了 exit,或者主线程(main)执行了 return
线程 ID
线程 ID 是线程的唯一标识,pthread_create 可返回线程 ID,线程也可获取自己的线程 ID:
不能直接使用 == 运算符判断两个线程 ID 是否相等,因为其底层实现可能是无符号长整型、指针、结构:
但 NPTL 中,它实际上是一个经强制转化为无符号长整形的指针。 在 Linux 中线程 ID 在所有进程中都是唯一的,不过其它 Unix 实现则不一定。线程 ID 可复用,当某个线程终止后,其 ID 可被后续新线程复用。
连接已终止的线程
类似进程中的 wait 系列函数:
与进程类似,若线程结束后没有连接,则会产生僵尸线程,除非手动将其分离(后续介绍)。如向 pthread_join 传入一个已经连接过的线程将会导致无法预知的行为,因为线程 ID 可能会被复用。 线程的连接与进程中的 wait 有以下不同:
线程之间的关系是对等的,不像进程存在父子关系。进程中的任意线程均可以调用 pthread_join 与其他线程连接起来。
不能以非阻塞方式连接,也不能连接任意线程(如别的进程中的线程)。
线程分离
线程默认清空下是可连接的,但也可指定其为分离状态。分离状态的线程在终止后,系统会自动清理并移除。当不关心线程的返回状态时,这十分有用。
一旦线程处理分离状态,则不可被连接,也不能重返可连接状态。在其他线程调用了 exit 或者主线程 return 后,分离的线程仍然会被终止。
线程与进程
线程有以下优点:
线程间数据共享十分简单
创建线程很快
相比与进程有以下缺点:
多线程编程时,需要确保调用线程安全函数,或以线程安全的方式调用函数(后续介绍)
因为共享共一个全局空间,某个线程的 bug 可能会影响到全局
线程共用宿主进程的虚拟地址空间,分配大量线程时,则会出现问题
线程中处理信号十分麻烦,在多线程程序中应避免使用信号
线程同步
线程同步旨在同步对共享资源的使用,可通过互斥量和条件变量实现。
互斥量
线程可以直接访问全局变量,但必须确保多个线程不会同时修改同一变量。这部分全局变量称之为临界区(共享资源),操作临界区的代码必须是原子的,某线程访问临界区时,不应该被其他线程中断。 互斥量有两种状态:已锁定和未锁定。任何时候只有一个线程可以锁定该互斥量,试图对一个已经上锁的互斥量加锁可能会阻塞进程或者返回失败。 一旦线程锁定某一互斥量,便只有该线程可以解锁。一般情况下,对不同共享资源会使用不同的互斥量,每个线程在访问同一资源时,一般遵循:
针对该共享资源锁定互斥量
访问共享资源
解锁
互斥量类型为 pthread_mutex_t
,在使用之前必须对其初始化:
加锁和解锁互斥量:
当线程需要同时访问多个不同资源时,这些资源由互斥量管理,则可能会发生死锁。例如:
线程 A 锁定了 mutex1
线程 B 锁定了 mutex2
线程 A 尝试锁定 mutex2, 线程 B 尝试锁定 mutex1
死锁发生
要避免此类死锁问题,最简单的办法就是定义互斥量的层级关系。当多个线程对一组互斥量操作时,总是以相同的顺序对互斥量进行锁定。 或者先使用 pthread_mutex_lock 锁定第一个互斥量,后续互斥量使用 pthread_mutex_trylock 尝试锁定,如果失败,则解锁之前锁定的互斥量。这种方法效率会低一些,但比较灵活。
条件变量
互斥量防止多个线程同时访问同一共享资源,条件变量允许线程阻塞,直到某个线程就某个共享资源的状态变化通知其他线程。 采用条件变量可以允许一个线程休眠,直至收到另一线程的通知。条件变量一般结合互斥量使用,就互斥量状态改变发出通知。 条件变量也有静态分配和动态分配两种方式:
条件变量的主要操作是发送信号(通知其他线程,此信号并不是系统的那个信号)和等待。等待操作是指收到一个通知前一直处于阻塞状态。
条件变量并不保存状态信息,发送信号通知其他线程时,若无线程在等待该条件变量,这个信号则会被忽略。
pthread_cond_wait
pthread_cond_wait 会阻塞线程直到收到条件变量,其内部会执行如下步骤:
解锁互斥量 mutex,此互斥量与控制目标资源访问的互斥量是同一个
阻塞调用线程,直到另一线程就此条件变量发出信号
重新锁定 mutex
其总与访问共享资源的同一个互斥量绑定,共享资源的互斥量与条件变量一般有以下形式:
线程锁定互斥量以访问共享资源
检查共享资源状态
如果共享资源并没有处于预期状态,则应该解锁互斥量(以便其他线程可以访问),并阻塞
线程因为条件变量被唤醒后,应立即加锁,因为一般线程会马上访问共享变量
函数 pthread_cond_wait 会自动执行 3 4 步的解锁和加锁,且解锁与陷入阻塞为一个原子操作,其他线程不会在此线程陷入阻塞前获取到此互斥量。 上面的代码中并没有用 if 语句判断 pthread_cond_wait 控制的条件的状态,而是采用 while 不断检查资源状态,因为从 pthread_cond_wait 中返回时不能对判断条件的状态做任何假设,因为可能会存在虚假唤醒的情况。
例子:生产者-消费者
最后更新于