SUS定义了一些系统层面上对于线程的限制,比如进程可以创建的最大线程数、线程栈可用的最小字节数等等。

pthread接口允许我们传入线程或同步对象的属性来调节线程或同步对象的行为。

系统限制

SUS定义了下面4个线程相关的限制,以增强不同系统之间的可移植性。

这些限制的值均可通过sysconf函数来获取。

限制名称描述name参数
PTHREAD_DESTRUCTOR_ITERATIONS线程退出时操作系统试图销毁线程特定数据的最大次数_SC_THREAD_DESTRUCTOR_ITERATIONS
PTHREAD_KEYS_MAX进程可以创建的键的最大数目_SC_THREAD_KEYS_MAX
PTHREAD_STACK_MIN一个线程的栈可用最小字节数_SC_THREAD_STACK_MIN
PTHREAD_THREADS_MAX进程可以创建的最大线程数_SC_THREAD_THREADS_MAX

下面的例子展示了使用sysconf函数获取上面这些限制的值:

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

static void pr_sysconf(const char* mesg, int name)
{
    long val;
    printf("%s", mesg);
    if ((val = sysconf(name)) < 0)
    {
        // 返回-1并不代表sysconf调用出错
        if (errno != 0)
        {
            if (errno == EINVAL)
                // errno设置为EINVAL表示不支持字段
                printf(" (not supported)\n");
            else
                perror("sysconf");
        }
        else
            // errno没有设置,表示不确定值
            printf(" (no limit)\n");
    }
    else
        printf(" %ld\n", val);
}
int main()
{
    // 线程退出时操作系统试图销毁线程特定数据的最大次数
    pr_sysconf("PTHREAD_DESTRUCTOR_INTERATIONS", _SC_THREAD_DESTRUCTOR_ITERATIONS);
    // 进程可以创建的键的最大数目
    pr_sysconf("PTHREAD_KEYS_MAX", _SC_THREAD_KEYS_MAX);

    // 一个线程可用栈的最小字节数
    pr_sysconf("PTHREAD_STACK_MIN", _SC_THREAD_STACK_MIN);

    // 进程可以从创建的最大线程数
    pr_sysconf("PTHREAD_THREADS_MAX", _SC_THREAD_THREADS_MAX);
}
// PTHREAD_DESTRUCTOR_INTERATIONS 4
// PTHREAD_KEYS_MAX 1024
// PTHREAD_STACK_MIN 16384
// PTHREAD_THREADS_MAX (no limit)

sysconf函数返回值小于0时,还需要通过errno进一步判断是不支持、无限制以及调用失败这三种情况。

线程创建属性

通过向pthread接口传入线程或同步对象的属性可以来调节线程或同步对象的行为。管理这些属性的函数一般遵循相同的模式。

关于属性有以下几点说明:

  1. 每个对象与他自己类型的属性对象进行关联(线程与线程属性关联、互斥量与互斥量属性关联)。
  2. 一个属性对象可以表示多个属性。属性对象对应用程序是不透明的。
  3. 具有一个初始化函数将属性设置为默认值。
  4. 具有一个销毁属性对象的函数。
  5. 具有一个从属性对象中获取属性值的函数。
  6. 具有一个设置属性值到属性对象的函数。属性值作为参数进行传递。

线程属性的类型:pthread_attr_t,可以传入pthread_create函数改变线程的行为。

线程属性的初始化函数:pthread_attr_init,属性对象的内容空间是动态分配的,值为操作系统实现支持线程属性的默认值。

线程属性的销毁函数:pthread_attr_destroy,释放动态分配的属性对象内存空间。

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
// 返回值:成功返回0, 失败返回错误编号

下面是POSIX.1支持的4个线程属性,这4个属性均包含在pthread_attr_t结构中并在线程创建时进行设置。

属性名称描述
detachstate线程的分离状态属性
guardsize线程栈末尾的警戒缓冲区大小(字节数)
stackaddr线程栈的最低地址
stacksize线程栈的最小长度(字节数)

含有两个线程属性分别是可取消状态cancelstatue)和可取消类型canceltype),用于调节线程被取消时的行为。

线程分离状态

如果对线程的终止状态不感兴趣,有以下两种方法可以使线程处于分离状态,操作系统会在线程(处于分离状态)退出时收回它所占用的资源。

  1. 如果线程已存在,可以调用pthread_detach函数使线程处于分离状态
  2. 如果线程还未创建,可以修改pthread_attr_t结构中的detachstate线程属性,使线程一开始就处于分离状态

可以使用下面两个函数修改或获取pthread_attr_t结构中的detachstate线程属性。

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
// 返回值:成功返回0,失败返回错误编号

pthread_attr_getdetachstate函数用于获取当前的detachstate线程属性。detachstate线程属性的取值有两种,分别是PTHREAD_CREATE_DETACHEDPTHREAD_CREATE_JOINABLE

下面的示例给出了以分离状态创建线程:

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

/* Detach state.  */
// enum
// {
//   PTHREAD_CREATE_JOINABLE,
// #define PTHREAD_CREATE_JOINABLE      PTHREAD_CREATE_JOINABLE
//   PTHREAD_CREATE_DETACHED
// #define PTHREAD_CREATE_DETACHED      PTHREAD_CREATE_DETACHED
// };
// 该函数会以分离状态创建线程
static int makepthread(void* (*fn)(void*), void* arg)
{
    int err;
    pthread_t tid;
    pthread_attr_t attr;
    err = pthread_attr_init(&attr);
    if (err != 0)
        return err;

    err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (err == 0)
        err = pthread_create(&tid, &attr, fn, arg);

    int detachstate;
    pthread_attr_getdetachstate(&attr, &detachstate);
    printf("%d\n", detachstate);
    pthread_attr_destroy(&attr);

    return err;
}

static void* thr_fn(void* arg)
{
    printf("thr_fn\n");
    return NULL;
}

int main()
{
    makepthread(thr_fn, NULL);
    sleep(3);
}
// $ ./makepthread_detachstate
// 1
// thr_fn

线程栈属性

如果线程栈的虚拟地址空间不够用,那么可以通过使用线程的栈属性,为线程分配可替代的栈空间

在编译阶段可以使用_POSIX_THREAD_ATTR_STACKADDR_POSIX_THREAD_ATTR_STACKSIZE符号来检测系统是否支持线程栈属性。

可以使用函数pthread_attr_getstackpthread_attr_setstack函数对线程栈属性进行管理(设置或获取线程栈地址与大小)。

#include <pthread.h>

int pthread_attr_setstack(pthread_attr_t *attr,
                         void stackaddr[.stacksize],
                         size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *restrict attr,
                         void **restrict stackaddr,
                         size_t *restrict stacksize);
// 返回值: 成功返回0 失败返回错误编号

stackaddr参数是分配的缓冲区的最低地址,也是线程栈的最低内存地址,但不一定是线程栈的开始位置,因为线程栈可能是从高地址向低地址方向增长的。

stackaddr参数指定的缓冲区地址与系统寻址边界对齐。malloc函数返回的缓冲区默认是对其的。

如果仅修改默认线程栈的大小,而不处理线程栈的分配问题,可以使用函数pthread_attr_getstacksizepthread_attr_setstacksize

#include <pthread.h>

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,
                             size_t *restrict stacksize);
// 返回值: 成功返回0,失败返回错误编号

设置线程栈大小时,stacksize不能小于系统限制值PTHREAD_STACK_MIN

下面的例子展示了设置以及获取线程栈属性

#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void* thr_fn(void* arg)
{
    pthread_attr_t attr;
    // 在线程内部获取线程属性
    pthread_getattr_np(pthread_self(), &attr);

    void* stackbase = NULL;
    size_t stacksize;
    size_t guardsize;
    pthread_attr_getstack(&attr, &stackbase, &stacksize);
    pthread_attr_getguardsize(&attr, &guardsize);
    printf("stackbase=%p stacksize=%zu guardsize=%zu\n", stackbase, stacksize / 1024, guardsize / 1024);

    pthread_attr_destroy(&attr);

    return NULL;
}
int main()
{
    pthread_t pid;
    int err = 0;
    // 获取默认栈属性及大小
    err = pthread_create(&pid, NULL, thr_fn, NULL);
    if (err != 0)
        printf("create thread failed\n");
    pthread_join(pid, NULL);

#define STACKSIZE (20 * 1024 * 1024)  // 20Mb
    // 仅设置线程栈的大小
    pthread_attr_t attr;
    if (pthread_attr_init(&attr) == 0)
    {
        pthread_attr_setstacksize(&attr, STACKSIZE);

        err = pthread_create(&pid, &attr, thr_fn, NULL);
        if (err != 0)
            printf("create thread failed\n");
        pthread_join(pid, NULL);

        pthread_attr_destroy(&attr);
    }
    //

    // 设置线程栈的位置及大小
    if (pthread_attr_init(&attr) == 0)
    {
        char* stackbuf = (char*)malloc(sizeof(char) * STACKSIZE);
        pthread_attr_setstack(&attr, stackbuf, STACKSIZE);

        err = pthread_create(&pid, &attr, thr_fn, NULL);
        if (err != 0)
            printf("create thread failed\n");
        pthread_join(pid, NULL);

        pthread_attr_destroy(&attr);
    }

    return 0;
}
// $ ./set_thread_stack_attr
// stackbase=0x7f7489400000 stacksize=8192 guardsize=4
// stackbase=0x7f7482c00000 stacksize=20480 guardsize=4
// stackbase=0x7f74817fe010 stacksize=20480 guardsize=0

线程栈警戒缓冲区大小

线程栈警戒缓冲区大小guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。默认值为系统页大小PAGE_SIZE

如果修改了线程栈地址,即stackaddr属性,则系统会认为应用程序来管理栈,进而使栈警戒缓冲机制无效,即将guardsize大小设置为0(如上面例子所示)。

下面两个函数用于获取与设置线程栈警戒缓冲区大小,即guardsize值:

#include <pthread.h>

int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,
                             size_t *restrict guardsize);
// 返回值:成功返回0,失败返回错误编号

guardsize的参数一般取值为系统页大小的整数倍,Linux下系统页大小为4096。

下面的例子中设置线程栈的警戒缓冲区大小为4096,即一个系统页大小。

$ cat set_thread_guardsize.c
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void* thr_fn(void* arg)
{
    pthread_attr_t* pAttr = (pthread_attr_t*)arg;
    pthread_attr_t attr;

    void* stackbase = NULL;
    size_t stacksize;
    pthread_attr_getstack(pAttr, &stackbase, &stacksize);

    size_t guardsize;
    pthread_attr_getguardsize(pAttr, &guardsize);

    printf("stackbase=%p stacksize=%zu guardsize=%zu\n", stackbase, stacksize / 1024, guardsize / 1024);

    return NULL;
}
int main()
{
    pthread_t pid;
    int err = 0;

#define STACKSIZE (20 * 1024 * 1024)  // 20Mb
    // 仅设置线程栈的大小
    pthread_attr_t attr;

    // 设置线程栈的位置及大小
    if (pthread_attr_init(&attr) == 0)
    {
        char* stackbuf = (char*)malloc(sizeof(char) * STACKSIZE);
        pthread_attr_setstack(&attr, stackbuf, STACKSIZE);
        pthread_attr_setguardsize(&attr, 4096);

        err = pthread_create(&pid, &attr, thr_fn, &attr);
        if (err != 0)
        {
            printf("create thread failed\n");
            _exit(-1);
        }
        pthread_join(pid, NULL);

        free(stackbuf);
        pthread_attr_destroy(&attr);
    }

    return 0;
}
// $ ./set_thread_guardsize
// stackbase=0x7f736ebff010 stacksize=20480 guardsize=4

线程取消属性

下面的线程属性决定线程取消(即调用pthread_cancel()函数)时的行为。

可取消状态

线程的可取消状态属性取值可以是PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLE,默认为PTHREAD_CANCEL_ENABLE

线程可以通过pthread_setcancelstate来修改其可取消状态。

线程可以通过调用pthread_testcancel函数来设置自定义取消点。执行pthread_testcancel函数时,如果可取消状态属性为PTHREAD_CANCEL_DISABLE,那么不会有任何效果。

线程取消原理:

  1. 当前线程可以通过调用pthread_cancel函数来取消其他线程。
  2. 默认情况下,线程的可取消状态属性设置为PTHREAD_CANCEL_ENABLE,此时线程在收到取消请求之后,还会继续运行,直到线程到达某个取消点
  3. 取消点是线程检查它是否被取消的一个位置,如果有取消请求,则按照请求处理。
  4. 当线程可取消状态属性设置为PTHREAD_CANCEL_DISABLE时,如果其他线程调用pthread_cancel来取消当前线程,当前线程并不会立即被杀死。而是将取消请求挂起,直到当前线程的可取消状态属性值变为PTHREAD_CANCEL_ENABLE之后,才会在下一个取消点对所有取消请求进行处理
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
// 返回值:成功返回0,失败返回错误编号
void pthread_testcancel(void);

可取消类型

线程的可取消类型属性决定线程收到取消请求之后是立即取消还是到达取消点才能被取消

线程的可取消类型属性可取值为PTHREAD_CANCEL_DEFERRED(推迟取消)与PTHREAD_CANCEL_ASYNCHRONOUS(异步取消)。默认取值为PTHREAD_CANCEL_DEFERRED

当取值为PTHREAD_CANCEL_ASYNCHRONOUS异步取消时,线程可以在任意位置被取消,而不是等到取消点。

可以通过pthread_setcanceltype函数来设置线程的可取消类型属性。

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
// 返回值:成功返回0,失败返回错误编号

下面的例子展示了在不同的可取消类型与可取消状态属性时线程的行为:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

static void* thr_fn1(void* arg)
{
    // 线程1,可取消类型与可取消状态均为默认值,此时可取消类型为延迟取消、可取消状态为启用状态
    // 即当线程收到取消请求时并不会立即取消,而是等到达取消点之后才会取消
    int oldstate;
    int oldtype;
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);

    // 判断线程时在执行循环时取消还是到达取消点才取消
    printf("thread1: for running\n");
    struct timespec starttime;
    struct timespec endtime;

    printf("thread1: the timer started\n");
    clock_gettime(CLOCK_MONOTONIC, &starttime);
    printf("thread1: starttime: %ld\n", starttime.tv_sec);
    for (int i = 0; i < 100000; i++)
    {
        for (int j = 0; j < 100000; j++)
        {
        }
        // printf函数中调用了系统接口write函数,因此也是取消点
        printf("thread1: for %d\n", i);
    }
    // clock_gettime 函数不是取消点
    clock_gettime(CLOCK_MONOTONIC, &endtime);
    printf("thread1: endtime: %ld\n", endtime.tv_sec);

    printf("thread1: after %ld s\n", endtime.tv_sec - starttime.tv_sec);

    printf("thread1: reach at cancel point\n");
    // sleep函数为取消点
    sleep(3);
    return NULL;
}
static void* thr_fn2(void* arg)
{
    // 线程2,
    // 可取消状态为默认值,即PTHREAD_CANCEL_EANBLE、启用状态。
    // 可取消类型为异步取消,即PTHREAD_CANCEL_ASYNCHRONOUS、当收到取消请求时立即取消,而非到达取消点
    int oldstate;
    int oldtype;
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype);

    // 判断线程时在执行循环时取消还是到达取消点才取消
    printf("thread2: for running\n");
    struct timespec starttime;
    struct timespec endtime;

    printf("thread2: the timer started\n");
    clock_gettime(CLOCK_MONOTONIC, &starttime);
    printf("thread2: starttime: %ld\n", starttime.tv_sec);
    for (int i = 0; i < 100000; i++)
    {
        for (int j = 0; j < 100000; j++)
        {
        }
        printf("thread2: for %d\n", i);
    }
    // clock_gettime 函数不是取消点
    clock_gettime(CLOCK_MONOTONIC, &endtime);
    printf("thread2: endtime: %ld\n", endtime.tv_sec);

    printf("thread2: after %ld s\n", endtime.tv_sec - starttime.tv_sec);

    printf("thread2: reach at cancel point\n");
    // sleep函数为取消点
    sleep(3);
    return NULL;
}
static void* thr_fn3(void* arg)
{
    // 线程3
    // 可取消状态为关闭状态,即PTHREAD_CANCEL_DISABLE
    // 可取消类型为默认,即延迟取消状态,PTHREAD_CANCEL_DEFERRED
    int oldstate;
    int oldtype;

    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
    // pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);

    // 判断线程时在执行循环时取消还是到达取消点才取消
    printf("thread3: for running\n");
    struct timespec starttime;
    struct timespec endtime;

    printf("thread3: the timer started\n");
    clock_gettime(CLOCK_MONOTONIC, &starttime);
    printf("thread3: starttime: %ld\n", starttime.tv_sec);
    for (int i = 0; i < 100000; i++)
    {
        for (int j = 0; j < 100000; j++)
        {
        }
        printf("thread3: for %d\n", i);
    }
    // clock_gettime 函数不是取消点
    clock_gettime(CLOCK_MONOTONIC, &endtime);
    printf("thread3: endtime: %ld\n", endtime.tv_sec);

    // 重新启用可取消状态,此时到达下一个取消点,线程会被取消
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);
    printf("thread3: after %ld s\n", endtime.tv_sec - starttime.tv_sec);

    printf("thread3: reach at cancel point\n");
    // sleep函数为取消点
    sleep(3);
    return NULL;
}

int main()
{
    pthread_t td1, td2, td3;
    void* tdRet;
    int err = 0;
    err = pthread_create(&td1, NULL, thr_fn1, NULL);
    if (err != 0)
        _exit(-1);

    // 休眠1s等待线程设置可取消类型与可取消状态,虽然设置的值与默认值一致
    sleep(1);
    // 在线程执行for循环之间发送取消请求
    printf("main thread: cancel td1\n");
    pthread_cancel(td1);

    // 等待线程取消结果
    printf("main thread: wait td1\n");

    pthread_join(td1, &tdRet);
    if (tdRet == PTHREAD_CANCELED)
        printf("main thread: join thread td1\n");
    else
        printf("main thread: join thread td1 failed\n");

    sleep(15);
    err = pthread_create(&td2, NULL, thr_fn2, NULL);
    if (err != 0)
        _exit(-1);

    // 休眠1s等待线程设置可取消类型与可取消状态,虽然设置的值与默认值一致
    sleep(1);
    // 在线程执行for循环之间发送取消请求
    printf("main thread: cancel td2\n");
    pthread_cancel(td2);

    // 等待线程取消结果
    printf("main thread: wait td2\n");

    pthread_join(td2, &tdRet);
    if (tdRet == PTHREAD_CANCELED)
        printf("main thread: join thread td2\n");
    else
        printf("main thread: join thread td2 failed\n");

    sleep(15);
    err = pthread_create(&td3, NULL, thr_fn3, NULL);
    if (err != 0)
        _exit(-1);

    // 休眠1s等待线程设置可取消类型与可取消状态,虽然设置的值与默认值一致
    sleep(1);
    // 在线程执行for循环之间发送取消请求
    printf("main thread: cancel td3\n");
    pthread_cancel(td3);

    // 等待线程取消结果
    printf("main thread: wait td3\n");

    pthread_join(td3, &tdRet);
    if (tdRet == PTHREAD_CANCELED)
        printf("main thread: join thread td3\n");
    else
        printf("main thread: join thread td3 failed\n");
}
// thread1: for running
// thread1: the timer started
// thread1: starttime: 20983
// thread1: for 0
// thread1: for 1
// ...
// thread1: for 7560
// thread1: for 7561
// thread1: for 7562
// thread1: for 7563
// main thread: join thread td1
// thread2: for running
// thread2: the timer started
// thread2: starttime: 20999
// thread2: for 0
// thread2: for 1
// thread2: for 2
// ...
// thread2: for 6943
// thread2: for 6944
// thread2: for 6945
// main thread: cancel td2 在for循环过程中,线程2收到取消请求立即取消,而非等待到达取消点
// main thread: wait td2
// main thread: join thread td2
// thread3: for running
// thread3: the timer started
// thread3: starttime: 21015
// thread3: for 0
// thread3: for 1
// thread3: for 2
// ...
// thread3: for 6954
// thread3: for 6955
// thread3: for 6956
// main thread: cancel td3 循环过程中,线程3收到取消请求并没有处理,直到设置可取消状态为PTHREAD_CANCEL_ENABLE
// main thread: wait td3
// thread3: for 6957
// thread3: for 6958
// thread3: for 6959
// ...
// thread3: for 99997
// thread3: for 99998
// thread3: for 99999
// thread3: endtime: 21029 // 在这里重新设置可取消状态为PTHREAD_CANCEL_ENABLE,之后的printf是取消点,处理取消请求
// thread3: after 14 s
// main thread: join thread td3