如何看待七牛 CEO 许式伟开源的 Cerl?

打字不方便,贴个链接,背景上下文信息里面讲的挺清楚了。qiniu/cerl · GitHub 请大家点评分析一下,强调一下,这个帖子不是为了搭个擂台围观撕逼的,而是期望各位从技术角度分析一下,cerl有足够的说服力支持其对erlang的评价吗?纯分析一下cerl技术本身也欢迎,谢谢。
关注者
1,197
被浏览
95,079

17 个回答

围观群众一枚,有些想法分享一下。是关于 CERL 的 README 中提到的[ECUG专题回顾]《再谈CERL:详论Go与Erlang的并发编程模型差异》这篇文章的。

文章中作者提到了 Erlang 在关于锁的问题上存在自相矛盾(尽力避免锁,但实践中不得用到锁)。我觉得这个论点和论据都是有问题的。

12306订票的过程中肯定有锁,从顾客选票开始直到最终完成交易前,这张票一定是被锁住,无法被其他顾客操作的。
它在本质上和程序中用到的锁是一样,即避免两个并发的控制流对一个共享数据进行操作。
如果用 Erlang 写12306,它显然无法避免这个锁,但如果据此就说 Erlang 在尝试避免锁这件事情上失败了,感觉上是有点怪怪的对吧?

这世界上的锁有千千万万种,在讨论 Erlang 是否成功避免了锁之前,应当先明确, Erlang 尝试解决的是哪种锁?
多线程程序中,并行线程对全局变量(共享状态)的读写会造成竞态,因此引入了锁加以解决。这种锁的最大问题是什么?不是死锁也不是饥饿(原因后述),是程序员容易忘记用锁。这才是这种锁带给程序员的最大心智负担。

Erlang 尝试解决的就是这种锁,不是其他的锁。它的解决办法是复古,多线程程序需要这种锁,是因为线程共享地址空间。Erlang 直接干掉线程概念,提供独立地址空间的廉价进程。Erlang 不是尝试避免这种锁,它是通过机制让这种锁不存在。如果这种锁不存在,也就谈不上忘记用它。

重要的事情需要说两遍:Erlang 解决的是多线程并发模型带来的锁,它的解决办法是提供一种新的用户态进程并发模型。除此以外,任何不是因多线程的原因所产生的锁(以及相关问题),Erlang的并发模型都没有解决,但在我看来,它也并没有企图去解决那些锁。

比如死锁,它不是锁带来的问题,它的本质是N个任务间对彼此的完成有环形依赖关系。这不是多线程所带来的,无论是用单进程,还是多个物理进程,无论你的代码中是否用到锁,这个问题都存在。你在Erlang里让两个进程A,B互相并发做一个gen_server call,这里面没有任何锁,但一样会死锁。A依赖B完成,同时B又依赖A完成,这个逻辑本身是错误的,你怎么实现都是错的,它不是锁的问题,也不是多线程的问题,更不是 Erlang 的并发模型企图解决的问题。

时间不早了,还有些来不及写了,另外也没有对原文中举的几个例子作进一步的分析,就先到这里吧。
先从Coding Style上说几点吧:
0. 很多 compiling warnings,包括 async/include/async/Shell.h 中 ApplicationInit 的构造函数把 servicesInit 成员和 timerInit 成员的初始化顺序写反了,这种错误不止一处,还有 FastTimer.h。
1. space 和 tab 混用
2. async/include/async/DataQueue.h 里改了 pack(1) 但没有改回去,基本上谁用谁死。
3. 很多class应该禁用copy-ctor和assignment operator,但没有禁用,例如 async/include/async/Mutex.h 中的 ScopedLock,FastTimer.h 中的 FastTimerQueue,socketio/sync.h 中的 SyncSocketFile 等等。基本上C++98/03中需要写destructor的class要么应该自己定义copy-ctor和assignment operator,要么应该禁用 copy-ctor和assignment operator。
4. 不该用 explicit 的构造函数:async/include/async/socketio/epoll.h 的 ConnectSocket 。
5. 不是 64-bit clean,这从第 0 点的编译器警告能反映出一些来,另外一些混合位宽的整数运算也可能存在风险(int vs. size_t,int vs. pointer 等等)。
6. 不该用继承:IoCompletionPort.h 中的 IoCompletionPortST 继承了 std::deque<IoCompletionPort::Message>;sockets.h 中的 SockaddrIn 继承了 C 头文件中的 SOCKADDR_IN struct。
7. const member function 不应该返回 non-const reference,例如 Buffered.h 中的 FileT& BufferedReader::get_file() const。
8. 命名不地道,async/include/async/Mutex.h 中的 FiberNestableMutex 应该是 FiberReentrantMutex 。
9. POD 成员没有初始化,例如 MessageQueue.h 中的 FiberMessageQueue 在构造函数中没有为 WaitingNode* m_waitingList; 成员赋初值,只给 BOOL m_fWorking; 赋了值。
10. 错误的强制转型,task/ShareFiberTaskPool.h 中 ShareFiberTaskPool::addTask() 把 TaskStub* 强制转型为 MessageQueue::Node*,而 TaskStub 和 Node 两个类型无关,只是 object layout 上有点“关系”,这属于 hacking。
11. msgq/condition.h 中的 LimitedMessageQueue 是个 bounded producer consumer queue,其实现也有问题,首先是 push() 只在队列长度 0 -> 1 的时候 notify_one(),这有可能造成多个 consumers 中只唤醒一个,其余饥饿,哪怕队列中有多个可供消费的数据;类似的,pop() 只在队列长度 full -> not full 的时候 notify_one(),这有可能造成 producers 饥饿;最后,clear() 应该 notify producer。MessageQueue::push() 只在队列长度 0 -> 1 的时候 notify_all(),有可能造成不必要的唤醒。

too many errors to continue ……

我只管中窥豹看了看 async/ 下的代码片段,没有阅读代码整体逻辑,也没有实际运行程序,就目前的感受,说句实话,连 Effective C++ 的条款都没有完全做到啊。不是说C++程序绝对不能打破 Effective C++ 的规则,而是目前的代码打破这些 rule 并没有合理的理由。

我只好当这代码是 proof of concept 了。

话又说回来,有代码总比空口瞎嚷嚷的强10000倍。