7.3. 可扩展性与多线程

用 Boost.Asio 这样的库来开发应用程序,与一般的 C++ 风格不同。 那些可能需要较长时间才返回的函数不再是以顺序的方式来调用。 不再是调用阻塞式的函数,Boost.Asio 是启动一个异步操作。 而那些需要在操作结束后调用的函数则实现为相应的句柄。 这种方法的缺点是,本来顺序执行的功能变得在物理上分割开来了,从而令相应的代码更难理解。

象 Boost.Asio 这样的库通常是为了令应用程序具有更高的效率。 应用程序不需要等待特定的函数执行完成,而可以在期间执行其它任务,如开始另一个需要较长时间的操作。

可扩展性是指,一个应用程序从新增资源有效地获得好处的能力。 如果那些执行时间较长的操作不应该阻塞其它操作的话,那么建议使用 Boost.Asio. 由于现今的PC机通常都具有多核处理器,所以线程的应用可以进一步提高一个基于 Boost.Asio 的应用程序的可扩展性。

如果在某个 boost::asio::io_service 类型的对象之上调用 run() 方法,则相关联的句柄也会在同一个线程内被执行。 通过使用多线程,应用程序可以同时调用多个 run() 方法。 一旦某个异步操作结束,相应的 I/O 服务就将在这些线程中的某一个之中执行句柄。 如果第二个操作在第一个操作之后很快也结束了,则 I/O 服务可以在另一个线程中执行句柄,而无需等待第一个句柄终止。

  1. #include <boost/asio.hpp>
  2. #include <boost/thread.hpp>
  3. #include <iostream>
  4.  
  5. void handler1(const boost::system::error_code &ec)
  6. {
  7. std::cout << "5 s." << std::endl;
  8. }
  9.  
  10. void handler2(const boost::system::error_code &ec)
  11. {
  12. std::cout << "5 s." << std::endl;
  13. }
  14.  
  15. boost::asio::io_service io_service;
  16.  
  17. void run()
  18. {
  19. io_service.run();
  20. }
  21.  
  22. int main()
  23. {
  24. boost::asio::deadline_timer timer1(io_service, boost::posix_time::seconds(5));
  25. timer1.async_wait(handler1);
  26. boost::asio::deadline_timer timer2(io_service, boost::posix_time::seconds(5));
  27. timer2.async_wait(handler2);
  28. boost::thread thread1(run);
  29. boost::thread thread2(run);
  30. thread1.join();
  31. thread2.join();
  32. }

上一节中的例子现在变成了一个多线程的应用。 通过使用在 boost/thread.hpp 中定义的 boost::thread 类,它来自于 Boost C++ 库 Thread,我们在 main() 中创建了两个线程。 这两个线程均针对同一个 I/O 服务调用了 run() 方法。 这样当异步操作完成时,这个 I/O 服务就可以使用两个线程去执行句柄函数。

这个例子中的两个计时数均被设为在五秒后触发。 由于有两个线程,所以 handler1()handler2() 可以同时执行。 如果第二个计时器触发时第一个仍在执行,则第二个句柄就会在第二个线程中执行。 如果第一个计时器的句柄已经终止,则 I/O 服务可以自由选择任一线程。

线程可以提高应用程序的性能。 因为线程是在处理器内核上执行的,所以创建比内核数更多的线程是没有意义的。 这样可以确保每个线程在其自己的内核上执行,而没有同一内核上的其它线程与之竞争。

要注意,使用线程并不总是值得的。 以上例子的运行会导致不同信息在标准输出流上混合输出,因为这两个句柄可能会并行运行,访问同一个共享资源:标准输出流 std::cout。 这种访问必须被同步,以保证每一条信息在另一个线程可以向标准输出流写出另一条信息之前被完全写出。 在这种情形下使用线程并不能提供多少好处,如果各个独立句柄不能独立地并行运行。

多次调用同一个 I/O 服务的 run() 方法,是为基于 Boost.Asio 的应用程序增加可扩展性的推荐方法。 另外还有一个不同的方法:不要绑定多个线程到单个 I/O 服务,而是创建多个 I/O 服务。 然后每一个 I/O 服务使用一个线程。 如果 I/O 服务的数量与系统的处理器内核数量相匹配,则异步操作都可以在各自的内核上执行。

  1. #include <boost/asio.hpp>
  2. #include <boost/thread.hpp>
  3. #include <iostream>
  4.  
  5. void handler1(const boost::system::error_code &ec)
  6. {
  7. std::cout << "5 s." << std::endl;
  8. }
  9.  
  10. void handler2(const boost::system::error_code &ec)
  11. {
  12. std::cout << "5 s." << std::endl;
  13. }
  14.  
  15. boost::asio::io_service io_service1;
  16. boost::asio::io_service io_service2;
  17.  
  18. void run1()
  19. {
  20. io_service1.run();
  21. }
  22.  
  23. void run2()
  24. {
  25. io_service2.run();
  26. }
  27.  
  28. int main()
  29. {
  30. boost::asio::deadline_timer timer1(io_service1, boost::posix_time::seconds(5));
  31. timer1.async_wait(handler1);
  32. boost::asio::deadline_timer timer2(io_service2, boost::posix_time::seconds(5));
  33. timer2.async_wait(handler2);
  34. boost::thread thread1(run1);
  35. boost::thread thread2(run2);
  36. thread1.join();
  37. thread2.join();
  38. }

前面的那个使用两个计时器的例子被重写为使用两个 I/O 服务。 这个应用程序仍然基于两个线程;但是现在每个线程被绑定至不同的 I/O 服务。 此外,两个 I/O 对象 timer1timer2 现在也被绑定至不同的 I/O 服务。

这个应用程序的功能与前一个相同。 在一定条件下使用多个 I/O 服务是有好处的,每个 I/O 服务有自己的线程,最好是运行在各自的处理器内核上,这样每一个异步操作连同它们的句柄就可以局部化执行。 如果没有远端的数据或函数需要访问,那么每一个 I/O 服务就象一个小的自主应用。 这里的局部和远端是指象高速缓存、内存页这样的资源。 由于在确定优化策略之前需要对底层硬件、操作系统、编译器以及潜在的瓶颈有专门的了解,所以应该仅在清楚这些好处的情况下使用多个 I/O 服务。