BOOST 线程完全攻略 - 基础篇

时间:2024-04-27 11:04:16

http://blog.****.net/iamnieo/article/details/2908621

2008-09-10 12:48 9202人阅读 评论(3) 收藏 举报

目录(?)[+]

C++多线程开发是一个复杂的事情,mfc下提供了CWinThread类,和AfxBeginThread等等函数,但是在使用中会遇到很多麻烦事情,例如线程之间参数传递的问题,我们一般都是把参数new一个结构体,传递给子线程,然后释放的工作交给了子线程,这样不可避免会有内存泄漏的危险,例如线程关闭的问题,我们一般用WaitForSingleObject来完成线程关闭工作,但是这个函数并不一定保证线程能收到要关闭的信号,这样父亲已经退出工作了,子线程还在工作,程序也会有潜在的危险。

所以我已经慢慢不再用这套线程机制了,boost标准stl库的出现,让我眼前一亮,boost所推行的简洁代码概念和模板概念,让我有了清风扑面的感觉,本文将介绍如何使用boost::thread来取代不太安全的MFC线程编程。

本文所牵涉到的源码下载地址:

http://download.****.net/source/618903

基础篇主要是汇集和转载一些已有网文,让初学者入门boost::thread.

一. 安装 原地址http://www.douban.com/group/topic/2494650/

1.下载boost_1_34_1压缩文件,解压缩到d:/boost_1_34_1/目录下

   
  2.编译bjam 
   
  从vs2005的工具菜单进入命令提示窗口(一定要从这进),cd到d:/boost_1_34_1/tools/jam/src下执行build.bat,会在d:/boost_1_34_1/tools/jam/src/bin.ntx86/產生bjam.exe,將bjam.exe複製到d:/boost_1_34_1/下。 
   
  3.设定编译环境 
   
  修改user-config.jam (d:/boost_1_34_1/tools/build/v2/user-config.jam) 的MSVC configuration 
  # MSVC configuration 
   
  # Configure msvc (default version, searched in standard location 
  # and PATH). 
  # using msvc ; 
  using msvc : 8.0 : : <compileflags>/wd4819 <compileflags>/D_CRT_SECURE_NO_DEPRECATE <compileflags>/D_SCL_SECURE_NO_DEPRECATE <compileflags>/D_SECURE_SCL=0 ; 
   
  4.编译boost 
  將目錄移至d:/boost_1_34_1/下執行 
   bjam --without-python --toolset=msvc-8.0 --prefix=d:/boost install 
  參數說明 
  --without-python 表示不使用 python 
  --toolset : 所使用compiler,Visual Studio 2005為msvc-8.0 
  --prefix:指定編譯後library的安裝目錄 
  这一步要花很长时间(大约50分钟) 
  5.设定vs2005环境 
  Tools -> Options -> Projects and Solutions -> VC++ Directories 
  在Library files加上d:/boost/lib 
  在Include files加上d:/boost/include/boost-1_34_1

1 创建线程

就像std::fstream类就代表一个文件一样,boost::thread类就代表一个可执行的线程。缺省构造函数创建一个代表当前执行线程的实例。一个重载的构造函数以一个不需任何参数的函数对象作为参数,并且没有返回值。这个构造函数创建一个新的可执行线程,它调用了那个函数对象。

起先,大家认为传统C创建线程的方法似乎比这样的设计更有用,因为C创建线程的时候会传入一个void*指针,通过这种方法就可以传入数据。然而,由于Boost线程库是使用函数对象来代替函数指针,那么函数对象本身就可以携带线程所需的数据。这种方法更具灵活性,也是类型安全(type-safe)的。当和Boost.Bind这样的功能库一起使用时,这样的方法就可以让你传递任意数量的数据给新建的线程。

目前,由Boost线程库创建的线程对象功能还不是很强大。事实上它只能做两项操作。线程对象可以方便使用==和!=进行比较来确定它们是否是代表同一个线程;你还可以调用boost::thread::join来等待线程执行完毕。其他一些线程库可以让你对线程做一些其他操作(比如设置优先级,甚至是取消线程)。然而,由于要在普遍适用(portable)的接口中加入这些操作不是简单的事,目前仍在讨论如何将这些操组加入到Boost线程库中。

Listing1展示了boost::thread类的一个最简单的用法。 新建的线程只是简单的在std::out上打印“hello,world”,main函数在它执行完毕之后结束。

  1. #include <boost/thread/thread.hpp>
  2. #include <iostream>
  3. void hello()
  4. {
  5. std::cout <<
  6. "Hello world, I'm a thread!"
  7. << std::endl;
  8. }
  9. int main(int argc, char* argv[])
  10. {
  11. boost::thread thrd(&hello);
  12. thrd.join();
  13. return 0;
  14. }

2 互斥体

任何写过多线程程序的人都知道避免不同线程同时访问共享区域的重要性。如果一个线程要改变共享区域中某个数据,而与此同时另一线程正在读这个数据,那么结果将是未定义的。为了避免这种情况的发生就要使用一些特殊的原始类型和操作。其中最基本的就是互斥体(mutex,mutual exclusion的缩写)。一个互斥体一次只允许一个线程访问共享区。当一个线程想要访问共享区时,首先要做的就是锁住(lock)互斥体。如果其他的线程已经锁住了互斥体,那么就必须先等那个线程将互斥体解锁,这样就保证了同一时刻只有一个线程能访问共享区域。

互斥体的概念有不少变种。Boost线程库支持两大类互斥体,包括简单互斥体(simple mutex)和递归互斥体(recursive mutex)。如果同一个线程对互斥体上了两次锁,就会发生死锁(deadlock),也就是说所有的等待解锁的线程将一直等下去。有了递归互斥体,单个线程就可以对互斥体多次上锁,当然也必须解锁同样次数来保证其他线程可以对这个互斥体上锁。

在这两大类互斥体中,对于线程如何上锁还有多个变种。一个线程可以有三种方法来对一个互斥体加锁:

  1. 一直等到没有其他线程对互斥体加锁。
  2. 如果有其他互斥体已经对互斥体加锁就立即返回。
  3. 一直等到没有其他线程互斥体加锁,直到超时。

似乎最佳的互斥体类型是递归互斥体,它可以使用所有三种上锁形式。然而每一个变种都是有代价的。所以Boost线程库允许你根据不同的需要使用最有效率的互斥体类型。Boost线程库提供了6中互斥体类型,下面是按照效率进行排序:

boost::mutex,

boost::try_mutex,

boost::timed_mutex,

boost::recursive_mutex,

boost::recursive_try_mutex,

boost::recursive_timed_mutex

如果互斥体上锁之后没有解锁就会发生死锁。这是一个很普遍的错误,Boost线程库就是要将其变成不可能(至少时很困难)。直接对互斥体上锁和解锁对于Boost线程库的用户来说是不可能的。mutex类通过teypdef定义在RAII中实现的类型来实现互斥体的上锁和解锁。这也就是大家知道的Scope Lock模式。为了构造这些类型,要传入一个互斥体的引用。构造函数对互斥体加锁,析构函数对互斥体解锁。C++保证了析构函数一定会被调用,所以即使是有异常抛出,互斥体也总是会被正确的解锁。

这种方法保证正确的使用互斥体。然而,有一点必须注意:尽管Scope Lock模式可以保证互斥体被解锁,但是它并没有保证在异常抛出之后贡献资源仍是可用的。所以就像执行单线程程序一样,必须保证异常不会导致程序状态异常。另外,这个已经上锁的对象不能传递给另一个线程,因为它们维护的状态并没有禁止这样做。

List2给出了一个使用boost::mutex的最简单的例子。例子*创建了两个新的线程,每个线程都有10次循环,在std::cout上打印出线程id和当前循环的次数,而main函数等待这两个线程执行完才结束。std::cout就是共享资源,所以每一个线程都使用一个全局互斥体来保证同时只有一个线程能向它写入。

许多读者可能已经注意到List2中传递数据给线程还必须的手工写一个函数。尽管这个例子很简单,如果每一次都要写这样的代码实在是让人厌烦的事。别急,有一种简单的解决办法。函数库允许你通过将另一个函数绑定,并传入调用时需要的数据来创建一个新的函数。 List3向你展示了如何使用Boost.Bind库来简化List2中的代码,这样就不必手工写这些函数对象了。

  1. #include <boost/thread/thread.hpp>
  2. #include <boost/thread/mutex.hpp>
  3. #include <iostream>
  4. boost::mutex io_mutex;
  5. struct count
  6. {
  7. count(int id) : id(id) { }
  8. void operator()()
  9. {
  10. for (int i = 0; i < 10; ++i)
  11. {
  12. boost::mutex::scoped_lock
  13. lock(io_mutex);
  14. std::cout << id << ": "
  15. << i << std::endl;
  16. }
  17. }
  18. int id;
  19. };
  20. int main(int argc, char* argv[])
  21. {
  22. boost::thread thrd1(count(1));
  23. boost::thread thrd2(count(2));
  24. thrd1.join();
  25. thrd2.join();
  26. return 0;
  27. }

除了使用Boost.Bind来简化创建线程携带数据,避免使用函数对象

  1. #include <boost/thread/thread.hpp>
  2. #include <boost/thread/mutex.hpp>
  3. #include <boost/bind.hpp>
  4. #include <iostream>
  5. boost::mutex io_mutex;
  6. void count(int id)
  7. {
  8. for (int i = 0; i < 10; ++i)
  9. {
  10. boost::mutex::scoped_lock
  11. lock(io_mutex);
  12. std::cout << id << ": " <<
  13. i << std::endl;
  14. }
  15. }
  16. int main(int argc, char* argv[])
  17. {
  18. boost::thread thrd1(
  19. boost::bind(&count, 1));
  20. boost::thread thrd2(
  21. boost::bind(&count, 2));
  22. thrd1.join();
  23. thrd2.join();
  24. return 0;
  25. }

3 条件变量

有的时候仅仅依靠锁住共享资源来使用它是不够的。有时候共享资源只有某些状态的时候才能够使用。比方说,某个线程如果要从堆栈中读取数据,那么如果栈中没有数据就必须等待数据被压栈。这种情况下的同步使用互斥体是不够的。另一种同步的方式--条件变量,就可以使用在这种情况下。

条件变量的使用总是和互斥体及共享资源联系在一起的。线程首先锁住互斥体,然后检验共享资源的状态是否处于可使用的状态。如果不是,那么线程就要等待条件变量。要指向这样的操作就必须在等待的时候将互斥体解锁,以便其他线程可以访问共享资源并改变其状态。它还得保证从等到得线程返回时互斥体是被上锁得。当另一个线程改变了共享资源的状态时,它就要通知正在等待条件变量得线程,并将之返回等待的线程。

List4是一个使用了boost::condition的简单例子。有一个实现了有界缓存区的类和一个固定大小的先进先出的容器。由于使用了互斥体boost::mutex,这个缓存区是线程安全的。put和get使用条件变量来保证线程等待完成操作所必须的状态。有两个线程被创建,一个在buffer中放入100个整数,另一个将它们从buffer中取出。这个有界的缓存一次只能存放10个整数,所以这两个线程必须周期性的等待另一个线程。为了验证这一点,put和get在std::cout中输出诊断语句。最后,当两个线程结束后,main函数也就执行完毕了。

  1. #include <boost/thread/thread.hpp>
  2. #include <boost/thread/mutex.hpp>
  3. #include <boost/thread/condition.hpp>
  4. #include <iostream>
  5. const int BUF_SIZE = 10;
  6. const int ITERS = 100;
  7. boost::mutex io_mutex;
  8. class buffer
  9. {
  10. public:
  11. typedef boost::mutex::scoped_lock
  12. scoped_lock;
  13. buffer()
  14. : p(0), c(0), full(0)
  15. {
  16. }
  17. void put(int m)
  18. {
  19. scoped_lock lock(mutex);
  20. if (full == BUF_SIZE)
  21. {
  22. {
  23. boost::mutex::scoped_lock
  24. lock(io_mutex);
  25. std::cout <<
  26. "Buffer is full. Waiting..."
  27. << std::endl;
  28. }
  29. while (full == BUF_SIZE)
  30. cond.wait(lock);
  31. }
  32. buf[p] = m;
  33. p = (p+1) % BUF_SIZE;
  34. ++full;
  35. cond.notify_one();
  36. }
  37. int get()
  38. {
  39. scoped_lock lk(mutex);
  40. if (full == 0)
  41. {
  42. {
  43. boost::mutex::scoped_lock
  44. lock(io_mutex);
  45. std::cout <<
  46. "Buffer is empty. Waiting..."
  47. << std::endl;
  48. }
  49. while (full == 0)
  50. cond.wait(lk);
  51. }
  52. int i = buf[c];
  53. c = (c+1) % BUF_SIZE;
  54. --full;
  55. cond.notify_one();
  56. return i;
  57. }
  58. private:
  59. boost::mutex mutex;
  60. boost::condition cond;
  61. unsigned int p, c, full;
  62. int buf[BUF_SIZE];
  63. };
  64. buffer buf;
  65. void writer()
  66. {
  67. for (int n = 0; n < ITERS; ++n)
  68. {
  69. {
  70. boost::mutex::scoped_lock
  71. lock(io_mutex);
  72. std::cout << "sending: "
  73. << n << std::endl;
  74. }
  75. buf.put(n);
  76. }
  77. }
  78. void reader()
  79. {
  80. for (int x = 0; x < ITERS; ++x)
  81. {
  82. int n = buf.get();
  83. {
  84. boost::mutex::scoped_lock
  85. lock(io_mutex);
  86. std::cout << "received: "
  87. << n << std::endl;
  88. }
  89. }
  90. }
  91. int main(int argc, char* argv[])
  92. {
  93. boost::thread thrd1(&reader);
  94. boost::thread thrd2(&writer);
  95. thrd1.join();
  96. thrd2.join();
  97. return 0;
  98. }

线程局部存储

大多数函数都不是可重入的。这也就是说在某一个线程已经调用了一个函数时,如果你再调用同一个函数,那么这样是不安全的。一个不可重入的函数通过连续的调用来保存静态变量或者是返回一个指向静态数据的指针。 举例来说,std::strtok就是不可重入的,因为它使用静态变量来保存要被分割成符号的字符串。

有两种方法可以让不可重用的函数变成可重用的函数。第一种方法就是改变接口,用指针或引用代替原先使用静态数据的地方。比方说,POSIX定义了strok_r,std::strtok中的一个可重入的变量,它用一个额外的char**参数来代替静态数据。这种方法很简单,而且提供了可能的最佳效果。但是这样必须改变公共接口,也就意味着必须改代码。另一种方法不用改变公有接口,而是用本地存储线程(thread local storage)来代替静态数据(有时也被成为特殊线程存储,thread-specific storage)。

Boost线程库提供了智能指针boost::thread_specific_ptr来访问本地存储线程。每一个线程第一次使用这个智能指针的实例时,它的初值是NULL,所以必须要先检查这个它的只是否为空,并且为它赋值。Boost线程库保证本地存储线程中保存的数据会在线程结束后被清除。

List5是一个使用boost::thread_specific_ptr的简单例子。其中创建了两个线程来初始化本地存储线程,并有10次循环,每一次都会增加智能指针指向的值,并将其输出到std::cout上(由于std::cout是一个共享资源,所以通过互斥体进行同步)。main线程等待这两个线程结束后就退出。从这个例子输出可以明白的看出每个线程都处理属于自己的数据实例,尽管它们都是使用同一个boost::thread_specific_ptr。

  1. include <boost/thread/thread.hpp>
  2. #include <boost/thread/mutex.hpp>
  3. #include <boost/thread/tss.hpp>
  4. #include <iostream>
  5. boost::mutex io_mutex;
  6. boost::thread_specific_ptr<int> ptr;
  7. struct count
  8. {
  9. count(int id) : id(id) { }
  10. void operator()()
  11. {
  12. if (ptr.get() == 0)
  13. ptr.reset(new int(0));
  14. for (int i = 0; i < 10; ++i)
  15. {
  16. (*ptr)++;
  17. boost::mutex::scoped_lock
  18. lock(io_mutex);
  19. std::cout << id << ": "
  20. << *ptr << std::endl;
  21. }
  22. }
  23. int id;
  24. };
  25. int main(int argc, char* argv[])
  26. {
  27. boost::thread thrd1(count(1));
  28. boost::thread thrd2(count(2));
  29. thrd1.join();
  30. thrd2.join();
  31. return 0;
  32. }

5 仅运行一次的例程

还有一个问题没有解决:如何使得初始化工作(比如说构造函数)也是线程安全的。比方说,如果一个引用程序要产生唯一的全局的对象,由于实例化顺序的问题,某个函数会被调用来返回一个静态的对象,它必须保证第一次被调用时就产生这个静态的对象。这里的问题就是如果多个线程同时调用了这个函数,那么这个静态对象的构造函数就会被调用多次,这样错误产生了。

解决这个问题的方法就是所谓的“一次实现”(once routine)。“一次实现”在一个应用程序只能执行一次。如果多个线程想同时执行这个操作,那么真正执行的只有一个,而其他线程必须等这个操作结束。为了保证它只被执行一次,这个routine由另一个函数间接的调用,而这个函数传给它一个指针以及一个标志着这个routine是否已经被调用的特殊标志。这个标志是以静态的方式初始化的,这也就保证了它在编译期间就被初始化而不是运行时。因此也就没有多个线程同时将它初始化的问题了。Boost线程库提供了boost::call_once来支持“一次实现”,并且定义了一个标志boost::once_flag及一个初始化这个标志的宏BOOST_ONCE_INIT。

List6是一个使用了boost::call_once的例子。其中定义了一个静态的全局整数,初始值为0;还有一个由BOOST_ONCE_INIT初始化的静态boost::once_flag实例。main函数创建了两个线程,它们都想通过传入一个函数调用boost::call_once来初始化这个全局的整数,这个函数是将它加1。main函数等待着两个线程结束,并将最后的结果输出的到std::cout。由最后的结果可以看出这个操作确实只被执行了一次,因为它的值是1。

  1. #include <boost/thread/thread.hpp>
  2. #include <boost/thread/once.hpp>
  3. #include <iostream>
  4. int i = 0;
  5. boost::once_flag flag =
  6. BOOST_ONCE_INIT;
  7. void init()
  8. {
  9. ++i;
  10. }
  11. void thread()
  12. {
  13. boost::call_once(&init, flag);
  14. }
  15. int main(int argc, char* argv[])
  16. {
  17. boost::thread thrd1(thread);
  18. boost::thread thrd2(thread);
  19. thrd1.join();
  20. thrd2.join();
  21. std::cout << i << std::endl;
  22. return 0;
  23. }

6 Boost线程库的未来

Boost线程库正在计划加入一些新特性。其中包括boost::read_write_mutex,它可以让多个线程同时从共享区中读取数据,但是一次只可能有一个线程向共享区写入数据;boost::thread_barrier,它使得一组线程处于等待状态,知道所有得线程都都进入了屏障区;boost::thread_pool,他允许执行一些小的routine而不必每一都要创建或是销毁一个线程。

Boost线程库已经作为标准中的类库技术报告中的附件提交给C++标准委员会,它的出现也为下一版C++标准吹响了第一声号角。委员会成员对Boost线程库的初稿给予了很高的评价,当然他们还会考虑其他的多线程库。他们对在C++标准中加入对多线程的支持非常感兴趣。从这一点上也可以看出,多线程在C++中的前途一片光明。

7 参考资料:

  1. The Boost.Threads Library by Bill Kempf
  2. Visit the Boost website at <http://www.boost.org>.