由于在 C++ 阶段就已经深刻学习过 线程相关的知识,但是 C 语言的线程操作繁琐且难用,就算以后要用也必然是用封装过的线程,我们需要做这个工作。总的来讲,我们要显示三个内容:银行存钱取钱,阻塞队列,生产者消费者模型。
线程的基本操作
pthread_self–获取线程ID
每个线程都有一个唯一的ID。
在 Linux 中, pthread_t 类型定义为无符号长整型 ( unsigned long ) 。
1 |
|
pthread_create–创建线程
1 |
|
thread:pthread_t* 定义,用于接收创建的线程的标识符。
attr:线程属性,通常是 NULL ,采用默认属性。
start_routine:函数指针。
arg:传递给函数指针的形参。如果不传递任何参数,填 NULL。
代码:C 语言线程的基本使用
实际上这并不难,重点还是要演示如果传递更多的参数,已经如何获取线程的返回值。后者需要等到介绍后面的接口才能做,这里重点谈一谈前者。
我们看传递给函数指针的参数只有一个,理论上也只能传递一个。但是 void* 代表可以传递任何类型,结构体是一个类型,并且支持存储多个不同类型的参数,因此C 语言线程如何突破只能传递一个参数的限制?用结构体!
pthread_exit–终止线程
1 |
|
我们通过这个函数可以传递线程的返回值,即 retval 参数。然后其他线程就可以通过 pthread_join() 获取该返回值。等到我们讲完 pthread_join() 就实现获取返回值的代码。
让线程终止的方式,还要很多,下面列举:
- 进程终止 (比如调用 exit() ,从 main() 函数返回),那么该进程的所有线程都会立即终止。
- 从线程的入口函数返回。
- 线程调用 pthread_exit() 。
- 响应 pthread_cancel() 的取消请求。
pthread_join–连接已终止的线程
1 |
|
如果我们 detach 或者 等待 线程终止,主线程可能早早结束,会导致线程没有被执行的机会,或者执行到途中异常终止,主线程结束意味着进程结束,其他线程依赖于该进程,也就随之消亡。
但它还有一个作用,就是获取返回值。
代码地址:C 语言线程获取返回值
pthread_detach–分离线程
1 |
|
有时,我们并不关心线程的返回状态,反而希望在线程终止时系统能够自动清理并移除它。在这种情况下,我们可以调用 pthread_detach() 将线程标记为处于分离(detached)状态。一旦线程处于分离状态,就不能再使用 pthread_join() 来获取其返回状态了,也无法再回到可连接(joinable)状态了。
pthread_cancel–取消线程(了解)
通常情况下,程序中的多个线程会并发执行,每个线程各司其职。 但有时候,需要取消一个线程的执行。
与 pthread_exit 只能终止本线程不同。pthread_cancel 可以由其他线程调用以显式地向目标线程发起取消请求,且 pthread_cancel 不是强制的,是一种协商机制,目标线程可以选择是否响应这次取消操作。
1 |
|
发送取消请求后,pthread_cancel() 函数会立即返回,不会等待目标线程退出 。
目标线程会不会响应取消请求?以及何时响应?这取决于目标线程的两个属性:取消状态和取消类型。
(一)取消状态
目标线程会不会响应取消请求,是由目标线程的取消状态决定的,它有两个取值:
- PTHREAD_CANCEL_ENABLE :线程可以取消。这是默认值。
- PTHREAD_CANCEL_DISABLE :线程不可取消。如果此类线程收到取消请求,则会将取消请求挂起,直到线程的取消状态设置为启用。
下面是设置取消状态的方法:
1 |
|
state :取消状态,可取下面两个值:
- PTHREAD_CANCEL_ENABLE :线程可以取消。这是默认值。
- PTHREAD_CANCEL_DISABLE :线程不可取消。如果此类线程收到取消请求,则会将取消请求挂起,直到线程的取消状态设置为启用。
oldstate :传出参数,用来保存以前的取消状态。
(二)取消类型
目标线程何时响应取消请求,是由目标线程的取消类型决定的,它也有两个取值:
- PTHREAD_CANCEL_DEFERRED :延迟响应,挂起取消请求,直到下一个取消点才响应。这是默认值。
- PTHREAD_CANCEL_ASYNCHRONOUS :异步响应,可以在任意时间点响应取消请求 (未必是立即响应)。异步响应在实际应用中很少使用。
设置线程的取消类型:
1 |
|
type :取消类型,可取下面两个值:
- PTHREAD_CANCEL_DEFERRED :延迟响应,挂起取消请求,直到下一个取消点才响应。这是默认值。
- PTHREAD_CANCEL_ASYNCHRONOUS :异步响应,可以在任意时间点响应取消请求 (未必是立即响应)。异步响应在实际应用中很少使用。
oldtype :传出参数,用来保存以前的取消类型。
(三)取消点
若将线程的取消性状态和类型分别置为 ENABLE 和 DEFERRED(默认状态),那么只有当线程执行到某个取消点 时,取消请求才会起生效。取消点是一组函数,这些函数往往可以让线程陷入无限期的阻塞。
如果线程的取消类型为 DEFERRED ,当线程收到取消请求后,它会在下次抵达取消点时终止。如果该线程尚未分离,其它线程调用 pthread_join() 进行连接,会收到一个特殊的返回值 PTHREAD_CANCELED 。
(四)设置取消点
线程如果有挂起的取消请求,只要调用这个函数,线程就会终止。
1 |
|
如果线程执行的代码中不包含取消点,可以周期性地调用 pthread_testcancel() ,以确保对取消请求做出及时响应。
线程清理函数(了解)
线程收到取消请求后,会执行到下一个取消点终止。如果线程只是草草地直接终止,可能会让程序处于不一致的状态,比如:对共享变量的修改只进行了一半啦,没有释放互斥锁啦…… 这样的错误轻则导致其他线程产生错误的结果、发生死锁,重则让进程直接崩溃。
为了规避这一问题,线程可以设置一个或多个清理函数。当线程响应取消请求时,会自动执行这些清理函数。每个线程都拥有一个清理函数栈。当线程响应取消请求时,会依次执行栈中的清理函数 (从栈顶到栈底)。当执行完所有的清理函数后,线程终止。
1 |
|
线程同步
互斥锁
(一)初始化
下面有两种方式:
1 |
|
(二)上锁
1 |
|
如果已经没有上锁,上面就会锁住。如果已经上锁,就会:按顺序注释
- 一直阻塞,直到等待的互斥量解锁
- 立即失败,并返回状态码 EBUSY
- 如果在超时时间内,调用线程没能获得互斥量的所有权,那么函数 pthread_mutex_timedlock() 会返回错误码 ETIMEDOUT
(三)释放锁
1 |
|
(四)销毁
1 |
|
只有当互斥量处于未锁定状态,且后续不再使用它时,才应将其销毁。
(五)实战
代码地址:10个线程从银行取钱
代码地址:两个银行用户互相转账
条件变量
(一)初始化
1 |
|
(二)等待
1 |
|
pthread_cond_wait() 函数的语义如下:
- 释放所持有的互斥量 mutex 。
- 陷入阻塞状态。
- 当 pthread_cond_wait() 返回时,调用线程一定再一次获取了互斥量 mutex 。
如果不然,就会一直阻塞。而 pthread_cond_timedwait 可以在哪怕是在指定时间内没有满足要求,也会自动解除阻塞。
(三)唤醒
1 |
|
(四)销毁
1 |
|
当后续不再使用条件变量时,才应调用 pthread_cond_destroy() 将其销毁。
(五)实战
代码地址:阻塞队列
代码地址:生产者消费者