C1X 系列 : 多线程 (N1494)

简介:

1. 关于 C1X 标准

C1X 是 C 语言的下一个标准,用于取代现有的 C99 标准。 C1X 是一个非正式名字,该标准仍在制订中,最新的工作草案是 N1494 ,发布于 2010 年 6 月。与 C99 相比, C1X 在语言和库上有显著的变化,本文重点分析 N1494 草案中的多线程部分。

2. 呼之欲出的多线程

不瞒你说, C99 标准里面的内存模型仍然是单线程的,即所有代码都运行在一个线程(进程)内。也许,你简直不敢相信这个是真的,因为说不定你每天都与多线程打交道, 使用 _beginthread , CreateThread 或 pthread_create 等不同平台的函数去创建线程。当你担心每个变量被编译器优化时,不得不加上 volatile 修饰符时,或者加上内存栅栏 (memory barrier) ,都印证了 C99 为单线程的内存模型。

32 位保护模式的出现,催生了多任务操作系统的诞生,随之而来的就是多线程环境。随着多核时代的到来,多线程环境成为程序开发不可逃避的问题。无锁编程,并行 编程等已经成熟的技术在 C 社区里,常常给初学者遥不可及的感觉。在迫切需要多线程的时代,不同厂商和平台都纷纷开发了各自的多线程库,如 POSIX 的 pthreads 和 window 的 winThread 。软件工程师在特定的平台下,使用各自的线程库来开发多线程或并发程序,并非很难,但可移植却成了他们面临的问题。如 pthreads 在默认情况下,互斥锁是非递归的,要使用递归互斥锁,程序库必须支持一些扩展 feature ;而 window 下的锁默认情况下是递归的。 lock-wait-wakup 方式也不尽然相同,这对开发跨平台的多线程程序来说无异于雪上加霜。

放眼 C 语言的后来者,如 Java ,很早就支持了多线程,并且它的内存模型在不断地修改,以适应发挥多线程的优势。 Erlang 作为一种天生的并发编程语言,踏上编程语言的行列,成为并发编程的新星。同时,很多脚本语都内置地支持线程类或结构。 C/C++ 作为后来加入到多线程的行列,尽管已经太晚了,但对于 C 语言社区来说,这无疑是最令人兴奋的消息了。

3. C1X 多线程接口与语义

如果你有 pthreads 的开发经验,应该对 C1X threading 不会感到陌生,因为它们在 API ,参数和语义方面都惊人地一致,以致于你学会了 pthreads ,基本就学会了大半个 C1X threading 了。相反,对于 winthread 的开发人员来说,需要换一个角度来看等多线程,特别是 cnd_wait 和 cnd_signal 之间的 time race 关系。好吧,让我们一览目前最新的 C1X threading 编程吧。

3.1 线程管理

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

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

与 pthread_create 函数相比, thrd_create 函数没有线程属性这一参数,并具线程函数的返回值是 int ,而非 pthreads 的 void * 。这一特点与进程的返回值一致,都是使用整数表示一个任务的结束状态。

thrd_t thrd_current(void);

thrd_current 函数返回调用线程的标识符。类似于 pthreads 下的 pthread_self() 函数。

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 给其它线程或进程。

3.2 互斥对象和函数

       C1X threading 中提供了丰富的互斥对象,用户只需 mtx_init 初始化时,指定该互斥对象的类型即可,如递归的,支持 timeout 和,或者支持锁检测。

int mtx_int(mtx_t  *mtx, int type);

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

  1.      mtx_plain  — 简单的,非递归互斥对象
  2.      mtx_timed — 非递归的,支持超时的互斥对象
  3.      mtx_try       — 非递归的,支持锁检测的互斥对象
  4.      mtx_plain | mtx_recursive — 简单的,递归互斥对象
  5.      mtx_timed | mtx_recursive — 支持超时的递归互斥对象
  6.      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 进行解锁。

3.3 条件变量

       C1X 中的条件变量与 pthreads 中的条件变量是一样的, C1X 通过 mtx 对象和条件变量来实现 wait-notify 机制,这与 Java 语言里 Object 对象中的 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 互斥对象进行解锁操作,然后阻塞,直到条件变 量 cond 被 cnd_signal 或 cnd_broadcast 调用唤醒,当前线程变为非阻塞时,它将在返回之前锁住 mtx 互斥对 象。 cnd_timedwait 函数与 cnd_wait 类似,例外之处是当前线程在 xt 时间点上还未能被唤醒时,它将返回,此时返回值 为 thrd_timeout 。 cnd_wait 和 cnd_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)

cnd_destroy函数用于销毁条件变量。

3.4 初始化函数

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

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

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

3.5 线程专有数据 (thread-specific data, TSD) 和线程局部数据 (thread-local storage, TLS)

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

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

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

C1X 同时提供了 TSD 和 TLS 特性,而 pthreads 只提供 TSD ,但在 linux 下的 gcc 编译器提供了 TLS 作为扩展特性。 TSD 可认为线程私有内存下的 void * 组数,每个数据项的 key 对应于数组的下标,用于索引功能。当一个新线程创建时,线程的 TSD 区域将所有 key 关联的值设置为 NULL 。 TSD 是通过函数的方式来操作的。 C1X 中 TSD 提供的标准函数如下:

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 函数创建一个 key , dtor 为该 key 将要关联 value 的析构函数。当线程退出时,会调用 dtor 函数来释放该 key 关联的 value 所占用的资源,当然,如果退出时 value 值为 NULL , dtor 将不被调用。 tss_delete 函数删除一个 key , tss_get/tss_set 分别获得或设置该 key 所关联的 value 。

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

在声明式中,_Thread_local 只能单独使用,或者跟 static 或 extern 一起使用。

在某一区块( block scope) 中声明某一对象,如果声明存储修饰符有 _Thread_local ,那么必须同时有 static 或 extern 。

如果 _Thread_local 出现在一对象的某个声明式中,那么此对象的其余各处声明式都应该有 _Thread_local 存储修饰符。
如果某一对象的声明式中出现 _Thread_local 存储修饰符,那么它有线程储存期( thread storage duration )。该对象的生命周期为线程的整个执行周期,它在线程出生时创建,并在线程启动时初始化。每个线程均有一份该对象,使用声明时的名字即可引用正在执行当前 表达式的线程所关联的那个对象。

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

4. C1X threading 的未来

C1X threading 的整体设计与 pthreads 的惊人地一致,我甚至怀疑它们是出自一人(团队)之手。我最初接触多线程编译中的等待 – 通知原语是从 Java 中 Object 对象里的 wait 和 notify 函数中获得感性认识,然后在工作中将 pthreads 中的 pthread_cond_wait 和 pthread_cond_signal 函数应用于实际工作中,并解决了很多实际问题,如编写线程安全的数据结构。现在最新的 C1Xthreading 标准,线程等待 – 通知的方式与上述两者如同一辙。

与 pthreads 相比, C1X threading 没有了信号量操作,读写锁和自旋锁。显然开发人员可以借用 C1X threading 中提供的同步机制来实现信号量和读写锁,但要实现自旋锁是比较难,这需要深入了解所在平台和操作系统提供的原语,在此基础上再实现自旋锁。

对于 window 开发者来说, C1X threading 是一个新的标准,里面提供的同步原语与 winthread 相比,显得有点冷清。不过 window 下的开发人员通常都使用 C++ 或 MFC 提供的多线程库来开发,只是对于那些完全使用 C语言来开始跨平台多线程程序的同行来说,只能完全遵循 C1X threading 标准了。

C1X threading 以内存共享模型作为多线程编程模型,提供的同步机制基于锁来实现。将来是否会提供系统级别的,基于消息传递来实现无锁同步。

5. 总结

       多线程和多核开发时代已悄悄降临在我们的身边,无论你现在的开发工作是否与并行开发相关, C1X threading 都应该成为你手中的一把利器。 C1X threading 了却你心中很多疑虑,可移植, TSD/TLS 之抉择等。


文章转自 并发编程网-ifeve.com

相关文章
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
30 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
38 1
|
2月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
26 1
|
3月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
49 6