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

原文来自静雅斋,转载请注明出处。javascript

线程概念

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

线程标识

就像进程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函数的返回值当作退出状态。this

线程终止

线程其实能够当作轻量级的进程,进程若是调用了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函数侦听返回值而且回收空间,可是在不少状况下,咱们建立线程后,不会去管后续,因此就须要使用这个函数对其进行分离。

相关文章
相关标签/搜索