首页 时政热点 科技头条 智能AI 安全攻防 数码硬件 开发者生态 归档 标签 关于

C 语言的 Go 风格并发

2026-07-13 1 阅读 约9分钟阅读 ibobev
分享:
字号:
Go 的并发性是人们喜欢该语言的主要原因之一。您编写 go f() ,通过通道发送值,运行时调度程序仅在几个操作系统线程上运行数千个 goroutine。感觉毫不费力。 C 中不存在这样的机制。这让我想知道:仅使用 POSIX 线程可以接近 Go 的并发模型吗?显然,原生操作系统线程无法与轻量级 goroutine 的效率相匹配,但实际成本是多少,什么时候它会成为问题,有没有办法至少部分避免它?我在向 Solod (So) 添加并发性时遇到了这些问题,Solod 是 Go 的严格子集,可转换为纯 C,没有运行时,也没有垃圾收集器。最后,我得出的结论是,只要您诚实地进行权衡,就可以使用 pthread 做很多事情。这篇文章是关于我选择的基于 POSIX 线程的并发模型、它提供的好处以及它的局限性。 Mutex/Cond • 原子 • 池 • 通道 • 性能 • 设计 • 总结 Mutex/Cond So 的并发堆栈中的所有内容都建立在两个基本的 POSIX 原语之上:互斥体和条件变量。 sync.Mutex 是 pthread_mutex_t 的一个薄包装: // 摘自 So 的 stdlib 源代码。 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 structsync_Mutex { pthread_mutex_t mu ; }sync_Mutex; voidsync_Mutex_Lock (sync_Mutex * m) { int rc = pthread_mutex_lock (&​​m -> mu); if ( rc != 0 ) { so_panic( "同步:Mutex.Lock 失败" );这就是整个翻译——生成的 C 是 So 代码的近乎机械的镜像,只是噪音更大。从这里开始,我将主要展示 So 版本,但我也会为感兴趣的人提供 C 代码。这里没有什么令人兴奋的:sync.Mutex 是一个 pthread 互斥体包装器,如果出现问题(这种情况很少见),它会发生恐慌。伴随原语是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 structsync_Cond { pthread_cond_t cond ;同步互斥 * mu ; }sync_Cond;无效sync_Cond_Wait(sync_Cond * c); // 包装 pthread_cond_wait voidsync_Cond_Signal (sync_Cond * c); // 包装 pthread_cond_signal voidsync_Cond_Broadcast (sync_Cond * c); // 包装 pthread_cond_broadcast 这两种类型 — Mutex 和 Cond — 是基础。其他并发工具(Once、线程池、通道)是使用互斥体和一个或多个条件变量构建的。这对性能有几个影响,我们稍后会看到。原子 并不是所有东西都需要锁。 So 的同步/原子镜像 Go 的: Bool 、 Int32 、 Int64 、 Uint32 、 Uint64 和通用 Pointer[T] ,所有这些都带有 Load 、 Store 、 Swap 和 CompareAndSwap 方法。好处是这些根本不需要 pthreads。它们直接映射到 C 编译器的 __atomic 内置指令 - 与 Go 编译器发出的硬件指令相同。因此,它们没有理由变慢,而且它们也不是: Atomic op Go So Winner Load 2ns 2ns ~same Store 2ns 2ns ~same CompareAndSwap 13ns 13ns ~same 每个数字都是单个线程上一次操作的成本。 sync.Once 是有效使用原子的一个很好的例子。它的快速路径只需要一个原子加载——在给定的函数运行之后,以后每次调用 Do 都会检查一个标志并返回: type Once struct { mu Mutex doneatomic 。 Bool } // 当且仅当 Do 第一次为此 o 被调用时,Do 才会调用 f。 func ( o * 一次 ) Do ( f func ()) { if o .完毕 。 Load () { // 无锁快速路径 return } // 慢速路径... } 显示翻译后的 C 代码 typedef structsync_Once {sync_Mutex mu ; atomic_Bool 完成; }sync_Once; // 当且仅当 Do 第一次为此 o 被调用时,Do 才会调用 f。 voidsync_Once_Do (sync_Once * o , void ( * f )()) { if (atomic_Bool_Load ( & o -> did )) { // 无锁快速路径 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
这篇文章对您有帮助吗?

订阅66必读

每日精选科技资讯,直达你的邮箱