Boost Thread 程序设计指南
创建执行绪
就像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> #include <iostream> void hello() { std::cout << "Hello world, I'm a thread!" << std::endl; } int main( int argc, char * argv[]) { boost:: thread thrd(&hello); thrd.join(); return 0; } |
Mutex (mutual exclusion)
任何写过多执行绪程式的人都知道避免不同执行绪同时访问共用区域的重要性。如果一个执行绪要改变共用区域中某个资料,而与此同时另一执行绪正在读这个资料,那么结果将是未定义的。为了避免这种情况的发生就要使用一些特殊的原始类型和操作。其中最基本的就是互斥体(mutex,mutual exclusion的缩写)。一个互斥体一次只允许一个执行绪访问共用区。当一个执行绪想要访问共用区时,首先要做的就是锁住(lock)互斥体。如果其他的执行绪已经锁住了互斥体,那么就必须先等那个执行绪将互斥体解锁,这样就保证了同一时刻只有一个执行绪能访问共用区域。
互斥体的概念有不少变种。Boost执行绪库支持两大类互斥体,包括简单互斥体(simple mutex)和递回互斥体(recursive mutex)。如果同一个执行绪对互斥体上了两次锁,就会发生锁死(deadlock),也就是说所有的等待解锁的执行绪将一直等下去。有了递回互斥体,单一执行绪就可以对互斥体多次上锁,当然也必须解锁同样次数来保证其他执行绪可以对这个互斥体上锁。
在这两大类互斥体中,对于执行绪如何上锁还有多个变种。一个执行绪可以有三种方法来对一个互斥体加锁:
一直等到没有其他执行绪对互斥体加锁。
如果有其他互斥体已经对互斥体加锁就立即返回。
一直等到没有其他执行绪互斥体加锁,直到超时。
似乎最佳的互斥体类型是递回互斥体,它可以使用所有三种上锁形式。然而每一个变种都是有代价的。所以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中的代码,这样就不必手工写这些函数物件了。
例2:
#include <boost/thread/thread.hpp> #include <boost/thread/mutex.hpp> #include <iostream> boost::mutex io_mutex; struct count { count( int id) : id(id) { } void operator()() { for ( int i = 0; i < 10; ++i) { boost::mutex::scoped_lock lock(io_mutex); std::cout << id << ": " << i << std::endl; } } int id; }; int main( int argc, char * argv[]) { boost:: thread thrd1(count(1)); boost:: thread thrd2(count(2)); thrd1.join(); thrd2.join(); return 0; } |
例3: 这个例子和例2一样,除了使用Boost.Bind来简化创建执行绪携带资料,避免使用函数物件
#include <boost/thread/thread.hpp> #include <boost/thread/mutex.hpp> #include <boost/bind.hpp> #include <iostream> boost::mutex io_mutex; void count( int id) { for ( int i = 0; i < 10; ++i) { boost::mutex::scoped_lock lock(io_mutex); std::cout << id << ": " << i << std::endl; } } int main( int argc, char * argv[]) { boost:: thread thrd1( boost::bind(&count, 1)); boost:: thread thrd2( boost::bind(&count, 2)); thrd1.join(); thrd2.join(); return 0; } |
条件变数
有的时候仅仅依靠锁住共用资源来使用它是不够的。有时候共用资源只有某些状态的时候才能够使用。比方说,某个执行绪如果要从堆叠中读取资料,那么如果栈中没有资料就必须等待资料被压栈。这种情况下的同步使用互斥体是不够的。另一种同步的方式--条件变数,就可以使用在这种情况下。
条件变数的使用总是和互斥体及共用资源联系在一起的。执行绪首先锁住互斥体,然后检验共用资源的状态是否处于可使用的状态。如果不是,那么执行绪就要等待条件变数。要指向这样的操作就必须在等待的时候将互斥体解锁,以便其他执行绪可以访问共用资源并改变其状态。它还得保证从等到得执行绪返回时互斥体是被上锁得。当另一个执行绪改变了共用资源的状态时,它就要通知正在等待条件变数得执行绪,并将之返回等待的执行绪。
List4是一个使用了boost::condition的简单例子。有一个实现了有界缓存区的类和一个固定大小的先进先出的容器。由于使用了互斥体boost::mutex,这个缓存区是执行绪安全的。put和get使用条件变数来保证执行绪等待完成操作所必须的状态。有两个执行绪被创建,一个在buffer中放入100个整数,另一个将它们从buffer中取出。这个有界的缓存一次只能存放10个整数,所以这两个执行绪必须周期性的等待另一个执行绪。为了验证这一点,put和get在std::cout中输出诊断语句。最后,当两个执行绪结束后,main函数也就执行完毕了。
#include <boost/thread/thread.hpp> #include <boost/thread/mutex.hpp> #include <boost/thread/condition.hpp> #include <iostream> const int BUF_SIZE = 10; const int ITERS = 100; boost::mutex io_mutex; class buffer { public : typedef boost::mutex::scoped_lock scoped_lock; buffer() : p(0), c(0), full(0) { } void put( int m) { scoped_lock lock(mutex); if (full == BUF_SIZE) { { boost::mutex::scoped_lock lock(io_mutex); std::cout << "Buffer is full. Waiting..." << std::endl; } while (full == BUF_SIZE) cond.wait(lock); } buf[p] = m; p = (p+1) % BUF_SIZE; ++full; cond.notify_one(); } int get() { scoped_lock lk(mutex); if (full == 0) { { boost::mutex::scoped_lock lock(io_mutex); std::cout << "Buffer is empty. Waiting..." << std::endl; } while (full == 0) cond.wait(lk); } int i = buf[c]; c = (c+1) % BUF_SIZE; --full; cond.notify_one(); return i; } private : boost::mutex mutex; boost::condition cond; unsigned int p, c, full; int buf[BUF_SIZE]; }; buffer buf; void writer() { for ( int n = 0; n < ITERS; ++n) { { boost::mutex::scoped_lock lock(io_mutex); std::cout << "sending: " << n << std::endl; } buf.put(n); } } void reader() { for ( int x = 0; x < ITERS; ++x) { int n = buf.get(); { boost::mutex::scoped_lock lock(io_mutex); std::cout << "received: " << n << std::endl; } } } int main( int argc, char * argv[]) { boost:: thread thrd1(&reader); boost:: thread thrd2(&writer); thrd1.join(); thrd2.join(); return 0; } |
执行绪局部存储
大多数函数都不是可重入的。这也就是说在某一个执行绪已经调用了一个函数时,如果你再调用同一个函数,那么这样是不安全的。一个不可重入的函数通过连续的调用来保存静态变数或者是返回一个指向静态资料的指标。举例来说,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。
例5:
#include <boost/thread/thread.hpp> #include <boost/thread/mutex.hpp> #include <boost/thread/tss.hpp> #include <iostream> boost::mutex io_mutex; boost::thread_specific_ptr< int > ptr; struct count { count( int id) : id(id) { } void operator()() { if (ptr.get() == 0) ptr.reset( new int (0)); for ( int i = 0; i < 10; ++i) { (*ptr)++; boost::mutex::scoped_lock lock(io_mutex); std::cout << id << ": " << *ptr << std::endl; } } int id; }; int main( int argc, char * argv[]) { boost:: thread thrd1(count(1)); boost:: thread thrd2(count(2)); thrd1.join(); thrd2.join(); return 0; } |
仅运行一次的常式
还有一个问题没有解决:如何使得初始化工作(比如说构造函数)也是执行绪安全的。比方说,如果一个引用程式要产生唯一的全域的物件,由于产生实体顺序的问题,某个函数会被调用来返回一个静态的物件,它必须保证第一次被调用时就产生这个静态的物件。这里的问题就是如果多个执行绪同时调用了这个函数,那么这个静态物件的构造函数就会被调用多次,这样错误产生了。
解决这个问题的方法就是所谓的“一次实现”(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。
#include <boost/thread/thread.hpp> #include <boost/thread/once.hpp> #include <iostream> int i = 0; boost::once_flag flag = BOOST_ONCE_INIT; void init() { ++i; } void thread () { boost::call_once(&init, flag); } int main( int argc, char * argv[]) { boost:: thread thrd1(& thread ); boost:: thread thrd2(& thread ); thrd1.join(); thrd2.join(); std::cout << i << std::endl; return 0; } |
Boost执行绪库的未来
Boost执行绪库正在计画加入一些新特性。其中包括boost::read_write_mutex,它可以让多个执行绪同时从共用区中读取资料,但是一次只可能有一个执行绪向共用区写入资料;boost::thread_barrier,它使得一组执行绪处于等候状态,知道所有得执行绪都都进入了屏障区;boost::thread_pool,他允许执行一些小的routine而不必每一都要创建或是销毁一个执行绪。
Boost执行绪库已经作为标准中的类库技术报告中的附件提交给C+标准委员会,它的出现也为下一版C标准吹响了第一声号角。委员会成员对Boost执行绪库的初稿给予了很高的评价,当然他们还会考虑其他的多执行绪库。他们对在C标准中加入对多执行绪的支持非常感兴趣。从这一点上也可以看出,多执行绪在C+中的前途一片光明。