[单刷APUE系列]第十一章——线程[1]

 2023-09-05 阅读 35 评论 0

摘要:线程概念 在前面的章节,都是以多进程单线程概念来讲解的,特别是早期的Unix环境,没有引入线程模型,所以无所谓线程概念,也就是一个进程在某一时刻只能做一件事情,而多线程则是可以让进程拥有多个线程,这样进程就能在某一时刻

线程概念

在前面的章节,都是以多进程单线程概念来讲解的,特别是早期的Unix环境,没有引入线程模型,所以无所谓线程概念,也就是一个进程在某一时刻只能做一件事情,而多线程则是可以让进程拥有多个线程,这样进程就能在某一时刻做不止一件事情。线程的好处和缺点就不多说了,相信各位应该都有体会了。
当然,多线程和多处理器或者多核是无关的,多线程的出现是为了解决异步和并行,即使是运行在单核心上,也能得到性能提升,例如,当IO线程处于阻塞状态,其他的线程就能抢占CPU,从而得到资源有效利用。
在前面的章节中,也介绍了进程内存空间是如何的,具体包含了那些内容,而多线程的引入,则将其内容扩充了。通常情况下,谈论Unix环境的多线程就是特指pthread,一个进程在启动的时候只有一个控制线程,而用户可以通过系统提供的API创建管理线程,实际上,线程是非常轻量化的,进程的正文段、数据段等实际上都是共享的,包括了全局内存啊文件描述符啊,这些资源实际上都是共享的,也就是说,线程虽然创建管理销毁很容易,但是也会导致资源抢占的问题,线程主要是在内核空间中寄存器等东西需要占用内存。

线程标识

就像进程ID一样,线程也有自己的ID,叫做线程ID。进程ID相对于这个系统而言,而线程ID则是相对于进程ID而言,两个进程的同一个线程ID是没有可比性的。
在现代的Unix环境中,系统已经提供了pthread_t数据类型作为线程ID的存储类型,由于不同的Unix环境的实现不同,有些是使用整形,有些是使用一个结构体,所以为了保证可移植性,我们不能直接去操作这个数据类型。

int pthread_equal(pthread_t t1, pthread_t t2);
pthread_t pthread_self(void);

一个是比较函数,一个是获取线程自身的线程ID,当然,由于线程ID的数据结构不确定性,所以在调试输出的时候很麻烦,通常的做法就是使用第三方调试库,或者自己写一个调试函数,根据当前系统来确定是输出结构体还是整形。

线程创建

前面说过,进程创建的时候一般只有一个线程,当需要多线程的时候需要开发者自行调用函数库来创建管理销毁,新的线程创建函数如下

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

其实原著中有一些翻译错误,例如,原著中这么写

当 pthread_create 成功返回时,新创建线程的线程ID会被设置成 tidp 指向的内存单元

这句话非常让人费解,实际上Unix手册是这么讲的

The pthread_create() function is used to create a new thread, with attributes specified by attr, within a process.  If attr is NULL, the default attributes are used.  If the attributes specified by attr are modified later, the thread's attributes are not affected.  Upon successful completion, pthread_create() will store the ID of the created thread in the location specified by thread.

pthread_create函数被用来创建一个新的线程,并且会应用attr参数指定的属性,如果attr参数为null,则会使用默认的属性,后续对attr参数的修改不会影响以创建线程的属性。当函数成功返回的时候,pthread_create函数将会把线程ID存储在thread参数的内存位置。这样大家应该就明白了。

Upon its creation, the thread executes start_routine, with arg as its sole argument.  If start_routine returns, the effect is as if there was an implicit call to pthread_exit(), using the return value of start_routine as the exit status.  Note that the thread in which main() was originally invoked differs from this.  When it returns from main(), the effect is as if there was an implicit call to exit(), using the return value of main() as the exit status.

当创建后,线程执行start_routine参数指定的函数,并且将arg参数作为其唯一参数,如果start_routine函数返回了,就是隐含了pthread_exit()函数的调用,并且将start_routine函数的返回值作为退出状态。注意,main函数中唤起的线程和这种方式创建的线程是有区别的,当main函数返回的时候,就隐含了exit()函数的调用,并且将main函数的返回值当做退出状态。

线程终止

线程其实可以当做轻量级的进程,进程如果调用了exit_Exit或者_exit,则进程会终止,而线程也可以终止,单个线程可以使用一下三种方式退出

  1. 线程返回,返回值是线程退出码

  2. 线程被同一进程的其他线程取消

  3. 线程调用pthread_exit

void pthread_exit(void *value_ptr);The pthread_exit() function terminates the calling thread and makes the value value_ptr available to any successful join with the terminating thread.

从上面我们好像看到了一些新的内容,提到了successful join,其实是一个类似wait的函数。

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

前面提到了线程有三种方式结束,线程返回、线程取消、使用pthread_exit函数。如果是简单的返回,那么rval_ptr就会包含返回码,如果线程被取消,则rval_ptr将被设置为PTHREAD_CANCELED

#include "include/apue.h"
#include <pthread.h>void *thr_fn1(void *arg)
{printf("thread 1 returning\n");return((void *)1);
}void *thr_fn2(void *arg)
{printf("thread 2 exiting\n");pthread_exit((void *)2);
}int main(int argc, char *argv[])
{int err;pthread_t tid1, tid2;void *tret;err = pthread_create(&tid1, NULL, thr_fn1, NULL);if (err != 0)err_exit(err, "can't create thread 1");err = pthread_create(&tid2, NULL, thr_fn2, NULL);if (err != 0)err_exit(err, "can't create thread 2");err = pthread_join(tid1, &tret);if (err != 0)err_exit(err, "can't join with thread 1");printf("thread 1 exit code %ld\n", (long)tret);err = pthread_join(tid2, &tret);if (err != 0)err_exit(err, "can't join with thread 2");printf("thread 2 exit code %ld\n", (long)tret);exit(0);
}

运行后的结果如下

thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2

除了能看到pthread_exit和return都是一样的效果以外,我们还能发现一些书上没有提到的东西。比如,线程退出后依旧会等待进程进行清理工作,或者我们可以类比父子进程,主线程创建了子线程,所以子线程需要等待父线程使用函数清理回收,而且pthread_join函数是一个阻塞函数,当然,实际的线程工作当然不是如同这样的。
在对线程函数的查看中我们可以看到,无论是参数还是返回值,都是一个无类型指针,这代表着我们可以传递任何的数据。但是,请记住,C语言编程是存在栈分配和堆分配的,如果是栈分配的变量,我们需要考虑到访问的时候内存是否已经被回收了,所以,像这类的情况,基本都是使用堆分配变量手动管理内存的。

int pthread_cancel(pthread_t thread);

pthread_cancel函数会发起一个取消请求给thread参数指定的线程,目标线程的取消状态和类型确定了取消过程发生的时间。当取消过程生效的时候,目标线程的取消清理函数将会被调用。当最后一个取消清理函数返回的时候,指定线程的数据析构函数将会被调用,当最后一个数据析构函数返回的时候,线程将会终止。
当然,pthread_cancel函数是异步请求,所以不会等待线程的完全终止。最终如果使用pthread_join函数侦听线程结束,实际上会得到PTHREAD_CANCELED常量。

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

就像进程退出会有进程清理函数一样,线程退出也会有线程清理函数,从上面的函数名称中也能猜出来实际上使用的是栈来存储函数指针。也就是说,注册的顺序和调用的顺序是反过来的。
pthread_cleanup_push函数将routine函数指针压入栈顶,当当前线程退出的时候被调用,换言之,这个函数实际上是针对当前线程的行为。
pthread_cleanup_pop函数弹出当前栈顶的routine清理函数,如果execute参数为非0,将会执行这个清理函数,如果不存在清理函数,则pthread_cleanup_pop将不会做任何事情。

pthread_cleanup_push() must be paired with a corresponding pthread_cleanup_pop(3) in the same lexical scope.

pthread_cleanup_push函数需要和pthead_cleanup_pop函数在一个作用域内配对使用,原著对此给出的解释是这两个函数可能是以宏定义的形式实现的。
注意:这两个函数只会在pthraed_exit()返回的时候被调用,如果是线程函数返回,则不会调用。
而且,经过实际测试,苹果系统下确实是通过宏定义实现这两个函数的。所以,如果在这两个函数尚未调用的时候就返回的话,会导致段错误。根据猜想,应该是返回的时候栈被改写了,但是清理函数仍然会继续调用。
以下是我自己的代码

#include "include/apue.h"
#include <pthread.h>void cleanup(void *arg)
{printf("cleanup: %s\n", (char *)arg);
}void *thr_fn1(void *arg)
{printf("thread 1 start\n");pthread_cleanup_push(cleanup, "thread 1 first handler");pthread_cleanup_push(cleanup, "thread 1 second handler");printf("thread 1 push complete\n");pthread_cleanup_pop(0);pthread_cleanup_pop(0);return((void *)1);
}void *thr_fn2(void *arg)
{printf("thread 2 start\n");pthread_cleanup_push(cleanup, "thread 2 first handler");pthread_cleanup_push(cleanup, "thread 2 second handler");printf("thread 2 push complete\n");if (arg)pthread_exit((void *)2);pthread_cleanup_pop(0);pthread_cleanup_pop(0);pthread_exit((void *)2);
}int main(int argc, char *argv[])
{int err;pthread_t tid1, tid2;void *tret;err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);if (err != 0)err_exit(err, "can't create thread 1");err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);if (err != 0)err_exit(err, "can't create thread 2");err = pthread_join(tid1, &tret);if (err != 0)err_exit(err, "can't join with thread 1");printf("thread 1 exit code %ld\n", (long)tret);err = pthread_join(tid2, &tret);if (err != 0)err_exit(err, "can't join with thread 2");printf("thread 2 exit code %ld\n", (long)tret);exit(0);
}

笔者在这里将原著的第一个线程的代码改了,令其能执行完pthread_cleanup_pop()函数以后在执行return语句,就不存在错误了,但是依旧不会执行清理代码。

~/Development/Unix » ./a.out
thread 1 start
thread 2 start
thread 1 push complete
thread 2 push complete
cleanup: thread 2 second handler
cleanup: thread 2 first handler
thread 1 exit code 1
thread 2 exit code 2

所以在开发中,如果使用了清理函数,则应当使用pthread_exit()函数返回。
我们知道,进程如果终止了,则需要父进程执行清理工作,而线程如果终止了,那么线程的终止状态将会保存直到pthread_join函数的调用,但是如果使用pthread_detach函数将线程分离,则线程退出时候将会立刻回收存储资源

int pthread_detach(pthread_t thread);The pthread_detach() function is used to indicate to the implementation that storage for the thread thread can be reclaimed when the thread terminates.  If thread has not terminated, pthread_detach() will not cause it to terminate.  The effect of multiple pthread_detach() calls on the same target thread is unspecified.

pthread_detach函数被用来标识一个线程可以在终止后回收存储空间,如果线程没有终止,pthread_detach将不会导致线程终止。
实际上是这样的,当一个线程创建的时候,默认是joinable的,所以就像进程一样,如果终止了,则需要手动使用pthread_join函数侦听返回值并且回收空间,但是在很多情况下,我们创建线程后,不会去管后续,所以就需要使用这个函数对其进行分离。

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/5/1211.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息