多线程与处理器的核心数无关,即使单核处理器也可以运行多线程。

多线程的设计有很多优点:

  1. 简化处理异步事件
  2. 共享内存和文件描述符
  3. 提高程序吞吐量
  4. 提高交互程序的响应等等

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

进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符

Linux下的线程模型由POSIX.1-2001提供,线程接口也称为pthread,功能检测方式有宏定义#ifdef _POSIX_THREADSsysconf常量_SC_THREADS

#include <stdio.h>
#include <unistd.h>

int main()
{
#ifdef _POSIX_THREADS
    printf("support posix thread\n");
#else
    printf("unsupport posix thread\n");
#endif

    int iRet = sysconf(_SC_THREADS); // 支持选项返回正值
    printf("%d\n", iRet);
}
// support posix thread
// 200809

线程标识

类似于进程ID,在系统中唯一,而线程ID用于标识线程,在整个进程上下文中是唯一的

线程ID使用数据类型pthread_t来表示,该数据类型依赖于系统的具体实现,可能是整数也可能是结构体(唯一后果是无法打印该数据类型的值)。

/* Thread identifiers.  The structure of the attribute type is not
   exposed on purpose.  */
typedef unsigned long int pthread_t;

可以使用接口pthread_equal()对两个线程ID进行比较,如下所示:

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
// RETURN VALUE
// If the two thread IDs are equal, pthread_equal() returns a nonzero value; 
// otherwise, it returns 0.

可以使用接口pthread_self()获得自身的线程ID,如下所示:

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

// RETURN VALUE
// This function always succeeds, returning the calling thread's ID.

使用场景

当线程需要识别以线程ID作为标识的数据结构时,需要将pthread_self()接口与pthread_equal()接口联合使用。

pthread_self()用于获取当前线程IDpthread_equal()用于对比当前线程ID和数据结构中的线程ID

例如,主线程用于将工作任务添加到队列中,用线程ID来控制每个工作线程处理那些作业。

如下图所示,主线程在每个待处理作业的结构中放置处理该作业的线程ID,每个工作线程只能移出相对应的作业

创建线程

传统Unix进程模型中,每个进程只有一个控制线程,与基于线程模型中每个进程只包含一个线程是相同的。

在POSIX线程模型中,进程刚开始是以单进程中单个控制线程启动运行的,在创建其他控制线程之前和传统Unix进程模型一致。

创建线程可以通过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);
// RETURN VALUE
// On success, pthread_create() returns 0; 
// on error, it returns an error number, and the contents of *thread are undefined.

pthread_create成功返回之后,tidp指向的内存单元保存新线程ID,attr指定新线程的不同属性,start_rtn是函数指针,指向新线程运行的起始地址,arg是传递给start_rtn的参数。

新线程可以访问进程的地址空间,并继承调用线程的浮点环境信号屏蔽字,但是该线程的挂起信号集会被清除

pthread接口调用失败时会直接返回错误码,而不会像其他POSIX接口一样设置errno,这样做的好处在于可以直接从返回码中判断哪个接口执行失败

为了兼容使用errno的接口,每个线程都提供了errno副本,这是因为errno是全局变量,多线程之间会相互影响。

下面是线程创建实例:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_t ntid;  // 保存新创建线程id

static void printtids(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* start_rtn(void* arg)
{
    printtids("new thread: ");
    return ((void*)0);
}

int main()
{
    int err;
    err = pthread_create(&ntid, NULL, start_rtn, NULL);
    if (err != 0)
        printf("%d can't create thread", err);
    printtids("main thread: ");
    sleep(1);
    return 0;
}
// main thread:  pid 12071 tid 139843092297536 (0x7f2fc1d73740)
// new thread:  pid 12071 tid 139843088676544 (0x7f2fc19ff6c0)

新线程是使用pthread_self()接口来获取线程ID,而非从全局变量或线程参数中获取,这其中的原因在于不确定新线程的运行时间,如果新线程在pthread_create返回之间就运行了,那么参数可能是为初始化的。

其次Linux中虽然是用unsigned long int来标识线程ID,但更像是是进程地址空间的某个地址。

终止线程

终止进程的三种方式:

  1. 进程中的任意线程调用了exit_Exit以及_exit接口。
  2. 进程接收到信号,并且该信号的处理动作是退出进程。
  3. 主线程从main函数中返回。

在不终止进程的情况下,终止线程的方式有以下三种:

  1. 线程执行结束正常返回,返回值是线程的退出码。
  2. 线程被进程中另一线程取消,通过调用pthread_cancel接口。
  3. 线程调用pthread_exit接口主动退出。

线程终止方式之pthread_exit

当前线程可以通过调用pthred_exit()来退出,并设置终止状态。

#include <pthread.h>
[[noreturn]] void pthread_exit(void *retval);
// 无返回值

retvalvoid*类型的指针,可以指向任何类型的数据,其指向的数据将作为线程退出时的返回值。

如果线程不需要返回任何数据,将retval参数置为NULL即可。

retval指针不能指向函数内部的局部数据(比如局部变量),这是因为该指针会被多线程进行访问,局部变量在退出时会被释放。

线程终止方式之pthread_cancel

线程可以调用pthread_cancel()接口来请求取消同进程中的其他线程。

#include <pthread.h>
int pthread_cancel(pthread_t thread);
// RETURN VALUE
// On success, pthread_cancel() returns 0; on error, it returns a nonzero error number.

默认情况下,pthread_cancel接口取消thread线程的行为与线程调用pthread_exit(PTHREAD_CANCELED)相同,但是线程可以选择忽略取消或者控制如何被取消

线程控制被取消的方式

线程可以指定其退出时需要执行的函数,这种函数被称为线程清理处理程序(thread cleanup handler)。

线程可以建立多个清理处理程序,该处理程序记录在栈中,因而调用顺序和注册顺序相反。

线程可以通过pthread_cleanup_push()注册清理函数(清理函数压栈),使用pthread_cleanup_pop()接口去除清理函数(清理函数出栈)。

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

routine是清理函数的起始地址,arg是传递给清理函数的参数。

清理函数出栈并执行的条件有以下三个:

  1. 调用pthread_exit时。
  2. 响应取消请求信号时。
  3. 用非零execute参数调用pthread_cleanup_pop接口时。

如果execute为0调用pthread_cleanup_pop接口,清理函数仅出栈但并不执行。

Linux使用宏定义实现这两个接口,因此这两个接口需要成对使用,否则会编译不过。

// pthread.h
#  define pthread_cleanup_push(routine, arg) \
  do {                                                                        \
    __pthread_cleanup_class __clframe (routine, arg)

#  define pthread_cleanup_pop(execute) \
    __clframe.__setdoit (execute);                                            \
  } while (0)

清理函数栈使用栈空间,这样即使中间return退出线程并不执行pthread_cleanup_pop时也不会导致内存泄漏。

下面是另一种实现方式:

#define pthread_cleanup_push(routine,arg)                                       
  { struct _pthread_cleanup_buffer _buffer;                                     
    _pthread_cleanup_push (&_buffer, (routine), (arg));  
#define pthread_cleanup_pop(execute)                                            
    _pthread_cleanup_pop (&_buffer, (execute)); }

下面例子验证了return不会调用清理函数,pthread_exit()退出线程后会调用清理函数。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

static void cleanup1(void* arg)
{
    printf("cleanup1: %s\n", (char*)arg);
}
static void cleanup2(void* arg)
{
    printf("cleanup2: %s\n", (char*)arg);
}
static void cleanup3(void* arg)
{
    printf("cleanup3: %s\n", (char*)arg);
}

static void* start_rtn1(void* arg)
{
    printf("thread1 start\n");
    pthread_cleanup_push(cleanup1, "thread1 first handler");
    pthread_cleanup_push(cleanup2, "thread1 second handler");
    pthread_cleanup_push(cleanup3, "thread1 third handler");
    printf("thread1 push complete\n");
    if (arg != NULL)
        return ((void*)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return ((void*)1);
}
static void* start_rtn2(void* arg)
{
    printf("thread2 start\n");
    pthread_cleanup_push(cleanup1, "thread2 first handler");
    pthread_cleanup_push(cleanup2, "thread2 second handler");
    pthread_cleanup_push(cleanup3, "thread2 third handler");
    printf("thread2 push complete\n");
    if (arg != NULL)
        pthread_exit((void*)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return ((void*)1);
}

int main()
{
    int err;
    pthread_t tid1, tid2;
    void* tret;

    err = pthread_create(&tid1, NULL, start_rtn1, (void*)1);
    if (err != 0)
        printf("can't create thread1\n");

    err = pthread_create(&tid2, NULL, start_rtn2, (void*)1);
    if (err != 0)
        printf("can't create thread2\n");

    err = pthread_join(tid1, &tret);  // 传递二维指针,为了接收指针值,类似于char**
    if (err != 0)
        printf("can't join with thread1\n");
    printf("thread1 exit code %ld\n", (long)tret);

    err = pthread_join(tid2, &tret);  // 传递二维指针,为了接收指针值,类似于char**
    if (err != 0)
        printf("can't join with thread2\n");
    printf("thread2 exit code %ld\n", (long)tret);
    return 0;
}
// thread1 start
// thread1 push complete
// thread2 start
// thread2 push complete
// thread1 exit code 1
// cleanup3: thread2 third handler  从这儿看出清理函数的调用顺序和执行顺序相反
// cleanup2: thread2 second handler
// cleanup1: thread2 first handler
// thread2 exit code 1

获取线程终止状态

进程中其他线程通过调用pthread_join()接口可以访问保存线程终止状态retval指针,此时调用线程会阻塞,直到thread参数指定的线程以上述三种方式退出。

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

// RETURN VALUE
// On success, pthread_join() returns 0; on error, it returns an error number.

默认情况下,线程的终止状态会保存直到对该线程调用pthread_join

pthread_join()接口还可以将线程设置为分离状态,处于分离状态的线程在执行结束时会由系统回收资源。已处于分离状态的线程再调用该接口会返回EINVAL,表示错误。

因此线程分离之后,不能再用pthread_join接口等待其终止状态。

多个线程对同一线程调用pthread_join,其行为是未定义的。 如果调用pthread_join的线程被取消,则pthread_join的目标线程状态不变,可以重新设置分离状态。

  1. 如果线程正常返回,则retval中包含返回码。
  2. 如果使用pthread_exit()接口退出线程,retval中存储就是传递给pthread_exit()的参数。
  3. 如果线程被取消,retval的内存单元就设置为PTHREAD_CANCELED

具体的实现方式是将这三种退出方式的线程终止状态保存在全局变量中,其他进程通过pthread_join可以访问到该全局变量

如果return返回的整数值,则会将整数值存储在void* retval中。

整数与void*的转换

void*是无类型指针,指向一块内存地址,对于32位系统为32位,对于64位系统为64位的。

当将一个整形数据强制转换为void*时,亦即将该整形数据当作地址来处理,并且可以获取该地址(该地址可能是无效的),但是只要不获取该地址上的值时,就不会出现运行时错误。

#include <stdio.h>
int main()
{
    long a = 12;
    void* p;  // 未初始化,随机分配地址
    // 将void*地址打印,得到地址的整型值
    printf("%ld\n", (long)p);

    // 此时将整形变量a强制转换为void*,系统中该地址并不存在
    // 但我们并不关心该地址上存储的是什么,只在乎地址是什么
    // 只要不获取该地址上的值,就不会出错
    p = (void*)a;
    // 因此可以使用void*存储一个整型值
    printf("%ld\n", a);
}
// 139809002932912
// 12

获取线程退出状态的实例

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* start_rtn1(void* arg)
{
    printf("thread1 returning\n");
    return ((void*)1);
}

void* start_rtn2(void* arg)
{
    printf("thread2 exiting\n");
    pthread_exit((void*)2);
}

int main()
{
    int err;
    pthread_t tid1, tid2;
    void* tret;

    err = pthread_create(&tid1, NULL, start_rtn1, NULL);
    if (err != 0)
        printf("can't create thread1\n");

    err = pthread_create(&tid2, NULL, start_rtn2, NULL);
    if (err != 0)
        printf("can't create thread2\n");

    err = pthread_join(tid1, &tret);  // 传递二维指针,为了接收指针值,类似于char**
    if (err != 0)
        printf("can't join with thread1\n");
    printf("thread1 exit code %ld\n", (long)tret);

    err = pthread_join(tid2, &tret);
    if (err != 0)
        printf("can't join with thread2\n");
    printf("thread2 exit code %ld\n", (long)tret);
    return 0;
}
// thread1 returning
// thread1 exit code 1
// thread2 exiting
// thread2 exit code 2

线程在给retval指针赋值时最重要的一点是在线程退出之后该指针指向的内存地址仍有效

栈空间地址作为pthread_exit参数实例

#include <pthread.h>
#include <stdio.h>
#include <unistd.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* start_rtn1(void* arg)
{
    struct foo foo = {1, 2, 3, 4};
    printfoo("thread1: \n", &foo);
    pthread_exit((void*)&foo);
}

void* start_rtn2(void* arg)
{
    printf("thread2: ID is 0x%lx\n", (unsigned long)pthread_self());
    pthread_exit((void*)0);
}

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

    err = pthread_create(&tid1, NULL, start_rtn1, NULL);
    if (err != 0)
        printf("can't create thread1\n");

    err = pthread_join(tid1, (void*)&fp);  // 传递二维指针,为了接收指针值,类似于char**
    if (err != 0)
        printf("can't join with thread1\n");

    sleep(1);
    printf("parent starting thread2\n");

    // 这儿注意的是线程2的栈是如何覆盖线程1的栈空间的
    err = pthread_create(&tid2, NULL, start_rtn2, NULL);
    if (err != 0)
        printf("can't create thread2\n");

    sleep(1);
    printfoo("parent: \n", fp);
    return 0;
}
// thread1:
//  structure at 0x7f1a559feeb0
//  foo.a=1
//  foo.b=2
//  foo.c=3
//  foo.d=4
// parent starting thread2
// thread2: ID is 0x7f1a559ff6c0
// parent:
//  structure at 0x7f1a559feeb0 经过线程2之后,线程1的栈空间虽然可以访问,但是内容已经发生了变化
//  foo.a=0
//  foo.b=0
//  foo.c=-120
//  foo.d=-1

从上述例子来看,必须使用全局变量或者堆空间地址作为pthread_exit的参数,用于返回给调用进程。

总结

进程原语线程原语描述
forkpthread_create创建新的控制流
exitpthread_exit从现有控制流中退出
waitpidpthread_join从控制流中得到退出状态
atexitpthread_cleanup_push注册在退出控制流时调用的接口
getpidpthread_self获取控制流的ID
abortpthread_cancel请求控制流的非正常退出

线程的分离方式pthread_detach,分离之后的线程结束会立即释放资源,无法使用pthread_join获取终止状态。

#include <pthread.h>
int pthread_detach(pthread_t thread);
// RETURN VALUE
// On success, pthread_detach() returns 0; on error, it returns an error number.