返回

文章详情

Go风味的C语言并发

Hacker News2026年7月13日 15:56

Go的并发是人们喜欢这门语言的主要原因之一。你编写go f(),通过通道发送值,运行时调度器在仅有的几个操作系统线程上运行数千个goroutine。这种感觉毫不费力。而在C语言中没有这样的机制。这让我想知道:仅使用POSIX线程,你能达到多接近Go的并发模型?显然,原生的操作系统线程不能与轻量级的goroutine相比,但是实际的成本是什么?什么时候会成为问题?有没有办法至少部分避免这个问题?我在给Solod(So)添加并发功能时遇到了这些问题,So是Go的一个严格子集,翻译为普通C,没有运行时,也没有垃圾收集器。最终,我得出结论:你可以用pthreads做很多事情——只要你诚实地面对权衡。这篇文章是关于我选择的基于POSIX线程的并发模型,它提供的好处,以及它的局限性。互斥量/条件变量 • 原子操作 • 线程池 • 通道 • 性能 • 设计 • 总结 互斥量/条件变量 So的并发栈中的一切都是建立在两个基本POSIX原语基础之上的:互斥量和条件变量。sync.Mutex是对pthread_mutex_t的一个薄包装: // 从So的标准库源代码提取。 type Mutex struct { mu pthread_mutex_t } func ( m * Mutex ) Lock () { rc := pthread_mutex_lock ( & m . mu ) if rc != 0 { panic ( "sync: Mutex.Lock failed" ) } } 由于So编译为C,这基本上是一个持有pthread_mutex_t的结构和一个调用pthread_mutex_lock的函数。以下是转换器的输出: // 转换后的C代码。 typedef struct sync_Mutex { pthread_mutex_t mu ; } sync_Mutex ; void sync_Mutex_Lock ( sync_Mutex * m ) { int rc = pthread_mutex_lock ( & m -> mu); if ( rc != 0 ) { so_panic ( "sync: Mutex.Lock failed" ); } } 这就是整个翻译——生成的C是So代码的一个近乎机械的镜像,只是更嘈杂。从这里开始,我主要会展示So版本,但我也会为感兴趣的人提供C代码。这里没有什么令人兴奋的:sync.Mutex是一个pthread互斥量包装,如果发生错误就会引发panic(这很少发生)。伴随的原语是sync.Cond,包装了pthread_cond_t。它是一个与互斥量相关的标准“等待条件成立”的工具: type Cond struct // 包装pthread_cond_t + pthread_mutex_t func ( c * Cond ) Wait () // 包装pthread_cond_wait func ( c * Cond ) Signal () // 包装pthread_cond_signal func ( c * Cond ) Broadcast () // 包装pthread_cond_broadcast 显示翻译后的C代码 typedef struct sync_Cond { pthread_cond_t cond ; sync_Mutex * mu ; } sync_Cond ; void sync_Cond_Wait ( sync_Cond * c ); // 包装pthread_cond_wait void sync_Cond_Signal ( sync_Cond * c ); // 包装pthread_cond_signal void sync_Cond_Broadcast ( sync_Cond * c ); // 包装pthread_cond_broadcast 这两个类型——Mutex和Cond——是基础。其他并发工具——Once、线程池、通道——都是使用一个互斥量和一个或多个条件变量构建的。这对性能有几个影响,如我们稍后所见。 原子操作 并非所有事情都需要锁。So的sync/atomic反映了Go的:Bool、Int32、Int64、Uint32、Uint64及一个通用的Pointer[T],都有Load、Store、Swap和CompareAndSwap方法。好的一点是这些根本不需要pthread。这些直接映射到C编译器的__atomic内置函数——Go编译器发出的相同硬件指令。因此,它们没有任何理由变得更慢,而实际上它们并没有。 原子操作的时间 Go So 胜者 Load 2ns 2ns ~相同 Store 2ns 2ns ~相同 CompareAndSwap 13ns 13ns ~相同 每个数字是单线程上一次操作的成本。sync.Once是有效使用原子的一个很好的例子。它的快路径只需要一个原子加载——在给定的函数运行后,每次未来调用Do都会检查一个标志并返回: type Once struct { mu Mutex done atomic . Bool } // Do在第一次调用时调用f func ( o * Once ) Do ( f func ()) { if o . done . Load () { // 无锁快速路径 return } // 慢路径… } 显示翻译后的C代码 typedef struct sync_Once { sync_Mutex mu ; atomic_Bool done ; } sync_Once ; // Do在第一次调用时调用f void sync_Once_Do ( sync_Once * o , void ( * f )()) { if ( atomic_Bool_Load ( & o -> done )) { // 无锁快速路径 return ; } // 慢路径… } 工作者池 为了实际并发运行代码,你需要线程。 conc.Thread类型包装了pthread_t及其相关函数: type Thread struct // 包装pthread_t func ( th Thread ) Wait () any // 包装pthread_join func ( th Thread ) Detach () // 包装pthread_detach 显示翻译后的C代码 typedef struct conc_Thread { pthread_t t ; } conc_Thread ; void * conc_Thread_Wait ( conc_Thread th ); // 包装pthread_join void conc_Thread_Detach ( conc_Thread th ); // 包装pthread_detach 考虑这个conc.Go函数: // Go启动一个操作系统线程来运行fn(ar...)

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡