threads.h是C11标准新增的多线程支持库,在此之前C语言实现多线程,除了使用系统API外用的最多的 就是pthread.h了,threads.h在语法上和pthread.h非常相似。

C语言包括对线程、原子操作、互斥、条件变量和线程专用存储的内置支持。

若编译器定义宏常量 __STDC_NO_THREADS__(C11) ,则不提供头文件<threads.h> 和所有列于此的名称。

退出和错误码以及互斥锁类型定义:

 /* Exit and error codes.  */
 enum
  {
    thrd_success  = 0,
    thrd_busy     = 1,
    thrd_error    = 2,
    thrd_nomem    = 3,
    thrd_timedout = 4
  };

  /* Mutex types.  */
  enum
  {
    mtx_plain     = 0,
    mtx_recursive = 1,
    mtx_timed     = 2
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 3.1 线程函数

typedef int (*thrd_start_t) (void*);
// 创建一个新的执行 __func 函数的线程,__func 的参数通过 __arg 传递,
// 如果成功,thr 将设置为新线程的标识符。
// 创建成功返回 thrd_success,否则返回其他。
int thrd_create (thrd_t *__thr, thrd_start_t __func, void *__arg);
/* 返回当前的线程标识符  */
thrd_t thrd_current (void);
/* 清除当前线程,清除任何线程本地数据并且释放资源。
   向__res 中返回指定的值。  */
// thrd_exit 会将线程置于已分离状态,线程资源会在其退出后被自动释放,而不需要主线程显式等待和回收资源。
void thrd_exit (int __res) __attribute__ ((__noreturn__));
/* 从当前环境中分离通过 __thr 标志的的线程。(不允许父线程清理。)*/
// 由操作系统负责回收线程的资源。
int thrd_detach (thrd_t __thr);
/* 阻塞当前线程,直到线程 __thr 执行完成,
   如果__res 是非空的,将会存储 __thr 在退出时的返回值。
   如果某一线程内没有调用 thrd_detach 函数将自己设置为detach状态,
   那么当它结束时必须由另外一个线程调用thrd_join函数将它留下的僵死状态变为结束,
   并回收它所占用的系统资源。*/
int thrd_join (thrd_t __thr, int *__res);
/* 检查 __lhs 和 __rhs 是否指向同一个线程。
   因为不同编译器实现的方式可能不一样,可能 thrd_t 是结构体。  */
int thrd_equal (thrd_t __lhs, thrd_t __rhs);
 /* 至少在持续时间指向的基于timespec的持续时间过去之前阻止当前线程的执行。
   如果收到未忽略的信号,睡眠可能会提前恢复。
   在这种情况下,如果 remaining 不是 NULL,则剩余持续时间将存储到剩余指向的对象中。*/
int thrd_sleep (const struct timespec *__time_point,
                struct timespec *__remaining);
/* 停止当前线程并调用调度器来决定接下来执行哪一个线程。
    当前线程也可能被调度器选中继续执行。*/
void thrd_yield (void);
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

# 3.2 互斥锁函数

/* 新建一个新的互斥对象,并指定类型为 __type. 如果成功,__mutex 指定新的互斥对象  */
// mtx_plain –简单的,非递归互斥对象
// mtx_timed –非递归的,支持超时的互斥对象
// mtx_plain | mtx_recursive –简单的,递归互斥对象
// mtx_timed | mtx_recursive –支持超时的递归互斥对象
int mtx_init (mtx_t *__mutex, int __type);
/* 阻塞当前线程直到 __mutex 指向的互斥对象解锁。*/
int mtx_lock (mtx_t *__mutex);
/* 阻塞当前线程直到互斥锁解锁,
   或者计数时间到.  */
int mtx_timedlock (mtx_t *__restrict __mutex,
    const struct timespec *__restrict __time_point);
    /* 在不阻塞的情况下试图锁定互斥对象.
    如果互斥对象是空闲的,则会控制该互斥对象,否则将会立即返回。 */
int mtx_trylock (mtx_t *__mutex);
/* 解锁互斥对象,可能会唤醒其他等待此互斥对象的线程。  */
int mtx_unlock (mtx_t *__mutex);

/* 摧毁互斥对象  */
void mtx_destroy (mtx_t *__mutex);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 3.3 条件变量函数

/* I初始化一个新的指向 __cond 的条件变量。  */
extern int cnd_init (cnd_t *__cond);

/* 取消阻塞一个当前等待 __cond 指向的条件变量的线程。  */
extern int cnd_signal (cnd_t *__cond);

/* 取消阻塞所有当前等待 __cond 指向的条件变量的线程。  */
extern int cnd_broadcast (cnd_t *__cond);

/* 自动对mtx互斥对象进行解锁操作,然后阻塞,直到条件变量cond被cnd_signal或cnd_broadcast
调用唤醒,当前线程变为非阻塞时,它将在返回之前锁住mtx互斥对象 */
extern int cnd_wait (cnd_t *__cond, mtx_t *__mutex);

/* 与cnd_wait类似,例外之处是当前线程在xt时间点上还未能被唤醒时,
它将返回,此时返回值为thrd_timeout。cnd_wait和cnd_timedwait函数在被调用前,
当前线程必须锁住mtx互斥对象。 */
extern int cnd_timedwait (cnd_t *__restrict __cond,
            mtx_t *__restrict __mutex,
            const struct timespec *__restrict __time_point);
            co
/* 摧毁互斥变量  */
extern void cnd_destroy (cnd_t *__COND);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 3.4 线程特定存储函数

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

在多线程程序中,经常要用全局变量来实现多个函数间的数据共享。由于数据空间是共享的,因此全局变量也为 所有线程共有。但有时应用程序设计中必要提供线程私有的全局变量,这个变量仅在线程中有效,但却可以跨过 多个函数访问,比如 linux 下 errno 的实现。

比如在程序里可能需要每个线程维护一个链表,而会使用相同的函数来操作这个链表,最简单的方法就是使用 同名而不同变量地址的线程相关数据结构。这样的数据结构可以由 Posix 线程库维护,成为线程私有数据 (Thread-specific Data,或称为 TSD)。

如果需要在一个线程内部的各个函数调用(全局)都能访问、但其它线程不能访问的变量 (被称为static memory local to a thread 线程局部静态变量),就需要新的机制来实现。这就是TLS。

C 语言支持两种方式使用 TLS:_Thread_localtss_create(),一个是语言层面支持,一个是库层面支持。

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

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

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

C11提供了TLS方法,可以像一般变量的方式去访问线程私有变量。 做法很简单,在声明和定义线程私变量时指定 _Thread_local 存储修饰符即可。

C 语言为不同的存储类别定义了多个关键字,例如 auto,static,register,extern。

从 C11 标准的规范开始,添加了 _Thread_local 指定符。_Thread_local 存储持续时间始于 线程创建时,并终止于线程终止。启动线程时,将初始化存储在 _Thread_local 对象中的值, 并在线程终止时对其进行清理。通常,线程局部对象是避免共享资源中竞争条件的另一种选择。 也就是说,我们隐式地将线程之间的数据分开,因为 _Thread_local 修饰的对象在每个线程具有单独的实例。

C11 threads.h 中定义了 thread_local 宏,以表示 _Thread_local

C11 有如下的描述:

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

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

线程存储为每个线程指定一个地址空间并且自动维护它,这个空间可以是静态区域的内存也可以是堆上的内存, 显然选择堆作为动态临时空间比较理想,因为每次可以分配不同的大小且用完后可以释放。首先需要为自动管理 的内存空间创建一个类型为tss_t的密匙,然后调用tss_create()方法将密匙关联到一个析构函数,当线程 执行完毕后会自动调用这个析构函数。

密匙的优点是不同线程的存储空间可以共用一个密匙,tss_t对象会分别引用它们。

/* 调用函数 __func 只一次,即使是被多个线程调用也是这样。
   必须使用相同的 __flag 对象进行所有调用。  */

// 可用于在一个多线程同时执行的环境下来初始化一个变量,即著名的延迟初始化单例模式。
// call_once函数使用flag来保确func只被调用一次。第一个线程使用flag去调用call_once时,
// 函数func会被调用,而接下来的使用相同flag来调用的call_once,func均不会再次被调用,
// 以保正func在多线程环境只被调用一次。
void call_once (once_flag *__flag, void (*__func)(void));
// 创建新的特定于线程的存储密钥,并将其存储在 tss_key 指向的对象中。尽管相同的键值可由不同的线程
// 使用,但 tss_set 绑定到键的值是在每个线程的基础上维护的,并且在调用线程的生命周期内一直存在。
// 值 NULL 与所有现有线程中的新创建的密钥相关联,并且在线程创建后,与所有TSS密钥相关联的值都初始化为 NULL 。
// 如果 destructor 不是空指针,则还将关联析构函数,该析构函数在 thrd_exit (但不是 tss_delete 且不在程序通过 exit 终止)释放存储时被调用。
// 在特定于线程的存储析构函数中对 tss_create 的调用导致未定义的行为。
// 只要线程终止时与key关联的值不为NULL,则 destructor 所指的函数将会自动被调用。
// 如果一个线程中有多个线程局部存储变量,那么对各个变量所对应的destructor函数的调用顺序是
// 不确定的,因此,每个变量的destructor函数的设计应该相互独立。
extern int tss_create (tss_t *__tss_id, tss_dtor_t __destructor);

// 只能在线程函数中调用。
// 返回由tss_key标识的当前线程的线程专有存储中保存的值。 不同的线程可能会获得由同一个密钥标识的不同值。
//在线程启动时(请参阅thrd_create),与所有TSS键相关的值为NULL。 使用tss_set可以将不同的值放入线程专有存储中。
extern void *tss_get (tss_t __tss_id);
// 只能在线程函数中调用。
// 将当前线程的tss_id标识的线程专有存储的值设置为val。 不同的线程可以为同一个键设置不同的值。
// 析构函数如果可用,则不会被调用。
extern int tss_set (tss_t __tss_id, void *__val);

// 销毁 tss_id 标识的特定于线程的存储。
//析构函数(如果已由 tss_create 注册)不会被调用(它们只能在线程退出时通过 thrd_exit
//或从线程函数返回而调用),程序员有责任确保每个知道的线程在调用 tss_delete 之前, tss_id 执行了所有必要的清理。
//如果 tss_delete 而另一个线程执行析构函数被调用 tss_id ,它是不确定这是否改变调用的到相关的析构函数的数量。
//如果在调用线程正在执行析构函数的同时调用 tss_delete ,则与 tss_id 相关联的析构函数将不会在此线程上再次执行。
// delete()并不检查当前是否有线程正在使用该线程局部数据变量,也不会调用清理函数destructor,
// 只是将其释放以供下一次调用pthread_key_create()使用。
extern void tss_delete (tss_t __tss_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
28
29
30
31
32
33
34
35

# 参考