最近leetcode的算法题中新增加了一个并发问题的分类, 目前提供了四道题,一道简单题,两道中级题和一道Hard题。这是leetcode尝试在并发算法分类的一个尝试,虽然目前还在摸索阶段,有的题目可以通过作弊
的方式提供高分答案,有的即使是通过了测试,但是其实还是有并发的问题的,只不过测试用例没有覆盖到而已。
这也说明了并发编程并不是一件很容易的事情,但是还是很高兴leetcode能在这个方面往前走一步。其中的最难的那道题,看它第一眼的时候觉得还是比较简单的,但是仔细思考后却发现实现它并不是一件很容易的事情,即使目前的接受率才51.9%,但其实已接收的提交有很多还是有并发的问题的,只不过由于验证的问题并没有验证出来。
这道题是这样子的,利用两个氢原子和一个氧原子生成一氧化二氢, 一氧化二氢这种化合物在常温常压下为无色无味的透明液体,也就是我们常说的水。这道题目我大致翻译一下:
有
oxygen
(氧)和hydrogen
(氢) 两个线程,你的目标是把它们分组生成水分子。当然这里有道闸门(barrier)在水分子生成之前不得不等待。oxygen
和hydrogen
线程每三个会被分成一组,包括两个hydrogen
线程和一个oxygen
,它们三个每个都会产生一个对应的原子组合成水分子。你必须保证当前分组内的各个线程提供的原子组合成一个水分子之后这些线程才能参与产生下一个水分子。换句话说:
- 假如一个
oxygen
到达了闸门,而其它hydrogen
还没来,那么这个oxygen
会一直等待另两个hydrogen
线程- 假如
hydrogen
线程到达了闸门,而其它的线程还没来,那么它会等待oxygen
线程和另外一个hydrogen
线程所以,一个水分子总是由两个
hydrogen
线程和oxygen
提供。你不必担心线程是如何分组的,外部调度器可以正确地把两个
hydrogen
线程和一个oxygen
线程分成一组。问题的关键是线程组要完整地通过闸门。你需要编写同步代码,保证线程能够按照前面的要求有序地生成水分子。
举几个例子:
|
|
|
|
约束条件:
- 总的输入是
3n
, 这里1 ≤ n ≤ 30
- 总的H的数量是
2n
- 总的O的数量是
O
目前leetcode这类题目只支持C、C++、Python和Java,还不支持其它编程语言。一开始我觉得这道题目还是比较容易解决的,使用三个Java的Semaphore对线程进行编排,提交解决方案也顺利通过的,耗时也在最前面。过了两天再品味这道题的时候,发现实现还是有问题的,因为同一个hydrogen
线程提供两个氢原子也可以顺利通过测试,随后看了讨论区的答案,很多也是有问题的,输出结果可能没问题,但是实际线程的运行并不符合题目的要求。
下面尝试使用Go来解决这个问题。
首先定义这个题的实现框架,保持和网站上其它语言一致:
|
|
hydrogen
线程会调用hydrogen
方法,而oxygen
线程会调用oxygen
方法。一组线程(三个线程,包括两个hydrogen
和一个oxygen
线程)调用完各自的方法后,输出完HHO
(或者HOH
、OHH
)后才能进行下一次的调用。
一个hydrogen
线程在本次HHO
完成后才能再次调用hydrogen
方法。
一个oxygen
线程在本次HHO
完成后才能再次调用oxygen
方法。
你现在的工作就是在这个代码中添加同步逻辑,保证上面的约束要满足,也就是程序能够按照要求顺利进行。
思来想去,还是回归原来的题目上,题目中已经使用了barrier的概念,我们还是使用它来实现更简洁。
实现代码如下:
Go标准库没有Barrier
同步原语,不像Java中的并发类那么丰富,所以这里使用了github.com/marusama/cyclicbarrier
第三方的库。
方法开始之前利用Semphore
控制要编排的goroutine的数量和种类,然后使用CyclicBarrier
来保证三个goroutine同时准备好了各自的原子。一旦三个goroutine都准备好了,释放Semphore
准备下一次的水分子合成。
|
|
|
|
|
|
|
|
本文提供了Go语言和Java语言的实现,还提供了测试用例来测试(我们就使用三个goroutine来测试):
如果不是生成水分子,而是使用四个线程一组生成双氧水,也就是过氧化氢 H₂O₂ ,又该如何编写同步代码呢?