线程之线程标识、创建以及终止

為{幸葍}努か 提交于 2020-02-28 19:14:29

本文来自个人博客:https://dunkwan.cn

线程标识

就像每个进程有一个进程ID一样,每个线程也都有自己的线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
线程ID是由pthread_t数据类型来表示的,实现的时候可以用一个结构来代表pthread_t数据类型,所以可移植的操作系统实现不能把它作为整数处理。因此必须使用一个函数来对两个线程ID进行比较。

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
返回值:若相等,返回非0数值;否则,返回0

Linux 3.2.0使用无符号长整型表示pthread_t数据类型。

Mac OS X 10.6.8 使用一个指向pthread结构的指针来表示pthread_t数据类型。

pthread_self函数用于获取自身的线程ID。

#include <pthread.h>
pthread_t pthread_self(void);
返回值:调用线程的线程ID。

线程创建

传统UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程中只包含一个线程是相同的。在POSIX线程的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。

pthread_create函数可用于新增线程的创建。

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
返回值:若成功,返回0;否则,返回错误编号。

tidp指向存有新建线程的线程ID的内存单元。

attr参数用于定制各种不同的线程属性。

start_rtn参数为函数的地址,新建线程从此函数开始运行,该函数只有一个无类型指针参数arg。如果需要向该函数传递一个以上的参数,需要将这些参数存放在一个结构中,然后将这个结构的地址作为arg参数传入。

测试示例:
创建一个线程,打印进程ID、新线程的线程ID以及初始线程的线程ID。

#include "../../include/apue.h"
#include <pthread.h>

pthread_t ntid;

void printids(const char *s)
{
    pid_t pid;
    pthread_t tid;

    pid = getpid();
    tid = pthread_self();
    printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid, 
           (unsigned long)tid, (unsigned long)tid);
}

void *thr_fn(void *arg)
{
    printids("new threads: ");
    return ((void *)0);
}

int main(void)
{
    int err;

    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if(err != 0)
        err_exit(err, "can't create thread");
    printids("main thread: ");
    sleep(1);
    return 0;
}

结果如下:

线程终止

单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。

  1. 线程可以简单地从启动例程中返回,返回值是线程的退出码。
  2. 线程可以被同一进程中的其他线程取消。
  3. 线程调用pthread_exit
#include <pthread.h>
void pthread_exit(void *rval_ptr);

调用pthread_join函数可自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
返回值:若成功,返回0;否则,返回错误编号。

测试示例1:

如何获取已终止的线程的退出码。

#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(void)
{
    int err;
    pthread_t tid1, tid2;
    void *tret;
    /* create new thread  */
    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");

    /* get exit code */
    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);

    return 0;
}

结果如下:

测试示例2:

用自动变量作为pthread_exit的参数时出现的问题。

#include "../../include/apue.h"
#include <pthread.h>

struct foo{
    int a, b, c, d;
};

void printfoo(const char *s, const struct foo *fp)
{
    printf("%s", s);
    printf(" structure at 0x%lx\n", (unsigned long)fp);
    printf(" foo.a = %d\n", fp->a);
    printf(" foo.b = %d\n", fp->b);
    printf(" foo.c = %d\n", fp->c);
    printf(" foo.d = %d\n", fp->d);
}

void *thr_fn1(void *arg)
{
    struct foo foo = {1, 2, 3, 4};

    printfoo("thread 1:\n", &foo);
    pthread_exit(&foo);
}

void *thr_fn2(void *arg)
{
    printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
    pthread_exit((void *)0);
}

int main(void)
{
    int err;
    pthread_t tid1, tid2;
    struct foo *fp;

    err = pthread_create(&tid1, NULL, thr_fn1, NULL);
    if(err != 0)
        err_exit(err, "can't create thread 1");
    err = pthread_join(tid1, (void **)&fp);
    if(err != 0)
        err_exit(err, "can't join with thread 1");
    sleep(1);
    printf("parent starting second thread\n");
    err = pthread_create(&tid2, NULL, thr_fn2, NULL);
    if(err != 0)
        err_exit(err, "can't create thread 2");
    sleep(1);
    printfoo("parent:\n", fp);

    return 0;
}

结果如下:

从上图可知tid1的结构内容被tid2线程所修改。

pthread_cancel函数用于请求取消同一进程中的其他线程。

#include <pthread.h>
int pthread_cancel(pthread_t tid);
返回值:若成功,返回0;否则,返回错误编号。

线程可以安排它退出时需要调用的函数,这进程在退出时可以用atexit函数安排退出类似。这样的函数称为线程清理处理程序。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数rtn是由pthread_cleanup_push函数调度的,调用时只有一个参数args

  • 调用pthread_exit时;
  • 响应取消请求时;
  • 用非零execute参数调用pthread_cleanup_pop时。

如果execute参数设置为0,清理函数将不被调用。不管发生上面哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理程序。

这些函数有一个限制,由于它们可以实现为宏,所以必须在与线程相同的作用域中以匹配对的形式使用。pthread_cleanup_push的宏定义可以包含字符{,这种情况下,在pthread_cleanup_pop的定义中要有相应的匹配字符}

测试示例:

如何使用线程清理程序。

#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, (void *)"thread 1 first handler");
    pthread_cleanup_push(cleanup, (void *)"thread 1 second handler");
    printf("thread 1 push complete\n");
    if(arg)
        return ((void *)1);
    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, (void *)"thread 2 first handler");
    pthread_cleanup_push(cleanup, (void *)"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(void)
{
    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);
    return 0;
}

结果如下:

由上图可知,thr_fn1中线程退出方式是如上述的第一种的方式进行退出的即从启动例程中返回,因此第一个线程的线程处理程序并未被调用。那不妨来看看将其退出方式改为第三种是否会被执行吧?

上面测试代码中修改如下部分:

void *thr_fn1(void *arg)
{
    printf("thread 1 start\n");
    pthread_cleanup_push(cleanup, (void *)"thread 1 first handler");
    pthread_cleanup_push(cleanup, (void *)"thread 1 second handler");
    printf("thread 1 push complete\n");
    if(arg)
        pthread_exit((void *)1);
        //return ((void *)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    //return ((void *)1);
    pthread_exit((void *)1);
}

结果如下:

正如上面猜测一样,将其退出方式改为第三种即使用pthread_exit函数退出时,第一个线程的清理程序得以执行。

下图是线程函数与进程函数的相似之处。

在默认情况下,线程终止状态会保存直到该线程调用pthread_join。如果线程已经被分离,线程的底层资源可以在线程终止时立即被收回。在线程被分离后,我们不能用pthread_join函数等待它的终止状态,因为分离状态的线程调用pthread_join会产生未定义行为。可以调用pthread_detach分离线程。

#include <pthread.h>
int pthread_detach(pthread_t tid);
返回值:若成功,返回0;否则,返回错误编号。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!