C++ Report: 编程原则


译者:封尘浪

--------------------------------------------------------------------------------

The Programming Principle
编程原则
Douglas C. Schmidt
Editor-in-chief
C++ Report
October 1996

I celebrated my 34th birthday recently. I thought 34 would be another one of those mundane birthdays beween 30 and retirement. But this year was different -- my parents gave me copy of Scott Adam's "The Dilbert Principle" as a gift. Unless you've spent the '90s programming in Lower Elbonia, I'm sure you know Dilbert as the posterchild for all the foibles we experience in today's techno-eccentric workplace.

 

我刚刚过完34岁生日,我曾想过34岁的生日只不过是30岁以后平凡生日中的其中一个罢了,但这一年与众不同, 我的父母送给我 Scott Adam’s" The Dibert Principle(搞怪的总则)",除非你花费了90秒的时间Lower Elbonia, 我确信你知道Dibert在所有的小癖好中是一个标准,就在今天我经历了一个技术上的搞怪。

 

One of my favorite Dilbert comic strips in the book is one where Dilbert's boss (the guy with the satanic hair) says to Dilbert: "I saw the code for your computer program yesterday. It looked easy. It's just a bunch of typing, and half the words were spelled wrong. And don't get me started about your over-use of colons." His "insights" reminded me of all the times I've listened to pompous theorists and managers (who've never written a line of production software in their lives) dismiss the accomplishments and contributions of programmers.

 

一个我最喜欢的小搞怪在这本书上就有一个,终极搞怪(邪恶的人拿着枪)他对小搞怪说:“我看了你昨天写的代码,它看起来很简单,仅仅是一串一串的单词,有一半都写错了,不要这样开始,你在过度复制代码。"他总是极具洞察力的提醒我,但我听起来就像傲慢的理论家和管理者,(谁没有在产品代码中用过其它代码啦)程序员们拒绝做有价值的事。

 

This "fond" memory started me thinking about what makes programming hard. There are many culprits, ranging from the chronic lack of stable requirements, to the buggy tools and components we layer our software upon, to the subtle and pernicious tradeoffs between forces like performance and correctness. This editor's corner focuses on the latter theme, using a recent experience to illustrate some of the foibles in my techno-eccentric workplace.

 

这个“多情”的内存开始让我思考什么使编程变得困难,哪儿有许多肇事者,缺乏习惯性的寻找所需要的东西,软件中有调试工具和组件,正确性和强制表现的折中是微妙的和有害的,编码的困难在这之后,引用最近的经验去说明一些在我工作中的小癖好。

 

When I'm not teaching courses, advising students, or proof-reading submissions to the C++ Report, I develop concurrent OO communication software frameworks and applications in C++ for my research. One of the fundamental components in a concurrent network programmer's toolkit is a thread-safe message queue.

 

当我没有讲课,劝告学生们或者校阅C++Report的时候, 我研究C++并开发了正式的面向交流软件框架和应用程序,基础组件之一,在一个并行网络程序员的工具包是一个线程安全信息队列。

 

Thread-safe message queues are commonly used in concurrent programs to exchange information (e.g., work requests or data buffers) between suppliers and consumers. For instance, Steve Vinoski and I presented a thread-safe message queue implementation in our April '96 Object Interconnections column. We used this queue to exchange messages between a supplier thread and a pool of consumer threads in a stock quoter server. In our example, the supplier reads requests from network clients and inserts them into the message queue. The consumer threads block on the queue in "hungry puppy" fashion, waiting to process request messages from the supplier.

 

线程安全消息队列在并行程序中被频繁的用来改变信息,服务端和客户端之间(工作要求或者数据缓冲),举个例子,“Steve Vinoski 和我启用了一个线程安全队列在我们的“April'96 上相互联系,我们使用这个队列去改变消息,在服务器端和在股东客户服务端的线程池中,在我们的这个例子中服务器读取网络客户的请求--插入他们的消息队列,客户的线程在队列中阻塞(像饥饿的小狗狂吠),等待所要求的进程被服务器处理。

 

The trick to making the message queue work for multi-threaded programs is to use synchronization mechanisms like mutual exclusion (mutex) locks and condition variables. Using these mechanisms, the basic logic for safely inserting a message into a queue that can be accessed concurrently by multiple threads looks like this:

这种把戏使消息队列工作在多线程程序中,利用同步作用,利用这些作用拒绝外来互斥体限制变量,使用这些方法,基本的逻辑给安全有趣的消息进入队列提供了同时访问,像下面这样利用多重线程。

 

void Message_Queue::insert (Message *message)
{
  // Make sure we own the lock before
  // entering the critical section.
  this->mutex_.lock (); 

  // Wait until there's room in the queue.
  while (/* queue is full */)
    this->not_full_cond_.wait (this->mutex_);

  // Insert message at end of queue (omitted).

  // Inform one waiting consumer that there's a message.
  this->not_empty_cond_.signal (); 

  // Let another thread acquire the lock.
  this->mutex_.unlock ();
}


 

Likewise, the basic logic for safely removing a message from the queue that can be accessed concurrently by multiple threads looks like this:


同理可得,用来从这个队列中安全的移除一个消息,能够同时的访问通过多个线程,像下面这样:

 

Message *
Message_Queue::remove (void)
{
  this->mutex_.lock ();

  // Wait until there's a message in the queue.
  while (/* queue is empty */)
    this->not_empty_cond_.wait (this->mutex_);

  // Remove message from front of queue (omitted).

  // Inform one waiting supplier that there's room.
  this->not_full_cond_.signal (); 
  this->mutex_.unlock ();
}


 

Although this code works correctly, it can perform poorly if the underlying threads implementation incurs a context switch on every this->not_{full,empty}_cond_.signal() operation. Since context switches are one of the major performance costs in concurrent programs it's worthwhile to minimize them.

 

尽管这些代码能够正确工作,但它表现不佳,如果潜在的进程招致上下文,打开每个this->not_{full,empty}_cond_.signal()操作,直到在执行程序时改变他们其中一个主要的性能开销,这种减少开销是值得做的。

 

If you think about how to optimize the message queue for a while, you might arrive at the conclusion that you can omit the not_empty_cond_.signal() if the message wasn't inserted into an empty queue. In this case, the consumer wouldn't have been blocked in the not_empty_cond_.wait(), so there's no point in signaling the consumer. Likewise, it might appear that there's no point in signaling the supplier if it didn't remove a message from a full queue. In this case, the supplier wouldn't have been blocked in the not_full_cond_.wait() either.

 

如果你认为关于怎样优化循环中的消息队列,你能得到一个结论,是你可以省去not_empty_cond_.signal() 如果这个消息没有插入在一个空队列中,在这种情况下,客户不会有阻塞 not_empty_cond_.wait(), 因此那儿没有指向客户的信号。同样的,他也许表现出那儿没有指向服务器的信号,如果它没有从空队列中移除一个消息,在这种情况下,服务器没有阻塞not_full_cond_.wait()。

 

Armed with this insight, you might be tempted to rewrite the code as follows:
有了这种洞察力,你也许想试着像下面这样重写代码:

 

void Message_Queue::insert (Message *message)
  // ...
  // Only inform one waiting consumer that there's 
  // a message if the queue was previously empty.
  if (queue_was_empty)
    this->not_empty_cond_.signal (); 
  // ...

and 


Message *Message_Queue::remove (void)
  // ...
  // Only inform one waiting supplier that there's room
  // in the queue if the queue was previously full.
  if (queue_was_full)
    this->not_full_cond_.signal (); 
  // ...


 

Unfortunately, if you did this, you'd have just fallen prey to Knuth's Law: "Premature optimization is the root of all evil" [KNUTH]! The reason, of course, is that this particular optimization works correctly only if there's a single supplier and a single consumer. If there are multiple consumers, however, it's very easy to end up in a situation where all consumers block indefinitely.
For instance, consider the case where 5 consumers in our stock quoter server's thread pool are blocked in calls to not_empty_cond_.wait(), waiting for requests to show up from the clients. If the supplier thread then inserts 5 "shutdown" messages into the queue in rapid succession all of them may be enqueued before any of the consumer threads wake up. However, in the "optimized" implementation above, the supplier only invokes the not_empty_cond_.signal() method once, i.e., when the first shutdown message is inserted into the previously empty queue. Hence, if the one awakened consumer thread terminates immediately after receiving the shutdown message, all the other threads will remain blocked indefinitely.

 

遗憾的是,如果你这样做了,你刚才就受骗了,Knuth's 的法则:“过早的优化是万恶的根源”,是个理由,当然了,这个详细的优化工作仅仅只在一个服务器和客户端信号上,如果那儿有多重客户,然而,他非常容易在所有客户长时间的阻塞的情况下结束。举个例子,考虑有5个客户在我们的服务器线程池中因为调用not_empty_cond_.wait()阻塞了,从客户端上等待相应,如果服务器线程在插入5个关闭消息到队列中时,一系列的调用使得在插入之前客户线程苏醒,然而在这个最优的启动上,当第一个关闭消息插入到之前的空队列中时,服务器仅仅调用not_empty_cond_.signal()方法一次。因此,如果一个苏醒着的客户线程立即结束之后收到关闭消息,所有其它的线程将会无限期的阻塞。

 

It turns out that the "Right Way[TM]" to optimize this code is to keep track of the number of waiting consumers and to only invoke signal() if the number is greater than 0. Moreover, if the condition variable implementation does this for you already, you can achieve this optimization without having to change the original code!

 

它原来是以正确的方式优化这段代码,如果这个数字大于0,保持利用数字跟踪等待客户和仅仅调用signal()。或者,做好准备如果这种情况有可能
变化,你能获得这种优化,还能修改原始代码!


So what's the moral of this story? First, I think it illustrates one reason why programming is so hard: because we're constantly trying to balance various forces that lure us towards either the scylla of performance or the charybdis of correctness (or any of the myriad other software quality factors that conflict with performance). As usual, the best defense is a deep knowledge of good concurrency patterns and techniques, plus a large quantify of diet coke...

 

这个故事给了我们什么教训那?首先,我认为它说明了为什么编程如此困难,“因为我们不断的试着去平衡各方势力,引诱我们对青蟹性能正确性或蟳(或者任何各种的软件质量冲突因素),像平常一样,最好的防范就是深入了解并发模式和技术,保持饮食健康!

 

Second, it essential to get feedback from other developers who have solved these types of problems before. The example of "premature optimization" I showed here is based on software I wrote recently. I'd made the optimization because I was initially dealing with only one supplier and one consumer. Unfortunately, the code crept into a class library that was then used to create thread pools, which triggered the bug. Fortunately, a bright colleague, Karlheinz Dorn of Siemens, found the error of my ways and Tim Harrison and I quickly fixed the bug. This type of substantive feedback is far more valuable than having your boss point out the overuse of colons...

 

其次,必要的途径是从那些解决过此类问题的开发者中获取经验。这是个草率的优化,我列举它是因为这是我最近写的软件,我做了一些优化,因为我最初只是供应商的一个客户,遗憾的是,这些代码在类库中运行的很慢,用它们创建线程池是很容易触发BUG,幸运的是,明智的同行西门子公司的
Karlheinz Dorn 找到了错误,我和 Tim Harrison 很快修复了这个BUG, 这种实质性的反馈比你的老板指着你说"复制过度"要有价值吧...

 

Incidentally, if you'd like to learn more about multi-threaded programming, I highly recommend a new book called "Programming with Threads" by Steve Kleiman, Devang Shah and Bart Smaalders (ISBN 0-13-172389-8). Although most of their examples are in C, the book is a masterpiece of good, practical concurrency techniques. If you'd like to obtain C++ components for multi-threading (such as the *correct* thread-safe message queue), check out the ACE release, which is a freely available C++ network programming framework obtainable via the WWW. Finally, to bring this full circle, Dilbert and his pals are also online. The Dilbert Principle really is an excellent book -- I highly recommend it. But don't wait until your 34th birthday to get it ;-).

 

顺便说一句,如果你想学习更多的关于多线程编程知识的话,我极力推荐一本新书"Programming with Threads"by Steven Kleiman,Devang Shah and Bart Smaalders (ISBN 0-13-172389-8),书中例子大多都是用C语言编写的,这是一本优秀的用来实践并发技术的书,如果你想用C++组件编写多线程程序(比如"合适的"线程安全消息队列),点击ACE网站,那儿有免费的可用的C++网络编程框架可以获得,最后,故事的结尾,呆伯特和他的朋友也在线,呆伯特的总则总是本卓越的书,我极力推荐,但不要等到你34岁生日时才得到啊.

点击下载《Programming with Threads》http://ishare.iask.sina.com.cn/f/23350751.html


References
[Knuth] Donald Knuth, ``"Structured Programming with go to Statements,'' Computing Surveys, Vol. 6, No. 4, December, 1974, page 268.

 

--------------------------------------------------------------------------------

Back to C++ Report Editorials home page.

Last modified 11:34:38 CDT 28 September 2006

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值