6

为什么说并发编程很难?

 1 year ago
source link: https://colobu.com/2023/07/16/why-conucrrent-programming-is-hard/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

为什么说并发编程很难?

最近看到一本关于算法的新书,买过来学习。
这本书中有一道并发问题,也是我先前多次分过的一道题:水分子的产生:

problem.jpg

这是一道并发题,在《The Little Book of Semaphores》v2.2.1 2006年版本中就有这道题("Building H2O"),而且据作者说这道题已经在伯克利大学的操作系统课程中十余年了,看起来是Andrews的并发编程的一道练习题。

这道题也被收编到leetcode的并发题中:H2O 生成。题目和这本书中的内容一样,是一道标记为中等难度的题目。

所以说这道题至少存在30年了,理论上大家对这道题目研究的就是透透的。

书中的解答和力扣中国的评论区解答的第一名是类似的,这是书上的解答:

public H2O() {
private Semaphore h = new Semaphore(2);
private Semaphore o = new Semaphore(2);
public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
h.acquire(1);
releaseHydrogen.run();
o.release(1);
public void oxygen(Runnable releaseOxygen) throws InterruptedException {
o.acquire(2);
releaseOxygen.run();
h.release(2);

首先这个解答有一个明显的瑕疵:private Semaphore o = new Semaphore(2);这一句应该初始化为0,而不是2,否则氧原子初始的时候毫无顾忌的就执行,一下释放四个氢原子。这是问题一。

力扣中国评论区第一名初始化就是正确的:

public H2O() {
private Semaphore h = new Semaphore(2);
private Semaphore o = new Semaphore(0);
......

这个代码看起来非常的简洁清爽,而且易懂,如果你提交代码,很快就通过,而且时间花费也很好,但是这个解答就对么?

回顾一下这个题目,其中有一条要求,就是每个水分子的原子必须来自不同的线程

但是上面的Java实现,两个氢原子可能来自同一个线程,违反了“三三成组”的原则。

可是为什么答案还能提交通过呢?只能说力扣的测试集设置的不好,它并没有区分H来自哪个线程,只要输出"HHO"的组合就算通过了。

一个修改版Java代码实现如下,代码也不复杂,增加了一个phaser同步原语:

import java.util.concurrent.Phaser;
import java.util.concurrent.Semaphore;
class H2O {
private Semaphore semO;
private Semaphore semH;
private Phaser phaser;
public H2O() {
semO = new Semaphore(1);
semH = new Semaphore(2);
phaser = new Phaser(3);
public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
semH.acquire();
releaseHydrogen.run();
phaser.arriveAndAwaitAdvance();
semH.release();
public void oxygen(Runnable releaseOxygen) throws InterruptedException {
semO.acquire();
releaseOxygen.run();
phaser.arriveAndAwaitAdvance();
semO.release();

所以你看,即使是一个存在了30年的并发老题,要想写对也不容易。

我在百度也Review了几十个同学的good coder题,发现有至少一半以上的同学都出现过并发的问题,所以虽然说使用Go、Java语言很容易写并发程序,但是能写好确实是意见比较困难的事。

重点来了,这里我给大家推荐我的一本即将上线的新书《深入理解Go并发编程》,经过了4年多的沉淀和积累,厚积薄发,全面介绍Go并发编程方方面面,敬请期待。

cpgo.png

在这本书中,也剖析了这道题,并给出了Go语言的一个解法:

package water
import (
"context"
"github.com/marusama/cyclicbarrier"
"golang.org/x/sync/semaphore"
type H2O struct {
semaH *semaphore.Weighted
semaO *semaphore.Weighted
b cyclicbarrier.CyclicBarrier
func New() *H2O {
return &H2O{
semaH: semaphore.NewWeighted(2),
semaO: semaphore.NewWeighted(1),
b: cyclicbarrier.New(3),
func (h2o *H2O) hydrogen(releaseHydrogen func()) {
h2o.semaH.Acquire(context.Background(), 1)
// releaseHydrogen() 输出一个H
releaseHydrogen()
h2o.b.Await(context.Background())
h2o.semaH.Release(1)
func (h2o *H2O) oxygen(releaseOxygen func()) {
h2o.semaO.Acquire(context.Background(), 1)
// releaseOxygen() 输出一个O
releaseOxygen()
h2o.b.Await(context.Background())
h2o.semaO.Release(1)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK