线程(十一)上

线程

线程的一些基础知识,包括线程的创建、线程终止

线程的概念

典型的UNIX进程可以看成只有一个控制线程;一个进程在某一时刻只能做一件事情。有了多个控制线程后,在程序设计时就可以把进程设计成在某一时刻做不止一件事,每个线程处理各自独立的任务。这种方法有很多好处

  1. 简化处理异步事件的代码
  2. 分解复杂的问题提高整个程序的吞吐量
  3. 使用多线程改善响应时间

每个线程都包含有表示执行环境所必须的信息。其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。

线程标识

每个线程都有一个线程ID。但跟进程不一样,进程ID在整个系统中是唯一的,线程ID只有在它所属的进程上下文中才有意义。

线程ID通过使用pthread_t数据类型表示,必须要使用一个函数对两个线程ID进行比较。

1
2
3
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 相等返回非零,不同返回0

线程通过调用pthread_self函数获得自身的线程ID

1
2
#include <pthread.h>
pthread_t pthread_self(void);

线程创建

新增的线程可以通过调用pthread_create函数创建。

1
2
3
4
5
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *), void *restrict arg);
//成功返回0,失败返回错误编号

  • 当pthread_create成功返回时,新创建线程的线程ID会被设置成tidp指向的内存单元。attr用于定制各种不同的线程属性(第12章介绍),设置为NULL,表示创建一个具有默认属性的线程。
  • 新创建的线程从start_rtn函数的地址开始执行,该函数只有一个无类型指针参数arg。如果有多个参数传递,需要把这些参数放到一个结构中,然后传递这个结构的地址进去。
  • 线程创建时不能保证执行顺序

如下程序创建了一个线程,打印了进程ID、新线程的线程ID以及初始线程的线程ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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 thread:");
return ((void *)0);
}

int main()
{
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);
exit(0);
}

运行结果如图所示

可以看到打印出了相同的进程ID,但在mac系统上线程ID不在相同地址段范围

线程终止

如果进程中的任意线程调用了exit、_Exit或者_exit,那么整个进程就会终止。
单个线程可以通过3种方式退出,可以在不终止整个进程的情况下,停止它的控制流。

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

rval_ptr参数是一个无类型指针,进程中的其他线程可以通过调用pthread_join函数访问到这个指针

1
2
3
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
//成功返回0,失败返回错误编号
  • 调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果线程简单地从它的启动例程返回,rval_ptr就包含返回码。如果线程被取消,由rval_ptr指定的内存单元就设置为PTHREAD_CANCELED。
  • 通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join就会调用失败,返回EINVAL

如下程序展示了如何获取已经终止线程的退出码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "apue.h"
#include <pthread.h>

void *thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return((void *)0);
}
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;
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("thred 2 exit code %ld\n",(long)tret);
exit(0);
}

运行结果如下:

线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程

1
2
3
#include <pthread.h>
int pthread_cancel(pthread_t tid);
//成功返回0,失败返回错误编号

pthread_cancel并不等待线程终止,它仅仅提出请求

线程可以安排它退出时需要调用的函数(线程清理处理程序)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,他们的执行顺序与他们注册时相反。

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

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

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

如果execute参数设置为0,清理函数将不被调用。

如下程序是使用线程清理处理程序的例子

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

}

int main(int argc, const char *args[])
{
    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);
}

运行结果如下

mac 下会产生core文件。这是因为在mac平台上,pthread_cleanup_push是用宏实现的,而宏把某些上下文存放到栈上。当线程1在调用pthread_cleanup_push和调用pthread_cleanup_pop之间返回时,栈已被改写,而这个平台在调用清理处理程序时就用了这个被改写的上下文。在Single UNIX Specification中,函数如果在调用pthread_cleanup_push和pthread_cleanup_pop之间返回,会产生未定义行为。唯一的可移植方法是调用pthread_exit.

感谢土豪
0%