Golang中借助GOMAXPROCS实现DCAS/MWCAS的技术疑问
背景
我正尝试在Golang和Rust中实现双比较交换(DCAS)以及可能的多宽比较交换(MWCAS),但由于没有CPU支持该指令,因此需要利用语言提供的特性来实现。
以下是一段示例代码:
func HttpServer(parallelism int) { runtime.GOMAXPROCS(parallelism) called := 0 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { time.Sleep(2 * time.Second) called++ fmt.Fprint(w, called) }) http.ListenAndServe(":8090", nil) }
这段代码整体存在问题,但当调用HttpServer(1)时却能按预期工作:假设调用该接口1000次,我会得到1000个不同的called值(从1到1001),且耗时不到3秒。我认为这是因为当设置parallelism = 1时,Go运行时会将time.Sleep之后的处理器代码作为原子块执行,同时等待所有其他处理器完成。
借助这种技巧,我可以轻松实现DCAS和MWCAS,无需使用性能更慢的原子操作(慢5倍)或互斥锁(慢25倍)。
问题
- 这段代码是否存在问题?我是否遗漏了某些细节?为何即使将parallelism设为1,竞态检测器仍会检测到竞态?(但结果正确,似乎不影响输出)
- 是否可以为单个goroutine及其后代设置
runtime.GOMAXPROCS?例如,让一个goroutine及其子goroutine以并行度1执行,另一个以并行度10执行。目前我能想到的唯一方法是启动多个进程并通过IPC通信。
问题1解答
这段代码存在根本性的未定义行为,只是在特定测试场景下巧合得到了正确结果,绝对不能依赖这种逻辑。
竞态检测器报警的原因
竞态检测器的判断逻辑很直接:只要多个执行流(这里是请求触发的多个goroutine)对同一个共享变量called进行无同步的读写操作,就会标记为竞态。即便设置了GOMAXPROCS(1),Go的goroutine调度器仍然会自主进行上下文切换——比如time.Sleep结束后,当前goroutine可能不会立刻继续执行,调度器可能先切换到其他等待的请求goroutine。你当前没遇到错误只是调度时机的巧合,不是语言层面的保证。
你遗漏的关键细节
GOMAXPROCS(1)只是限制了进程同时使用的OS线程数为1,但goroutine的调度权完全在Go runtime手里,没有任何机制保证time.Sleep后的代码会作为"原子块"执行。举个极端情况:某个goroutine在执行called++的中间(比如已经读取了旧值,但还没写回新值)被调度器暂停,切换到另一个请求goroutine,就会出现两个goroutine基于同一个旧值自增,最终called只增加1,结果直接出错。
另外,called变量没有任何同步机制,不同goroutine对它的读写可能因为CPU缓存的原因,无法及时看到其他goroutine的修改,这也是未定义行为的一部分。
问题2解答
不可以为单个goroutine或其后代单独设置runtime.GOMAXPROCS。runtime.GOMAXPROCS是全局设置,作用于整个进程的所有goroutine,它控制的是Go runtime可使用的OS线程数量,无法针对单个goroutine或goroutine树做隔离。
如果需要实现不同goroutine组的并行度隔离,除了多进程+IPC的方式,还可以考虑两种替代方案:
- 使用
sync.WaitGroup或自定义任务队列,手动控制同一组goroutine的并发数,比如让某一组任务最多同时运行1个goroutine,另一组最多运行10个。 - 利用
runtime.LockOSThread()将特定goroutine绑定到固定OS线程,但这也无法单独设置该线程的并行度,只是让goroutine不被调度到其他线程,本质上还是依赖全局的GOMAXPROCS设置。
内容的提问来源于stack exchange,提问作者Mascarpone

