`
kdyzi19u
  • 浏览: 16254 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

C1X系列: 多线程(N1494)

 
阅读更多

C1X系列: 多线程(N1494)
2010年08月03日
  除非特别声明,本站采用Creative Commons License许可,如承蒙转载,请注明作者以及出处,请勿用于商业用途。
如果博客中转载了您的文章并侵犯了您的版权,请立即通知本人,马上删除。
追求生活,享受技术,欢迎交流和建议。
E-mail:lyt2008bj#163.com
QQ联系
  
  
  
  
  
  
  
  
  
  C1X是C语言的下一个标准,用于取代现有的C99标准。C1X是一个非正式名字,该标准仍在制订中,最新的工作草案是N1494,发布于2010年6月。与C99相比, C1X在语言和库上有显著的变化,本文重点分析N1494草案中的多线程部分。 不瞒你说,C99标准里面的内存模型仍然是单线程的,即所有代码都运行在一个线程(进程)内。也许,你简直不敢相信这个,因为说不定你每天都与多线程打交道, 使用_beginthread, CreateThread或pthread_create等不同平台的函数去创建线程。当你担心每个变量被编译器优化时,不得不加上volidate修饰符时,都印证了C99为单线程的内存模型。
  32位保护模式的出现,崔生了多任务操作系统的诞生,随之而来的就是多线程环境。随着多核时代的到来,多线程环境成为程序开发不能退避的问题。无锁编程,并行编译等业已成熟的技术在C社区里,常常给很多初学都遥不可及的感觉。在迫切需要多线程的时代,不同厂商和平台都纷纷开发了自己的多线程库,如POSIX的pthreads和window的winThread。软件工程师在特定的平台下,使用各自的线程库来开发多线程或并发程序,并非很难,但可移植却成了他们面临的问题。如pthreads在默认情况下,互斥锁是非递归的,要使用递归互斥锁,程序库必须支持一些扩展feature;而window下的锁默认情况下是递归的。lock-wait-wakup方式也不尽然相同,这对开发跨平台的多线程程序无遇于雪上加霜。
  放眼C语言的后来者,如Java,很早就支持了多线程,并且它的内存模型在不断地修改,以适应发挥多线程的优势。Erlang作为一种天生的并发编程语言,踏上编程语言的行列,成为并发编程的新星。同时,很多脚本语都内置地支持线程类或结构。C/C++作为后来加入到多线程的行列,尽管已经太晚了,但对于C语言社区来说,这无疑是最令人兴奋的消息了。     如果你在Pthreads的开发经验,应该C1X threading不会感到陌生,因为它们在API,参数和语义方面都惊人地一致,以致于你学会了pthreads,基本就是大半个C1X threading了。相反,对于winthread的开发人员来说,需要换一个角度来看等多线程,特别是cnd_wait和cnd_signal之间的time race关系。好吧,让我们一览目前最新的C1X threading编程吧。      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线程的结束状态码。
  void thrd_sleep(const xtime *xt)
  thrd_sleep函数让当前线程中途休眠,直到由xt指定的时间过去后才醒过来。
  void thrd_yield(void)
  thrd_yield函数让出CPU给其它线程或进程。     C1X threading中提供了丰富的互斥对象,用户只需mtx_init初始化时,指定该互斥对象的类型即可,如递归的,支持timeout和,或者支持锁检测的。
  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。      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)
  销毁条件变量。     试想一下,如何在一个多线程同时执行的环境下来初始化一个变量,即著名的延迟初始化单例模式。你可能会使用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在多线程环境只被调用一次。      在多线程开发中,并不是所有的同步都需要锁的,有时巧妙的数据分解也可减少锁的碰撞。每个线程都拥有自己私有的数据,可以减少线程间共享数据之间的同步开销。 
  如果要将一些遗留代码进行线程化时,很多函数都使用了全局变量,而在多线程环下,最好的方法可能是将这些全局量变量变为线程么有的全局变量即可。 
  TSD和TLS就是专门用来处理线程私有数据的。 它的生存周期是整程序的生存周期,但它在每个线程都有一份变量,每个线程只能修改,读取属于自己的那份。如果通过指针方式来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有如下的描述:
  1. 在声明式中,_Thread_local只能单独使用,也可能跟static或extern一起使用。 
  2. 在某一区块(blcok scope)中声明某一对象,如果声明修饰符有_Thread_local,那么必须同时有static或extern。 
  3. 如果_Thread_local出现在一对象的某个声明式中,那么此对象的其余各处声明式都应该有_Thread_local修饰符。 
  4. 如果某一对象的声明式中出现_Thread_local修饰符,那么它有线程储存期(thread storage duration)。该对象的生命周期为线程的整个执行周期,它在线程出生时创建,并在线程启动时初始化。每个线程均有一份该对象,使用声明时的名字即可引用正在执行当前表达式的线程所关联的那个对象。
  TLS方式与传统的全局变量或static变量的使用方式完全一致,不同的,TLS变量在不同的线程上均有各自的一份。线程访问TLS时不会产生data race,因为不需要任何加锁机制。TLS方式需要编译器的支持,对于任何_Thread_local变量,编译器要将之编译并生成放到各个线程的private memory区域,并且访问这些变量时,都要获得当前线程的信息,从而访问正确的物理对象,当然这一切都是在链接过程早已安排好的。     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以内存共享模型作为多线程编程模型,提供的同步机制基于锁来实现。将来是否会提供系统级别的,基于消息传递来实现无锁同步。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics