C11新增多线程支持库-threads.h参考手册

线程管理

int thrd_create(thrd_t *thr,  thrd_start_t func,  void *arg);

thrd_create创建一个新线程,该线程的工作就是执行func(arg)调用,程序员需要为线程编写一个函数,函数签名为:thrd_start_t ,即int (*)(void*)类型的函数。新创建的线程的标识符存放在thr内。

thrd_t thrd_current(void);

thrd_current函数返回调用线程的标识符。

int thrd_detach(thrd_t thr);

thrd_detach会通知操作系统,当该线程结束时由操作系统负责回收该线程所占用的资源。

int thrd_equal(thrd_t thr0, thrd_t thr1);

thrd_equal用于判断两个线程标识符是否相等(即标识同一线程),thrd_t是标准约定的类型,可能是一个基础类型,也可能会是结构体,开发人员应该使用thrd_equal来判断两者是否相等,不能直接使用==。即便==在某个平台下表现出来是正确的,但它不是标准的做法,也不可跨平台。

void thrd_exit(int res)

thrd_exit函数提早结束当前线程,res为它的退出状态码。这与进程中的exit函数类似。

int thrd_join(thrd_t thr, int *res)

thrd_join将阻塞当前线程,直到线程thr结束时才返回。如果res非空,那么res将保存thr线程的结束状态码。如果某一线程内没有调用thrd_detach函数将自己设置为detach状态,那么当它结束时必须由另外一个线程调用thrd_join函数将它留下的僵死状态变为结束,并回收它所占用的系统资源。

void thrd_sleep(const xtime *xt)

thrd_sleep函数让当前线程中途休眠,直到由xt指定的时间过去后才醒过来。

void thrd_yield(void)

thrd_yield函数让出CPU给其它线程或进程。

互斥对象和函数

threads.h中提供了丰富的互斥对象,用户只需在mtx_init初始化时,指定该互斥对象的类型即可。

int mtx_int(mtx_t  *mtx, int type);

mtx_init函数用于初始化互斥对象,type决定互斥对象的类型,一共有下面6种类型:

  • mtx_plain –简单的,非递归互斥对象
  • mtx_timed –非递归的,支持超时的互斥对象
  • mtx_try –非递归的,支持锁检测的互斥对象
  • mtx_plain | mtx_recursive –简单的,递归互斥对象
  • mtx_timed | mtx_recursive –支持超时的递归互斥对象
  • mtx_try | mtx_recursive –支持锁检测的递归互斥对象
int mtx_lock(mtx_t *mtx)
int mtx_timedlock(mtx_t *mtx, const xtime *xt)
int mtx_trylock(mtx_t *mtx)

mtx_xxxlock函数对mtx互斥对象进行加锁 , 它们会阻塞,直到获取锁,或者xt指定的时间已过去。而trylock版本会进行锁检测,如果该锁已被其它线程占用,那么它马上返回thrd_busy

int mtx_unlock(mtx_t *mtx)

mtx_unlock对互斥对象mtx进行解锁。

条件变量

threads.h通过mtx对象和条件变量来实现wait-notify机制。

int cnd_init(cnd_t *cond)

初始化条件变量,所有条件变量必须初始化后才能使用。

int cnd_wait(cnd_t *cond, mtx_t *mtx)
int cnd_timedwait(cnd_t *cond, mtx_t *mtx, const xtime *xt)

cnd_wait函数自动对mtx互斥对象进行解锁操作,然后阻塞,直到条件变量condcnd_signalcnd_broadcast调用唤醒,当前线程变为非阻塞时,它将在返回之前锁住mtx互斥对象。cnd_timedwait函数与cnd_wait类似,例外之处是当前线程在xt时间点上还未能被唤醒时,它将返回,此时返回值为thrd_timeoutcnd_waitcnd_timedwait函数在被调用前,当前线程必须锁住mtx互斥对象。

int cnd_signal(cnd_t *cond)
int cnd_broadcast(cnd_t *cond)

cnd_broadcast唤醒那些当前已经阻塞在cond条件变量上的所有线程,而cnd_signal只唤醒其中之一。

void cnd_destroy(cnd_t *cond)

销毁条件变量。

初始化函数

试想一下,如何在一个多线程同时执行的环境下来初始化一个变量,即著名的延迟初始化单例模式。你可能会使用DCL技术。但在C11中,你可以直接使用call_once函来实现。

void call_once(once_flag *flag, void (*func)(void))

call_once函数使用flag来保确func只被调用一次。第一个线程使用flag去调用call_once时,函数func会被调用,而接下来的使用相同flag来调用的call_oncefunc均不会再次被调用,以保正func在多线程环境只被调用一次。

线程专有数据(TSD) 和线程局部数据 (TLS)

在多线程开发中,并不是所有的同步都需要加锁的,有时巧妙的数据分解也可减少锁的碰撞。每个线程都拥有自己私有数据,使用它可以减少线程间共享数据之间的同步开销。

如果要将一些遗留代码进行线程化,很多函数都使用了全局变量,而在多线程环下,最好的方法可能是将这些全局量变量换成线程私有的全局变量即可。

TSDTLS就是专门用来处理线程私有数据的。 它的生存周期是整个线程的生存周期,但它在每个线程都有一份拷贝,每个线程只能read-write-update属于自己的那份。如果通过指针方式来read-write-update其它线程的备份,它的行为是未定义的。

TSD可认为线程私有内存下的void *组数,每个数据项的key对应于数组的下标,用于索引功能。当一个新线程创建时,线程的TSD区域将所有key关联的值设置为NULLTSD是通过函数的方式来操作的。C11TSD提供的标准函数如下:

int tss_create(tss_t *key,  tss_dtor_t dtor)
void tss_delete(tss_t key)
void *tss_get(tss_t key)
int tss_set(tss_t key, void *val)

tss_create函数创建一个keydtor为该key将要关联value的析构函数。当线程退出时,会调用dtor函数来释放该key关联的value所占用的资源,当然,如果退出时value值为NULLdtor将不被调用。tss_delete函数删除一个keytss_get/tss_set分别获得或设置该key所关联的value

通过上述TSD来操作线程私有变量的方式,显得相对繁琐; C11提供了TLS方法,可以像一般变量的方式去访问线程私有变量。做法很简单,在声明和定义线程私变量时指定_Thread_local存储修饰符即可,关于_Thread_local,C11 有如下的描述:

  1. 在声明式中,_Thread_local只能单独使用,或者跟staticextern一起使用。
  2. 在某一区快中声明某一对象,如果声明存储修饰符有_Thread_local,那么必须同时有staticextern
  3. 如果_Thread_local出现在一对象的某个声明式中,那么此对象的其余各处声明式都应该有_Thread_local存储修饰符。
  4. 如果某一对象的声明式中出现_Thread_local存储修饰符,那么它有线程储存期。该对象的生命周期为线程的整个执行周期,它在线程出生时创建,并在线程启动时初始化。每个线程均有一份该对象,使用声明时的名字即可引用正在执行当前表达式的线程所关联的那个对象。

TLS方式与传统的全局变量或static变量的使用方式完全一致,不同的是,TLS变量在不同的线程上均有各自的一份。线程访问TLS时不会产生data race,因为不需要任何加锁机制。TLS方式需要编译器的支持,对于任何_Thread_local变量,编译器要将之编译并生成放到各个线程的private memory区域,并且访问这些变量时,都要获得当前线程的信息,从而访问正确的物理对象,当然这一切都是在链接过程早已安排好的。

以下列出本文参考的资料,在此向原作者致敬。

使用C11新增的多线程支持库-threads.h进行多线程编程

2019年12月6日更新

首先要感谢评论区的热心的同学们的提醒,经本人亲自验证:

VS 2019中移除了对threads.h的支持(评论区有同学提到单独安装v140工具集可以实现支持,但是测试发现没有效果),而GCC方面则在最新版中加入了对该头文件的支持。

以下为原文(程序代码部分更新了Linux版Dome)。


导语

threads.h是C11标准新增的多线程支持库,在此之前C语言实现多线程,除了使用系统API外用的最多的就是pthread.h了,threads.h在语法上和pthread.h非常相似。
当然,对于新出的C语言标准,各大编译器厂商并不会马上就支持。就比如说,Linux下主流的C语言编译器————GCC,直到GCC7.2版本都没能支持该库(PS:看到老外网站上说要安装最新版glibc才能获得对该库的支持,然而亲测无卵用)。相反,VS在这方面做的就很不错,VS2017已经可以完美支持该库了,本文也将基于VS2017社区版对该库的使用方法做介绍。

注:本文仅对多线程编程的概念及threads.h库文件的使用方法做简单介绍,并不会详尽介绍该库下的所有函数,如果你需要一个函数功能的参考手册可以参考此篇文章:C11新增多线程支持库-threads.h参考手册

以一个小程序为例子

本程序中使用到的库函数及宏:

  • thrd_t //此宏定义用于存放线程标识符的数据类型
  • thrd_create //此函数用于创建线程
  • thrd_detach //此函数用于通知操作系统,当线程结束时由操作系统负责释放资源
  • thrd_exit //此函数用于结束当前线程

程序功能:

主线程每2秒打印一次“I love ibadboy.net~~~”,共打印10次。子线程每1秒打印一次“He love ibadboy.net!!!”,共打印10次。我们知道,在只有一个主线程的C程序中该功能是无法实现的,因为后一段程序代码必须等待前一段代码执行完毕才可执行。但,在多线程编程中,各个线程可以一起执行(这里涉及到的同步、异步等等的高阶技术就不讨论了)。举个例子:在游戏开发中,如果程序需要实时监控用户键盘的输入,就不能把这段代码放到主线程中,因为这样的话该段代码就会被程序的其他部分阻塞掉而无法做到真正的“实时”,这时就可以利用多线程技术来化解尴尬啦!话不多说,直接上代码!

程序代码Linux版本

#include <stdio.h>
#include <unistd.h>	//包含sleep等函数
#include <stdbool.h>
#include <threads.h>	//包含多线程支持库头文件
#include <stdlib.h>	//包含exit等函数

int thr_fun(void *);

int main(void) {
    thrd_t thr;
    int ret; //保存thrd_create函数的返回值用于判断线程是否创建成功:0为成功,1为失败。
    ret = thrd_create(&thr, thr_fun, NULL); //将thr_fun函数放在一个新的线程中执行
    if (ret != thrd_success) {
        printf("error!!!\n");
        getchar();
        exit(-1);
    }
    ret = thrd_detach(thr); //通知操作系统,该线程结束时由操作系统负责释放资源。
    if (ret != thrd_success) {
        printf("error!!!\n");
        getchar();
        exit(-1);
    }
    for (int i = 0; i < 10; i++) {
        sleep(2);
        printf("I love ibadboy.net~~~\n");
    }
    getchar();
    return 0;
}

int thr_fun(void *argv) {
    int i = 0;
    while (true) {
        i++;
        sleep(1);
        printf("He love ibadboy.net!!!\n");
        if (i == 10) {
            thrd_exit(0);
        }
    }
}

编译命令:

gcc a.c -std=c11 -lpthread

程序代码Windows版本(经测试在VS 2019中已无法编译通过):

#include<stdio.h>
#include<stdbool.h>
#include<thr/threads.h>        //包含多线程支持库头文件
#include<Windows.h>
void thr_fun(void);
int main(void) {
    thrd_t thr;
    int ret;    //保存thrd_create函数的返回值用于判断线程是否创建成功:0为成功,1为失败。
    ret = thrd_create(&thr, thr_fun, NULL);        //将thr_fun函数放在一个新的线程中执行
    if (ret != thrd_success) {
        printf("error!!!\n");
        getchar();
        exit(-1);
    }
    ret = thrd_detach(thr);    //通知操作系统,该线程结束时由操作系统负责释放资源。
    if (ret != thrd_success) {
        printf("error!!!\n");
        getchar();
        exit(-1);
    }
    for (int i = 0; i < 10; i++) {
        Sleep(2000);
        printf("I love ibadboy.net~~~\n");
    }
    getchar();
    return 0;
}
void thr_fun(void) {
    int i = 0;
    while (true) {
        i++;
        Sleep(1000);
        printf("He love ibadboy.net!!!\n");
        if (i == 10) {
            thrd_exit(0);
        }
    }
}

程序输出:

C语言多线程演示小程序的代码输出

程序中用到的库函数介绍:

thrd_create函数用于创建新线程,如果创建成功,该函数会返回thrd_success,否则返回thrd_error

函数原型:

int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);

参数说明:

  • thr:指向放置新线程标识符的内存位置的指针。
  • func:要放在子线程中执行的函数。
  • arg:传递给执行的函数的参数,无参数填NULL。

thrd_detach函数用于通知操作系统,当线程结束后由操作系统负责释放资源,如果成功,则返回thrd_success,否则为thrd_error,如不调用该函数,则线程所使用的资源将在程序全部执行完后才会释放。

函数原型:

int thrd_detach( thrd_t thr );

参数说明:

  • thr:要作用的线程的标识符

thrd_exit函数用于结束当前进程,值得一提的是:使用该函数可以在不影响子进程的情况下结束主进程,而使用exit函数的话结束主进程将会连带结束整个程序。

函数原型:

_Noreturn void thrd_exit( int res );

参数说明:

  • res:要返回的值

结语

看完此篇文章相信你已经对使用threads.h库进行C语言的多线程开发有了初步了解,如果还有什么问题的话欢迎在评论区留言哦~