目录
一、线程的概念
1.1 定义
1.2 POSIX 线程标准
1.3 线程标识
1.4 相关函数
1.5 一些补充
二、线程的创建、终止与取消
2.1 创建
2.2 终止
2.2.1 return
2.2.2 pthread_exit
2.3 取消
2.3.1 函数介绍
2.3.2 禁止线程被取消
2.3.3 线程取消方式
2.4 清理
2.4.1 线程清理程序栈
2.4.2 pthread_cleanup_push
2.4.3 pthread_cleanup_pop
2.5 线程收尸
2.5.1 pthread_join
2.5.2 pthread_detach
2.6 代码示例
三、线程同步
3.1 互斥量
3.1.1 类型 pthread_mutex_t
3.1.2 pthread_mutex_init
3.1.3 pthread_mutex_lock
3.1.4 pthread_mutex_destroy
3.1.5 代码示例
3.2 线程池实现
3.3 令牌桶实现
3.4 条件变量
3.4.1 类型 pthread_cond_t
3.4.2 pthread_cond_init
3.4.3 pthread_cond_signal
3.4.4 pthread_cond_wait
3.4.5 pthread_cond_destroy
3.4.6 代码示例
3.5 令牌桶的非忙等版本
3.6 线程池的非忙等版本
3.7 信号量
3.8 读写锁
四、线程属性
4.1 pthread_attr_t 类型
4.2 pthread_attr_init
4.3 pthread_attr_destroy
4.4 设置属性的函数
4.5 代码示例
五、线程的同步属性
5.1 互斥量属性
5.1.1 pthread_mutexattr_t 类型
5.1.2 pthread_mutexattr_init
5.1.3 pthread_mutexattr_destroy
5.1.4 设置属性的函数
5.2 条件变量属性
5.2.1 pthread_condattr_t 类型
5.2.2 pthread_condattr_init
5.2.3 pthread_condattr_destroy
5.2.4 设置属性的函数
五、知识重构
5.1 重入
5.2 线程与信号
5.3 线程与 fork
六、线程模式
七、openmp 标准
后续学习中,时不时会提到进程。注意进程和线程的对比学习
进程:加载到内存的程序,一个正在运行的程序
线程:一个正在运行的函数
可以通过如下命令查看进程与线程的关系
ps axm
可以看出这样一个关系:进程就是容器,用来装线程
如下命令可以换种方式查看进程线程的关系,这里不再赘述
ps ax -L
多线程的实现有多套标准,POSIX 线程是一套目前最常用的标准,也是我们后续主要所学习的标准
何谓标准?可以这么理解:POSIX 线程标准给厂商规定了所需要实现的接口,但是接口具体如何实现厂商可以自由发挥
POSIX 线程标准以 pthread.h 头文件和一个线程库实现,因此,利用 gcc 编译和链接的时候需要加上 -pthread 选项。该标准下大约共有 100 个函数调用,全都以 pthread_ 开头,并可以分为四类:
- 线程管理,例如创建线程,等待(join)线程,查询线程状态等。
- 互斥锁(Mutex):创建、摧毁、锁定、解锁、设置属性等操作
- 条件变量(Condition Variable):创建、摧毁、等待、通知、设置与查询属性等操作
- 使用了互斥锁的线程间的同步管理
就像每个进程有一个进程 ID 一样,每个线程也有一个线程 ID,用于标识线程的身份。进程 ID 在整个系统中是唯一的,而线程 ID 只有在它所属的进程中是唯一的。归属于不同进程的两个线程可能有相同的线程 ID
进程 ID 是用 pid_t 数据类型来表示的,是一个非负整数。线程 ID 是用 pthread_t 数据类型来表示的(p 代表 POSIX 线程标准,thread 代表线程,_t 代表其是某种类型的 typedef)。不同系统下的 pthread_t 可能是由不同类型 typedef 来的(毕竟 POSIX 只规定了接口而未规定接口的实现,有些厂商开发的系统下的 pthread_t 可能是结构体类型),因此不能想当然地以为线程 ID 是一个整数!
#include
int pthread_equal(pthread_t t1, pthread_t t2);
功能:比较两个线程 ID
#include
pthread_t pthread_self(void);
功能:获取自身线程 ID
- 之前我们关注的问题都是以进程为最小单位进行思考的,以后要试试以线程为最小单位进行思考
- 在操作系统发展史上,多线程这一部分是先有标准化,再有应用的;而之前信号部分是先被应用,后面慢慢标准化的。因此,多线程这个部分学起来会感觉更规范,更容易学习
- 不建议将之前讲的信号机制和多线程机制大范围混用!因为太难啦
- 同个进程中的多个线程之间是兄弟关系,不分主次的
- 因为标准化的约束,其实之前我们用到的很多库都是支持多线程并发的
创建容易理解,那终止与取消有什么区别呢?
答:终止指的是线程执行完自己的使命然后结束;取消指的是线程工作做到一半被中途取消
man 3 pthread_create
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
功能:创建一个新线程
之前都是成功返回 0, 失败设置 errno 的。这里为什么不一样了?
答:失败返回 errno 而非设置 errno 是考虑到了多线程的特点。多个线程中的函数如果都去设置同一个全局的 errno,岂不是就乱套了?
代码示例:创建一个线程,线程打印正在运行
#include
#include
#include
#include
static void * func(void * p) {
puts("Thread is working!");
return NULL;
}
int main() {
pthread_t tid;
puts("Begin!");
int err = pthread_create(&tid, NULL, func, NULL); // 创建线程,即创建一个运行的函数
// func为运行的函数
if (err) {
fprintf(stderr, "pthread_create():%s\n",strerror(err));
exit(1);
}
puts("End!");
exit(0);
}
嗯嗯?为什么运行结果不一样?
答:因为线程的调度取决于调度器策略!如果先调度 func 线程,则会成功打印 Thread is working;如果先调度了 main 线程,main 线程调用 exit(0) 结束了当前进程,那么 Thread is working 就无法打印了,因为进程已经终止了
立即(不会执行清理程序)终止线程。如果是 main 线程调用 return,则直接终止整个进程
man 3 pthread_exit
#include
void pthread_exit(void *retval);
功能:终止一个线程
在多线程编程中,建议用 pthread_exit 替换 return
static void * func(void * p) {
puts("Thread is working!\n");
pthread_exit(NULL);
// return NULL;
}
为什么?有如下几个原因:
man pthread_cancel
#include
int pthread_cancel(pthread_t thread);
功能:取消线程
线程取消有什么意义?比如用多个线程在某一个树的不同分支中查找某个节点。只要其中一个线程找到了指定节点,其他线程哪怕没有找完,也可以取消查找了
禁止线程被取消是一个非常重要的机制。毕竟如果一个线程始终允许被取消,会出现如下问题
故一个线程一定有允许取消和不允许被取消两种状态,可以通过以下函数设置是否允许被取消
man pthread_setcancelstate
#include
int pthread_setcancelstate(int state, int *oldstate);
功能:设置线程是否允许被取消
当一个线程允许被取消,又有如下两种取消方式
可以通过如下函数设置取消方式
man pthread_setcanceltype
#include
int pthread_setcanceltype(int type, int *oldtype);
功能:设置线程的取消方式
#include
void pthread_testcancel(void);
功能:人为设置 cancel 点
线程会维护这样一个栈,栈中存放了一些函数(指针),这些函数代表了线程的一个个清理程序。当遇到如下情况时,栈中的清理函数会出栈并执行:
man pthread_cleanup_push
#include
void pthread_cleanup_push(void (*routine)(void *),
void *arg);
功能:向线程清理程序栈添加函数
man pthread_cleanup_pop
#include
void pthread_cleanup_pop(int execute);
功能:从线程清理程序栈出栈栈顶的函数
补充一下,pthread_cleanup_push 与 pthread_cleanup_pop 有一个限制:它们实现为宏,pthread_cleanup_push 的宏定义包含一定量的字符 {,在 pthread cleanup_pop 的定义中有与之对应的匹配字符 }。所以这两个函数必须在与线程相同的作用域中以匹配对的形式使用
代码示例:向线程清理程序栈添加函数,并出栈
#include
#include
#include
#include
static void cleanup_func(void *p) {
puts(p);
}
static void *func(void *p) {
puts("Thread is working!");
pthread_cleanup_push(cleanup_func, "cleanup:1");
pthread_cleanup_push(cleanup_func, "cleanup:2");
pthread_cleanup_push(cleanup_func, "cleanup:3"); // 依次入栈
puts("push over!");
// 成对出现
pthread_cleanup_pop(1); // 出栈并执行,栈顶函数会打印:cleanup:3
pthread_cleanup_pop(0); // 仅出栈
pthread_exit(NULL); // 出栈并执行,打印:cleanup:1
pthread_cleanup_pop(0); // 虽然执行不到,但是为了括号匹配,必须写
}
int main(void) {
pthread_t tid; // thread id
int err;
puts("Begin!");
err = pthread_create(&tid, NULL, func, NULL);
if(err) {
fprintf(stderr, "pthread_create(): %s\n", strerror(err));
exit(1);
}
pthread_join(tid, NULL); // 等待,直到tid所标识的线程终止,后面会介绍
puts("End!");
exit(0);
}
当一个线程终止或取消,还需要想办法为其“收尸”,从而将线程所占据的资源归还给系统
有人会问:不是可以手动通过线程清理程序释放资源吗,为什么还需要通过“收尸”让线程归还资源?
答:线程清理程序中释放的资源是受线程控制的一些资源,比如堆内存、文件描述符等等,这些资源在线程中申请,也理应在线程终止或取消前通过清理程序释放。而操作系统为记录线程的一些必要信息,还使用了额外的资源,这些资源需要通过“收尸”归还给系统(当然,进程终止肯定会统一释放所有资源的,但是我们不能寄希望于进程的终止,毕竟万一是个长时间运行的进程怎么办?)
man pthread_join
#include
int pthread_join(pthread_t thread, void **retval);
功能:阻塞等待,直到特定线程终止或被取消,并为其收尸
man pthread_detach
#include
int pthread_detach(pthread_t thread);
功能:不阻塞,显式要求特定线程自动收尸
使用这两个函数的几个注意点:从 man 手册截取的
- Either pthread_join(3) or pthread_detach() should be called for each thread that an application creates, so that system resources for the thread can be released.
- Once a thread has been detached, it can't be joined with pthread_join(3) or be made joinable again.
- Attempting to detach an already detached thread results in unspecified behavior.
需求:找出 30000000~30000200 之间的所有质数。要求创建 201 个线程来判断每个数是不是质数
最开始写出来的代码如下
#include
#include
#include
#include
#include
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1) // 线程总数
static void * thr_primer(void *p) {
int mark = 1;
int num = *(int*)p; // void*转化为int*才能解引用
for (int j = 2; j < num/2; ++j)
{
if (num % j == 0)
{
mark = 0;
break;
}
}
if (mark)
printf("%d is a primer\n",num);
pthread_exit(NULL);
}
int main() {
pthread_t tid[THRNUM];
int err;
for (int i = LEFT; i <= RIGHT; ++i) {
// 针对每个数,都创建一个线程去判断其是否为质数
err = pthread_create(tid + i - LEFT, NULL, thr_primer, &i);
if (err)
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
for (int i = LEFT; i <= RIGHT; ++i)
pthread_join(tid[i - LEFT], NULL);
exit(0);
}
居然每一次的运行结果都不一样!!原因在哪里?
答:产生了线程之间的竞争与冲突!
问题出现在如下语句
err = pthread_create(tid + i - LEFT, NULL, thr_primer, &i);
int num = *(int*)p;
对于每一个线程,都是从同一个地址去取 i 的值,会有如下问题
这样一来,30000000、30000001 都被漏判断了......
既然问题出在传同一个地址给别的线程,而地址中的值可能会被 main 线程改变。一种比较丑陋的解决办法如下:我们直接传值!
#include
#include
#include
#include
#include
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1) // 线程总数
static void * thr_primer(void *p) {
int mark = 1;
int num = (int)p; // p其实就是i,强转回int
for (int j = 2; j < num/2; ++j)
{
if (num % j == 0)
{
mark = 0;
break;
}
}
if (mark)
printf("%d is a primer\n",num);
pthread_exit(NULL);
}
int main() {
pthread_t tid[THRNUM];
int err;
for (int i = LEFT; i <= RIGHT; ++i) {
// 针对每个数,都创建一个线程去判断其是否为质数
err = pthread_create(tid + i - LEFT, NULL, thr_primer, (void*)i); // 我们就直接将i的值传过去,为了类型匹配强转一下
if (err)
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
for (int i = LEFT; i <= RIGHT; ++i)
pthread_join(tid[i - LEFT], NULL); // 对一个个线程收尸
exit(0);
}
虽然结果是对了,但是太丑陋了,gcc 给了那么多警告⚠!
警告是因为奇奇怪怪的强转太多了。换种思路,我们还是传地址给别的线程,只不过传给不同线程的是不同的地址
#include
#include
#include
#include
#include
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1) // 线程总数
static void * thr_primer(void *p) {
int mark = 1;
int num = *(int*)p;
for (int j = 2; j < num/2; ++j)
{
if (num % j == 0)
{
mark = 0;
break;
}
}
if (mark)
printf("%d is a primer\n",num);
pthread_exit(p); // 将地址返回,为了free
}
int main() {
pthread_t tid[THRNUM];
int err;
for (int i = LEFT; i <= RIGHT; ++i) {
int * p = malloc(sizeof(int));
*p = i; // 针对每个数,都开辟一片内存存放这个数,然后将这片内存传给线程
// 针对每个数,都创建一个线程去判断其是否为质数
err = pthread_create(tid + i - LEFT, NULL, thr_primer, (void*)p);
if (err)
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
void * ptr;
for (int i = LEFT; i <= RIGHT; ++i) {
pthread_join(tid[i - LEFT], &ptr); // 对一个个线程收尸
free(ptr); // 用ptr接受线程返回的地址,然后free
}
exit(0);
}
上面的代码创建了 201 个线程。这不由地使我们思考一个问题:一个进程最多能容纳多少线程?
每个线程的都需要一个独立的栈空间......
64 位系统意味着用户空间的虚拟内存最大值是 128T,这个数值是很大的,一个线程需占用 8M 栈空间的情况来算,那么理论上可以创建 128T/8M 个线程,也就是 1000 多万个线程,感觉可以放心使用
但是如果是 32 位系统,那么能够创建的线程数就有很有限了。我们需要限制线程的数量,并将任务合理分配给有限数量的线程,这将在后续展开讨论
来看一个现象:我们创建 200 个线程,每个线程都对全局量 count 执行 5000 次自增加,按道理来说应该总共会对 count 自增 200 * 5000 = 1000000 次
结果发现,count 并没有增加到 1000000!原因如下
因此,在我们的代码里,虽然总共会对 count 自增 200 * 5000 = 1000000 次,但是其实 count 的增加量可能无法达到 1000000
这种现象本质原因是:当其中一个线程在执行 count++ 这段代码时,有别的线程也在同时执行这段相同的代码
线程同步就是希望能够解决上述的竞争故障问题
这个类型的变量代表一把锁,用来锁住一段代码,被锁住的这段代码称为临界区。当一个线程在临界区,它能够阻止其他线程进入直到本线程离开临界区
man pthread_mutex_init
#include
// 按照指定方式初始化锁mutex
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 默认初始化锁mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
功能:初始化一把锁
注意:初始化一把锁相当于“指定锁的行为”,未被初始化的锁是无效的。若是要想定制锁的行为,或者想将锁存在堆中,必须要用到 pthread_mutex_init
通常情况下,我们默认初始化锁(采取默认的锁的行为)即可
#include
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 对一把锁上锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 尝试对一把锁上锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 对一把锁解锁
功能:针对一把锁进行上锁与解锁
#include
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能: 清除一把锁的初始化状态,使其无效
接下来我们练习一下这套函数的基本用法,通过互斥量,尝试修改上述对 count 自增 1000000 次的代码
#include
#include
#include
#include
#include
#define THRNUM 200 // 创建200个线程
static int count = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 默认行为的锁,默认初始化
// 注意mutex需要定义在所有线程都能看到的全局位置
static void *thr_add(void *p) {
for (int i = 0; i < 5000; ++i)
{
pthread_mutex_lock(&mutex); // 上锁
count++; // 这样一来这部分代码仅能同时被单一线程执行
pthread_mutex_unlock(&mutex); // 解锁
}
pthread_exit(NULL);
}
int main() {
pthread_t tid[THRNUM];
int err;
for (int i = 0; i < THRNUM; ++i) {
err = pthread_create(tid + i, NULL, thr_add, NULL);
if (err) {
fprintf(stderr, "pthread_create():%s\n",strerror(err));
exit(1);
}
}
for (int i = 0; i < THRNUM; ++i) {
pthread_join(tid[i], NULL); // 对一个个线程收尸
}
printf("count = %d\n", count);
pthread_mutex_destroy(&mutex); // destroy一把锁mutex
exit(0);
}
需求:创建四个线程,其中
- 第一个线程往终端不断打印字符 a
- 第二个线程往终端不断打印字符 b
- 第三个线程往终端不断打印字符 c
- 第四个线程往终端不断打印字符 d
我们希望最终打印出来的顺序是 abcdabcdabcdabcd........
一种想法是搭建一个循环锁链
#include
#include
#include
#include
#include
#define THRNUM 4
static pthread_mutex_t mut[THRNUM]; // 需要四把锁,构成锁链
void * thr_func(void *p) {
int n = (int)p;
int c = 'a' + n;
while (1) {
pthread_mutex_lock(mut+n); // 上锁打印本字母的线程
write(1, &c, 1);
pthread_mutex_unlock(mut+(n+1)%THRNUM); // 解锁打印下一个字母的线程
}
pthread_exit(NULL);
}
int main() {
pthread_t tid[THRNUM];
for (int i = 0; i < THRNUM; ++i) {
pthread_mutex_init(mut + i, NULL); // 初始化锁
pthread_mutex_lock(mut+i); // 上锁
int err = pthread_create(tid+i, NULL, thr_func, (void*)i);
if (err)
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
}
}
pthread_mutex_unlock(mut+0); // 解锁打印a的线程
alarm(5);
for (int i = 0; i < THRNUM; ++i) {
pthread_join(tid[i], NULL);
}
exit(0);
}
回到 2.6 讲解的求质数部分。我们需要限制线程的数量,并将任务合理分配给有限数量的线程。在进程章节的 3.3 我们介绍了一些将任务分配给不同进程的方式,当时说过用池内算法涉及到竞争,因此暂时没有实现。而在线程部分,我们似乎可以实现池内算法了,原因如下:
基本思路如下
代码实现如下
#include
#include
#include
#include
#include
#include
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM 10 // 线程总数限定为10
static int pool = 0; // 池
// >0表示池中有任务
// =0表示池中没有任务
// =-1表示任务完成
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 多个线程操作同一个池,需要加锁
static void * thr_primer(void *p) {
int num;
int mark;
while (1) {
mark = 1;
pthread_mutex_lock(&mutex); // 访问pool前,先上锁
while (pool == 0) // 一定要用while:因为并不保证从别的线程回来之后,pool一定不为0!
{
pthread_mutex_unlock(&mutex); // 一定要先解锁
sched_yield(); // 将cpu让给别的线程
pthread_mutex_lock(&mutex); // 回来需要上锁。然后再去看看pool
}
if (pool == -1)
{
pthread_mutex_unlock(&mutex); // 注意临界区中的任何跳转语句,跳出临界区之前一定要解锁
break;
}
num = pool; // 从池中获取
pool = 0;
pthread_mutex_unlock(&mutex);
for (int j = 2; j < num/2; ++j)
{
if (num % j == 0)
{
mark = 0;
break;
}
}
if (mark)
printf("thread[%d]: %d is a primer\n",(int)p, num);
}
pthread_exit(NULL);
}
int main() {
pthread_t tid[THRNUM];
int err;
for (int i = 0; i < THRNUM; ++i) { // 创建10个线程
err = pthread_create(tid + i, NULL, thr_primer, (void*)i);
if (err)
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
for (int i = LEFT; i <= RIGHT; ++i) { // 不断发任务
pthread_mutex_lock(&mutex);
while (pool != 0)
{
pthread_mutex_unlock(&mutex);
sched_yield(); // 将cpu让给别的线程
pthread_mutex_lock(&mutex);
}
pool = i; // 下发任务
pthread_mutex_unlock(&mutex);
}
pthread_mutex_lock(&mutex);
while (pool != 0) // 确保待完成的任务已经完成
{
pthread_mutex_unlock(&mutex);
sched_yield();
pthread_mutex_lock(&mutex);
}
pool = -1; // 相当于告知线程任务完成
pthread_mutex_unlock(&mutex);
for (int i = 0; i < THRNUM; ++i) {
pthread_join(tid[i], NULL); // 对一个个线程收尸
}
pthread_mutex_destroy(&mutex); // 使mutex不可用
exit(0);
}
从上述代码中学到的几个点:
- 凡是需要读或者写公共资源 pool 的代码段,最好都用锁使其变成临界区
- 从线程的临界区跳出线程的代码记得一定要先释放锁,否则会出现死锁
我们在并发的信号部分通过信号的机制实现了令牌桶,并将其封装成库。在这里,我们希望不使用任何信号机制,改用多线程的办法实现相同的功能
具体而言:
就是上面所创建的线程造成了并发的可能!
在实现之前,我们需要了解一个函数:pthread_once
man pthread_once
#include
int pthread_once(pthread_once_t *once_control,
void (*init_routine)(void));
pthread_once_t once_control = PTHREAD_ONCE_INIT;
功能: 保证某函数只能被调用一次
现在开始代码实现。注意,仅需更改接口函数的具体实现的部分!main.c 和 mytbf.h 都不用改
mytbf.c
#include
#include
#include
#include
#include
#include
#include "mytbf.h"
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static struct mytbf_st* job[MYTBF_MAX]; // 用于存放令牌桶的数组。最多存放MYTBF_MAX个桶
// 表示最多存在MYTBF_MAX套方案
static pthread_t tid; // 往job中的结构体里不断添加token的线程
static pthread_once_t init_once;
struct mytbf_st {
int aps; // 每秒增加的令牌数
int burst; // 每个桶中的令牌个数上限
int token; // 令牌个数
int pos; // 这个桶存放在数组的哪个位置
pthread_mutex_t mutex; // 每个桶都需要一个锁
};
static int get_free_pos_unlocked() { // 工具函数,获取数组中的空位置来存放令牌桶
// 这个函数本身未上锁,但是他应该在临界区被调用
for (int i = 0; i < MYTBF_MAX; ++i)
if (job[i] == NULL)
return i;
return -1;
}
static int min(int lhs, int rhs) { // 工具函数,求最小值
return lhs < rhs ? lhs : rhs;
}
static void* thr_alrm(void* p) {
while (1) {
pthread_mutex_lock(&mutex); // 读写job数组前上锁
for (int i = 0; i < MYTBF_MAX; ++i) {
if (job[i] != NULL) {
pthread_mutex_lock(&job[i]->mutex); // 读写结构体实例前上锁
job[i]->token += job[i]->aps;
if (job[i]->token > job[i]->burst)
job[i]->token = job[i]->burst;
pthread_mutex_unlock(&job[i]->mutex); // 读写结构体实例后解锁
}
}
pthread_mutex_unlock(&mutex); // 读写job数组后解锁
sleep(1); // 每隔一秒往桶中增加一次令牌
}
}
static void module_unload() { // 模块卸载
// 为什么叫模块?一个好的编程习惯是,将我们写的代码看成一个软件项目下的一个子模块......
// 模块卸载是为了使调用我们的模块后,环境能够恢复到和调用模块前一样
pthread_cancel(tid);
pthread_join(tid, NULL);
// 此时,往job的中的结构体不断添加token的线程终止了,并发结束
// 可以快快乐乐无需上锁读写job了
for (int i = 0; i < MYTBF_MAX; ++i)
{
if (job[i] != NULL)
{
mytbf_destroy(&job[i]);
}
}
pthread_mutex_destroy(&mutex);
}
static void module_load(void) { // 模块加载
// 模块加载表示做进入我们的模块之前的预处理。只能调用一次
// 创建线程,线程用于每隔一秒往桶中增加一次令牌
int err = pthread_create(&tid, NULL, thr_alrm, NULL);
if (err) {
fprintf(stderr, "pthread_create():%s\n", strerror(err));
}
}
// 创建令牌桶,返回一套令牌桶参数来表征一个桶
mytbf_t * mytbf_init(int aps, int burst) {
struct mytbf_st * me;
pthread_once(&init_once, module_load); // 如果main中创建多个桶,也只会调用一次load
me = malloc(sizeof(*me));
if (me == NULL)
return NULL;
me->token = 0;
me->aps = aps;
me->burst = burst;
pthread_mutex_init(&me->mutex, NULL);
pthread_mutex_lock(&mutex); // 读写job,上锁
int pos = get_free_pos_unlocked();
if (pos < 0) {
pthread_mutex_unlock(&mutex);
free(me);
return NULL;
}
me->pos = pos;
job[pos] = me; // 将创建的桶放进数组的空位置
pthread_mutex_unlock(&mutex); // 解锁
return me;
}
// 在特定令牌桶中中获取令牌token
int mytbf_fetchtoken(mytbf_t *ptr, int size) {
if (size <= 0)
return -EINVAL; // 这样做的好处是为了:用户可以通过EINVAL获取对应的错误提示字符串
struct mytbf_st *me = ptr; // 别忘了mytbf_t*是void*,struct mytbf_st*才是结构体指针
pthread_mutex_lock(&me->mutex); // 读写某个结构体实例中的token,上锁
while (me->token <= 0)
{
pthread_mutex_unlock(&me->mutex); // 从临界区跳出线程的代码记得一定要先释放锁
sched_yield(); // 主动让出cpu,让别的线程有机会抢到
pthread_mutex_lock(&me->mutex);
}
int n = min(me->token, size); // 希望获取size个令牌,但是桶中令牌个数是me->token
// 能获取到的应该是size与me->token中的最小值
me->token -= n;
pthread_mutex_unlock(&me->mutex); // 解锁
return n;
}
// 在特定令牌桶中归还令牌token
int mytbf_returntoken(mytbf_t *ptr, int size) {
if (size <= 0)
return -EINVAL;
struct mytbf_st *me = ptr;
pthread_mutex_lock(&me->mutex); // 读写某个结构体实例中的token,上锁
me->token += size;
if (me->token > me->burst)
me->token = me->burst;
pthread_mutex_unlock(&me->mutex); // 解锁
return size;
}
// 删除桶
int mytbf_destroy(mytbf_t *ptr) {
struct mytbf_st * me = ptr;
pthread_mutex_lock(&mutex);
job[me->pos] = NULL;
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&me->mutex);
free(ptr);
return 0;
}
从上述代码中学到的几个点:
- 因为临界区的代码无法由多个线程并发执行,为了提高代码运行效率,临界区应该越短越好
- 要明确因为什么造成的并发?上述代码中是因为我们创建了一个不断往位于 job 数组中的结构体实例增加 token 的线程,同时用户在别的线程也可能操作 job 数组及结构体实例,因此才造成的并发
- 读写公共资源 job 和结构体实例的代码段都需要上锁,使代码段成为临界区
- 可以看出将接口与实现分离的一个优点:只要接口定义明确,接口的具体实现对于在 main 中调用接口实现业务的用户来说是透明的
此外,如果我们看一看 CPU 使用率,可以发现
这是由于
while (me->token <= 0) { pthread_mutex_unlock(&me->mutex); sched_yield(); // 主动让出cpu,但是回来后未必token就大于0了 pthread_mutex_lock(&me->mutex); }
上述代码造成了忙等现象。假如 token 一直小于等于 0,则该循环会不断重复执行解锁、取消调度、被调度、上锁、判断 token、解锁、......,比较耗 CPU。这相当于是查询法,线程不断去查询 token 是否大于 0,CPU 资源全都花费在查询上了。如果能用通知法,将会节省很多 CPU 资源。下面的机制将能够实现通知法
这个类型的变量为条件变量,可以理解为一个“条件”。线程可以等待一个“条件”的满足,并休眠;也可以告知别的线程“条件已满足”,从而唤醒别的休眠的线程
这种思想最开始也称为“私有信号量”
man pthread_cond_init
#include
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
功能:初始化一个条件变量
注意:初始化一个条件变量相当于“指定条件变量的行为”,未被初始化的条件变量是无效的。若是要想定制条件变量的行为,或者想将条件变量存在堆中,必须要用到 pthread_cond_init
通常情况下,我们默认初始化条件变量(采取默认的条件变量的行为)即可
#include
int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒某一等待特定条件满足的线程
注意:所谓“唤醒”并不是指马上调度线程,而是使线程解除阻塞,加入就绪队列等待调度。如果有多个等待条件 cond 满足的线程,那么唤醒哪个等待特定条件满足的线程也是不确定的,反正只能唤醒一个。这个语句拟人化的比喻是“告知某个线程:条件 cond 被满足了”。如果没有等待唤醒的线程,将无事发生(毕竟压根都没人睡觉,就算闹钟响了也叫不醒任何人)
3.4.4 pthread_cond_broadcast
#include
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒所有等待条件 cond 满足的线程
#include
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);
功能:等待特定条件并休眠
为何 wait 规定一定要在临界区内被调用?
因此,wait 假定在被调用时,互斥量已是上锁状态。wait 的职责是释放锁,并让调用线程休眠(原子地)。当线程被唤醒时(在另外某个线程告知它条件被满足后),它必须重新获取锁,再返回调用者。所以我们才需要将锁传入 wait,方便 wait 对锁进行一系列操作
#include
int pthread_cond_destroy(pthread_cond_t *cond);
功能: 清除一个条件变量的初始化状态,使其无效
引入条件变量,而不使用锁链,实现同 3.1.4 相同的需求:创建四个线程,其中
- 第一个线程往终端不断打印字符 a
- 第二个线程往终端不断打印字符 b
- 第三个线程往终端不断打印字符 c
- 第四个线程往终端不断打印字符 d
我们希望最终打印出来的顺序是 abcdabcdabcdabcd........
#include
#include
#include
#include
#include
#define THRNUM 4
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int pre = 3; // 上一个打印的字母
// 0-a
// 1-b
// 2-c
// 3-d
void * thr_func(void *p) {
int n = (int)p;
int c = 'a' + n;
while (1) {
pthread_mutex_lock(&mutex);
while (n != (pre+1)%4) // 当前线程打印的非我们希望打印的
pthread_cond_wait(&cond, &mutex); // 释放锁,休眠
write(1, &c, 1);
pre = n; // 更新“上一个打印的字母”
pthread_cond_broadcast(&cond); // 让别的线程醒醒
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
通过条件变量的机制,能够避免忙等情况的出现
#include
#include
#include
#include
#include
#include
#include "mytbf.h"
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static struct mytbf_st* job[MYTBF_MAX]; // 用于存放令牌桶的数组。最多存放MYTBF_MAX个桶
// 表示最多存在MYTBF_MAX套方案
static pthread_t tid; // 往job中的结构体里不断添加token的线程
static pthread_once_t init_once;
struct mytbf_st {
int aps; // 每秒增加的令牌数
int burst; // 每个桶中的令牌个数上限
int token; // 令牌个数
int pos; // 这个桶存放在数组的哪个位置
pthread_mutex_t mutex; // 每个桶都需要一个锁
pthread_cond_t cond; // 每个桶都需要维护一个条件,用于通知“该桶的token还有剩余”
};
static int get_free_pos_unlocked() { // 工具函数,获取数组中的空位置来存放令牌桶
// 这个函数本身未上锁,但是他应该在临界区被调用
for (int i = 0; i < MYTBF_MAX; ++i)
if (job[i] == NULL)
return i;
return -1;
}
static int min(int lhs, int rhs) { // 工具函数,求最小值
return lhs < rhs ? lhs : rhs;
}
static void* thr_alrm(void* p) {
while (1) {
pthread_mutex_lock(&mutex); // 读写job数组前上锁
for (int i = 0; i < MYTBF_MAX; ++i) {
if (job[i] != NULL) {
pthread_mutex_lock(&job[i]->mutex); // 读写结构体实例前上锁
job[i]->token += job[i]->aps;
if (job[i]->token > job[i]->burst)
job[i]->token = job[i]->burst;
pthread_cond_broadcast(&job[i]->cond); // 发出一个通知
pthread_mutex_unlock(&job[i]->mutex); // 读写结构体实例后解锁
}
}
pthread_mutex_unlock(&mutex); // 读写job数组后解锁
sleep(1); // 每隔一秒往桶中增加一次令牌
}
}
static void module_unload() { // 模块卸载
// 为什么叫模块?一个好的编程习惯是,将我们写的代码看成一个软件项目下的一个子模块......
// 模块卸载是为了使调用我们的模块后,环境能够恢复到和调用模块前一样
pthread_cancel(tid);
pthread_join(tid, NULL);
// 此时,往job的中的结构体不断添加token的线程终止了,并发结束
// 可以快快乐乐无需上锁读写job了
for (int i = 0; i < MYTBF_MAX; ++i)
{
if (job[i] != NULL)
{
mytbf_destroy(&job[i]);
}
}
pthread_mutex_destroy(&mutex);
}
static void module_load(void) { // 模块加载
// 模块加载表示做进入我们的模块之前的预处理。只能调用一次
// 创建线程,线程用于每隔一秒往桶中增加一次令牌
int err = pthread_create(&tid, NULL, thr_alrm, NULL);
if (err) {
fprintf(stderr, "pthread_create():%s\n", strerror(err));
}
}
// 创建令牌桶,返回一套令牌桶参数来表征一个桶
mytbf_t * mytbf_init(int aps, int burst) {
struct mytbf_st * me;
pthread_once(&init_once, module_load); // 如果main中创建多个桶,也只会调用一次load
me = malloc(sizeof(*me));
if (me == NULL)
return NULL;
me->token = 0;
me->aps = aps;
me->burst = burst;
pthread_mutex_init(&me->mutex, NULL);
pthread_cond_init(&me->cond, NULL);
pthread_mutex_lock(&mutex); // 读写job,上锁
int pos = get_free_pos_unlocked();
if (pos < 0) {
pthread_mutex_unlock(&mutex);
free(me);
return NULL;
}
me->pos = pos;
job[pos] = me; // 将创建的桶放进数组的空位置
pthread_mutex_unlock(&mutex); // 解锁
return me;
}
// 在特定令牌桶中中获取令牌token
int mytbf_fetchtoken(mytbf_t *ptr, int size) {
if (size <= 0)
return -EINVAL; // 这样做的好处是为了:用户可以通过EINVAL获取对应的错误提示字符串
struct mytbf_st *me = ptr; // 别忘了mytbf_t*是void*,struct mytbf_st*才是结构体指针
pthread_mutex_lock(&me->mutex); // 读写某个结构体实例中的token,上锁
while (me->token <= 0)
{
// 等待条件满足。收到满足条件的通知前会阻塞,不会一直循环忙等
pthread_cond_wait(&me->cond, &me->mutex);
}
int n = min(me->token, size); // 希望获取size个令牌,但是桶中令牌个数是me->token
// 能获取到的应该是size与me->token中的最小值
me->token -= n;
pthread_mutex_unlock(&me->mutex); // 解锁
return n;
}
// 在特定令牌桶中归还令牌token
int mytbf_returntoken(mytbf_t *ptr, int size) {
if (size <= 0)
return -EINVAL;
struct mytbf_st *me = ptr;
pthread_mutex_lock(&me->mutex); // 读写某个结构体实例中的token,上锁
me->token += size;
if (me->token > me->burst)
me->token = me->burst;
pthread_mutex_unlock(&me->mutex); // 解锁
return size;
}
// 删除桶
int mytbf_destroy(mytbf_t *ptr) {
struct mytbf_st * me = ptr;
pthread_mutex_lock(&mutex);
job[me->pos] = NULL;
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&me->mutex);
pthread_cond_destroy(&me->cond);
free(ptr);
return 0;
}
原代码如下:假如 token 一直小于等于 0,原循环会不断重复执行解锁、取消调度、被调度、上锁、判断 token、解锁、......,比较耗 CPU。这相当于是查询法,线程不断去查询 token 是否大于 0,CPU 资源全都花费在查询上了
while (me->token <= 0) { pthread_mutex_unlock(&me->mutex); sched_yield(); // 主动让出cpu,但是回来后未必token就大于0了 pthread_mutex_lock(&me->mutex); }
优化后的代码如下:只要 token 小于等于 0,线程就会开始阻塞。直到有通知告诉它 token 被增加了,才会被唤醒。这样不会循环执行无意义的代码,能够节约 CPU 资源
while (me->token <= 0) { // 等待条件满足。收到满足条件的通知前会阻塞,不会一直循环忙等 pthread_cond_wait(&me->cond, &me->mutex); }
同理,修改 3.2 代码如下
#include
#include
#include
#include
#include
#include
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM 10 // 线程总数限定为10
static int pool = 0; // 池
// >0表示池中有任务
// =0表示池中没有任务
// =-1表示任务完成
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 多个线程操作同一个池,需要加锁
static pthread_cond_t cond_full = PTHREAD_COND_INITIALIZER; // 用于通知所有下游线程:池满了,干活!
static pthread_cond_t cond_empty = PTHREAD_COND_INITIALIZER; // 用于通知main线程:池空了,派活儿!
static void * thr_primer(void *p) {
int num;
int mark;
while (1) {
mark = 1;
pthread_mutex_lock(&mutex); // 访问pool前,先上锁
while (pool == 0) // 一定要用while
{
pthread_cond_wait(&cond_full, &mutex); // 等待条件:池变满
}
if (pool == -1)
{
pthread_mutex_unlock(&mutex); // 注意临界区中的任何跳转语句,跳出临界区之前一定要解锁
break;
}
num = pool; // 从池中获取
pool = 0;
pthread_cond_signal(&cond_empty); // 告诉main线程:池空了
pthread_mutex_unlock(&mutex);
for (int j = 2; j < num/2; ++j)
{
if (num % j == 0)
{
mark = 0;
break;
}
}
if (mark)
printf("thread[%d]: %d is a primer\n",(int)p, num);
}
pthread_exit(NULL);
}
int main() {
pthread_t tid[THRNUM];
int err;
for (int i = 0; i < THRNUM; ++i) { // 创建20个线程
err = pthread_create(tid + i, NULL, thr_primer, (void*)i);
if (err)
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
for (int i = LEFT; i <= RIGHT; ++i) { // 不断发任务
pthread_mutex_lock(&mutex);
while (pool != 0)
{
pthread_cond_wait(&cond_empty, &mutex); // 等待条件:池变空
}
pool = i; // 下发任务
pthread_cond_broadcast(&cond_full); // 告诉所有下游线程:池满了
pthread_mutex_unlock(&mutex);
}
pthread_mutex_lock(&mutex);
while (pool != 0) // 确保任务完成
{
pthread_cond_wait(&cond_empty, &mutex);
}
pool = -1;
pthread_cond_signal(&cond_full);
pthread_mutex_unlock(&mutex);
for (int i = 0; i < THRNUM; ++i) {
pthread_join(tid[i], NULL); // 对一个个线程收尸
}
pthread_cond_destroy(&cond_empty);
pthread_cond_destroy(&cond_full);
pthread_mutex_destroy(&mutex); // 使mutex不可用
exit(0);
}
信号量是有一个整数值的对象,可以用两个函数操作它
void mysem_post(mysem_t * s);
// 将信号量中的整数值加1
// 如果信号量中整数的值大于0,则唤醒一个线程
void mysem_wait(mysem_t * s);
// 如果信号量中的整数值小于等于0,则休眠等待
// 否则将信号量中的整数值减1
通过将信号量中整数的初始值设置成 0,可以实现条件变量的功能;将信号量中整数的初始值设置成 1,可以实现互斥量的功能;将信号量中整数的初始值设置成一个正整数,则能够表示“资源数目”
代码需求:
- 使用互斥量和条件变量实现信号量的功能,并封装成库
- 通过我们实现的信号量来完成线程数量的调控:我们希望始终维护筛选质数的线程最多为 N 个。当线程数不足 N 个,可以创建新线程;当线程数等于 N 个,则无法再创建更多线程了;当其中的某个线程结束,就又可以创建新线程
mysem.h,主要定义了接口
#ifndef MYSEM_H__
#define MYSEM_H__
typedef void mysem_t;
// 初始化一个信号量中的整型值,返回该信号量
mysem_t * mysem_init(int initval);
// 加一、若满足条件则唤醒
void mysem_post(mysem_t * s);
// 休眠,直到满足条件,再减一
void mysem_wait(mysem_t * s);
// 销毁信号量
void mysem_destroy(mysem_t * s);
#endif
mysem.c,主要实现了接口
#include
#include
#include
#include "mysem.h"
struct mysem_t
{
int value;
pthread_mutex_t mutex;
pthread_cond_t cond;
};
mysem_t * mysem_init(int initval) {
struct mysem_t *me;
me = malloc(sizeof(*me));
if (me == NULL)
return NULL;
me->value = initval;
pthread_mutex_init(&me->mutex, NULL); // 操作信号量中value应该是原子的
pthread_cond_init(&me->cond, NULL);
return me;
}
void mysem_post(mysem_t * s) {
struct mysem_t * me = s;
pthread_mutex_lock(&me->mutex);
me->value += 1; // 加一
if (me->value > 0) // 若满足条件则唤醒
pthread_cond_broadcast(&me->cond);
pthread_mutex_unlock(&me->mutex);
}
void mysem_wait(mysem_t * s) {
struct mysem_t * me = s;
pthread_mutex_lock(&me->mutex);
while (me->value <= 0) { // 休眠,直到满足value>0的条件
pthread_cond_wait(&me->cond, &me->mutex);
}
me->value -= 1; // 再减一
pthread_mutex_unlock(&me->mutex);
}
void mysem_destroy(mysem_t * s) {
struct mysem_t * me = s;
pthread_mutex_destroy(&me->mutex);
pthread_cond_destroy(&me->cond);
free(me);
}
main.c,主要模拟用户,通过信号量实现线程数量调控
#include
#include
#include
#include
#include
#include "mysem.h"
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1)
#define N 4 // 希望始终维护筛选质数的线程为4个
static mysem_t *sem;
// 如何调控线程个数?将信号量中的整型值看成某个资源的数目!
// 假设每创建一个线程都需要消耗一个资源
// 只要我们设定这个资源的初始数目为4,就可以保证创建的线程数目不会超过4
// 每创建一个线程,资源数目减一
// 当线程结束,资源数目加一
static void *thr_prime(void *p) {
int i, j, mark;
i = (int)p;
mark = 1;
for(j = 2; j < i/2; j++) {
if(i % j == 0) {
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer.\n", i);
// 假定每个线程需要2s完成
sleep(2);
// 增加信号量,归还这个线程消耗的资源
mysem_post(sem);
pthread_exit(NULL);
}
int main(void) {
int i, err;
pthread_t tid[THRNUM];
sem = mysem_init(N);
if(sem == NULL) {
fprintf(stderr, "mysem_init() failed!\n");
exit(1);
}
for(i = LEFT; i <= RIGHT; i++) {
// 减少信号量,消耗一个资源个数以创建线程
mysem_wait(sem);
err = pthread_create(tid+(i-LEFT), NULL, thr_prime, (void *)i);
if(err) {
fprintf(stderr, "pthread_create(): %s\n", strerror(err));
exit(1);
}
}
for (i = LEFT; i <= RIGHT; i++) {
pthread_join(tid[i-LEFT], NULL);
}
mysem_destroy(sem);
exit(0);
}
具体实现需要同时用到信号量与互斥锁,是个综合应用,暂略
在创建线程的那个函数里面,注意到有个 pthread_attr_t 类型的 attr
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 之前都是设置的 NULL 表示选用默认属性创建线程。这部分将着重关注这个 attr
线程具有属性,一个 pthread_attr_t 类型的对象能够代表一个线程的属性。可以往 pthread_create 函数的 attr 形参传入特定的 pthread_attr_t 类型的对象的指针,来创建具有特定属性的线程。注意:在使用 pthread_attr_t 类型对象前需要对其初始化,使用后需要对其去除初始化
#include
int pthread_attr_init(pthread_attr_t *attr);
功能:初始化 pthread_attr_t 类型的对象
#include
int pthread_attr_destroy(pthread_attr_t *attr);
功能:去除初始化 pthread_attr_t 类型的对象
通过下面函数可以设置 attr 所指对象代表的线程属性。详见 man 手册
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t * restrict attr, void ** restrict stackaddr, size_t * restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
int pthread_attr_getscope(const pthread_attr_t *attr, int *contentionscope);
- 打印一个进程所能创建的线程个数上限
- 改变一个线程创建时所需的栈大小,并使其具有自动收尸的属性,并验证
首先打印进程创建的线程个数上限
#include
#include
#include
#include
#include
void* thr(void * p) {
sleep(10000);
}
int main() {
int i;
pthread_t tid;
int err;
for (i = 0; ; ++i) {
err = pthread_create(&tid, NULL, thr, NULL);
if (err) {
fprintf(stderr, "pthread_create():%s\n", strerror(err));
break;
}
}
fprintf(stdout, "threads capacity:%d\n", i);
exit(0);
}
可以看到上限是 4743 个
然后再通过 pthread_attr_t 类型的对象定制一个线程创建时所需的栈大小,并使其具有自动收尸的属性。验证定制成功
#include
#include
#include
#include
#include
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int cur_thread = 0; // 希望按照线程创建的顺序打印线程地址
void* thr(void * p) {
pthread_mutex_lock(&mutex);
while (cur_thread != (int)p) // 如果当前线程不符合希望打印的线程号顺序,休眠
pthread_cond_wait(&cond, &mutex);
fprintf(stdout, "thread[%d] stack address: %p\n",cur_thread, &p);
pthread_cond_broadcast(&cond);
++cur_thread; // 下一个希望打印的线程号
pthread_mutex_unlock(&mutex);
sleep(10);
}
int main() {
int i;
pthread_t tid;
int err;
pthread_attr_t attr;
pthread_attr_init(&attr);
// 使每个线程所需占栈空间大小为1024*1024B=1024kB=1MB
err = pthread_attr_setstacksize(&attr, 1024 * 1024);
if (err)
fprintf(stderr, "pthread_attr_setstacksize():%s\n", strerror(err));
// 使每个线程创建后,默认选择“自动收尸”
err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (err)
fprintf(stderr, "pthread_attr_setdetachstate():%s\n", strerror(err));
for (i = 0; ; ++i) {
err = pthread_create(&tid, &attr, thr, (void*)i)// i作为线程编号
if (err) {
fprintf(stderr, "pthread_create():%s\n", strerror(err));
break;
}
}
pthread_attr_destroy(&attr);
puts("main thread end!");
pthread_exit(0); // 用pthread_exit不会终止进程,用来验证detach的作用
}
我们设置的每个线程所占栈空间大小为 1024 * 1024B = 1048576B,而测试出来两个紧挨着创建的线程中首个变量的地址相差 1114112 字节,约等于 1048576 字节,说明设置成功
在初始化互斥量的那个函数里面,注意到有个 pthread_mutexattr_t 类型的 attr
#include
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 之前都是设置的 NULL 表示选用默认属性初始化互斥量。这部分将着重关注这个 attr
互斥量具有属性,一个 pthread_mutexattr_t 类型的对象能够代表一个互斥量的属性。可以往 pthread_mutex_init 函数的 attr 形参传入特定的 pthread_mutexattr_t 类型的对象的指针,来创建具有特定属性的互斥量。注意:在使用 pthread_mutexattr_t 类型对象前需要对其初始化,使用后需要对其去除初始化
#include
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
功能:初始化 pthread_mutexattr_t 类型的对象
#include
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
功能:去除初始化 pthread_mutexattr_t 类型的对象
通过下面函数可以设置 attr 所指对象代表的互斥量属性。详见 man 手册
int pthread_mutexattr_setprioceiling(pthread_mutexattr_t *attr, int prioceiling);
int pthread_mutexattr_getprioceiling(pthread_mutexattr_t *attr, int *prioceiling);
int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol);
int pthread_mutexattr_getprotocol(pthread_mutexattr_t *attr, int *protocol);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_gettype(pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_setpolicy_np(pthread_mutexattr_t *attr, int policy);
int pthread_mutexattr_getpolicy_np(pthread_mutexattr_t *attr, int *policy);
在初始化条件变量的那个函数里面,注意到有个 pthread_condattr_t 类型的 attr
#include
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); 之前都是设置的 NULL 表示选用默认属性初始化条件变量。这部分将着重关注这个 attr
条件变量具有属性,一个 pthread_condattr_t 类型的对象能够代表一个条件变量的属性。可以通过往 pthread_cond_init 函数的 attr 形参传入特定的 pthread_condattr_t 类型的对象的指针,来创建具有特定属性的条件变量。注意:在使用 pthread_condattr_t 类型对象前需要对其初始化,使用后需要对其去除初始化
#include
int pthread_condattr_init(pthread_condattr_t *attr);
功能:初始化 pthread_condattr_t 类型的对象
#include
int pthread_condattr_destroy(pthread_condattr_t *attr);
功能:去除初始化 pthread_condattr_t 类型的对象
通过一些函数可以设置 attr 所指对象代表的条件变量属性。详见 man 手册
在信号章节,我们曾强调过重入现象(从某函数跳出,再跳回)带来的隐患。但是多线程这部分明明更容易跳来跳去的,为什么我们不再强调重入带来的隐患了呢?
例如三个线程,分别用 puts 向标准输出终端打印连续字符 aaa、bbb 和 ccc,可能出现的情况有:
aaabbbccc aaacccbbb cccbbbaaa // ...
但绝对不会出现下面这些情况:
abcabcabc aabbccabc // ...
因为我们用到的很多函数本身就支持多线程并发!比如标准 IO 中的很多函数内部其实都对操作缓冲区的代码段上了锁,因此我们可以放心地在并发代码中使用这些标准 IO 函数。我们称这种函数是线程安全的
当然,也存在线程不安全的 IO 调用,当考虑到效率问题(省去加锁和解锁的时间),并且确保只有单线程操作缓冲区时,可以使用下面的这些函数,这些函数在后面都加上了 _unlocked,表示内部实现不加锁
#include
int getc_unlocked(FILE *stream);
int getchar_unlocked(void);
int putc_unlocked(int c, FILE *stream);
int putchar_unlocked(int c);
void clearerr_unlocked(FILE *stream);
int feof_unlocked(FILE *stream);
int ferror_unlocked(FILE *stream);
int fileno_unlocked(FILE *stream);
int fflush_unlocked(FILE *stream);
int fgetc_unlocked(FILE *stream);
int fputc_unlocked(int c, FILE *stream);
size_t fread_unlocked(void *ptr, size_t size, size_t n,
FILE *stream);
size_t fwrite_unlocked(const void *ptr, size_t size, size_t n,
FILE *stream);
char *fgets_unlocked(char *s, int n, FILE *stream);
int fputs_unlocked(const char *s, FILE *stream);
这部分我们对并发—信号的一些知识进行重构
对多线程的进程而言,有进程级别的未决信号集 pending,但没有进程级别的信号屏蔽字 mask。而每个线程都有自己的 pending 和 mask(线程级别)
进程向进程发送信号,改变的是进程级别的 pending;线程向线程发送信号,改变的是线程级别的 pending。对于线程级别的信号响应,使用当前线程的 pending 和 mask 进行按位与;对于进程级别的信号响应,使用当前工作线程的 mask 和进程级别的 pending 进行按位与
此前讨论了进程如何使用 sigprocmask 函数来设置信号屏蔽字。然而,sigprocmask 的行为在多线程的进程中并没有定义,线程必须使用 pthread_sigmask 来设置当前线程的信号屏蔽字
#include
// 修改线程级别的信号屏蔽字
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
// 向指定线程发送信号
int pthread_kill(pthread_t thread, int sig);
虽然我们建议尽量不要在多线程编程阶段大范围混用信号机制,但是这里有一个结合了这两个机制的函数还是挺方便的
man 3 sigwait
#include
int sigwait(const sigset_t *set, int *sig);
功能:等待一个信号
The sigwait() function suspends execution of the calling thread until one of the signals specified in the signal set set becomes pending. The function accepts the signal (removes it from the pending list of signals), and returns the signal number in sig.
fork 通过复制进程创建子进程。有些标准下的 fork 是连进程内的所有线程一起复制的;而有些标准下的 fork 仅将调用 fork 的线程复制
这里补充一个函数,也是创建子进程的,但是可以控制子进程复制父进程的哪些部分,哪些又和父进程共享
man 2 clone
#include
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
与 fork(2) 相比,这些系统调用对调用进程和子进程之间共享哪些执行上下文提供了更精确的控制。例如,通过 clone 系统调用,调用者可以控制两个进程是否共享虚拟地址空间、文件描述符表和信号处理程序等等
流水线模式:第一个线程做完后第二个线程做,然后第三个线程做......排队做
工作组模式:任务分配,大伙儿一起做,最后任务汇总
C/S 客户端服务器模式:客户端提交任务,服务器执行任务,然后将任务结果返给客户端
openmp 标准不同于 POSIX 线程标准,是一套指导性编译处理方案。OpenMP 支持的编程语言包括 C、C++ 和 Fortran;而支持 OpenMp 的编译器包括 Sun Compiler,GNU Compiler 和 Intel Compiler 等。OpenMp 提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的 pragma 来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。当选择忽略这些 pragma,或者编译器不支持 OpenMp 时,程序又可退化为通常的程序(一般为串行),代码仍然可以正常运作,只是不能利用多线程来加速程序执行
详见 www.openmp.org
示例
真就每章字数越写越多......日均 6000 字了 TAT
今天蓉城主场收官战!!!
准备去看看,毕竟下赛季可能抢不到套票了,就没这么好的位置了