12.1 Boost-Thread(笔记)

1 thread

thread库为c++增加了线程处理的能力,它提供了简明清晰的线程、互斥量等概念,可以很容易地创建多线程应用程序。thread库也是高度可移植的,它支持使用最广泛的Windows和POSIX线程,用它编写的代码不需要修改就可以在Windows、UNiX等操作系统上编译运行。

1.1 编译thread库

thread库需要date_time库支持,因此必须先编译date_time库。

thread库需要编译才能使用,bjam命令如下:

bjam -toolset=msvc -with-thread -build-type=complete stdlib=stlport stage

如果要使用工程中嵌入源码的方式,可以直接在cpp文件中包含thread库的实现代码,如下:

bjam -toolset=msvc -with-thread -build-type=complete stdlib=stlport stage

如要要使用工程中嵌入源码的方式,可以直接在cpp文件中包含thread库的实现代码,如下:

//tprebuild.cpp
#define BOOST_DATE_TIME_SOURCE 						//thread库需要使用date_time库
#define BOOST_THREAD_NO_LIB
#include <boost/thread.hpp>

#ifdef _MSC_VER										//Windows系统的线程
extern "C" void tss_cleanup_implemented(void){}		//一个必须的函数
#include <libs/thread/src/win32/thread.cpp>
#include <libs/thread/src/win32/tss_dll.cpp>
#include <libs/thread/src/win32/tss_pe.cpp>
#else												//unix系统相关的实现文件
#include <libs/thread/src/pthread/thread.cpp>
#include <libs/thread/src/pthread/once.cpp>
#endif

预编译源文件tprebuild.cpp中定义了一个extern "C"空函数tss_cleanup_implememted(void)。这是因为当前Windows环境下的thread库没有实现自动tss(线程本地存储)清理功能,需要用户来完成。如果链接时没有这个函数就会发生link错误,因此嵌入编译需自行定义该函数来阻止link错误。通常可以用一个简单的空实现,也可以自行定义其他清理代码。

1.2 使用thread库

thread位于名字空间boost,为了使用thread组件需要包含头文件<boost/thread.hpp>。

因为thread库使用了date_time库,所以嵌入源码编译时,需要定义两个宏:BOOST_DATE_TIME_SOURCEBOOST_THREAD_NO_LIB,或者直接在工程中定义BOOST_ALL_NO_LIB,即:

#define BOOST_DATE_TIME_SOURCE
#define BOOST_THREAD_NO_LIB
#include <boost/thread.hpp>
using namespace boost;

Linux/UNIX下链接thread库时还需要使用-lpthread选项来链接POSIX线程库。

1.3 时间功能

在多线程编程时经常要用到超时处理,需要表示时间的概念。thread库直接利用date_time库提供了对时间的支持,可以使用millisec/milliseconds、microsec/mircroseconds等时间长度类表示超时的时间,或者用ptime表示某个确定的时间点。例如:

this_thread::sleep(posix_time::seconds(2));				//睡眠2秒钟
cout << "sleep 2 secondes" << endl;

为了更好地表述时间的线程相关含义,thread库重新定义了一个新的时间类型system_time,它是posix_time::ptime的同义词,即:

typedef boost::posix_time::ptime system_time;

同时thread库也提供了一个自由函数get_system_time(),它调用microsec_clock类方便地获得当前的UTC时间值。

此外,thread库还有一个非常简单的xtime结构,它简单地包装了ptime类,也具有一定的时间处理能力,但功能较少,主要供thread库内部使用,对库用户不推荐。

1.4 互斥量

互斥量是一种用于线程同步的手段,它可以在多线程编程中,防止多个线程同时操作共享资源(或称临界区)。一旦一个线程锁住了互斥量,那么其他线程就必须等待它解锁互斥量后才能再访问共享资源。

thread提供了七种互斥量类型(实际上只有五种),分别是:

  • mutex:独占式的互斥量,最简单常用的一种互斥量类型;
  • try_mutex:它是mutex的同义词,为了与兼容以前的版本而提供;
  • timed_mutex:它也是独占式的互斥量,但提供超时锁定功能;
  • recursive_mutex:递归式互斥量,可以多次锁定,相应地也要多次解锁;
  • recursive_try_mutex:它是recursive_mutex的同义词,为了与兼容以前的版本而提供;
  • recursive_timed_mutex:它也是递归式互斥量,基本功能同recursive_mutex,但提供超时锁定功能;
  • shared_mutexmultiple_reader/single_writer型的共享互斥量(又称读写锁)。

1.4.1 类摘要

这些互斥量除了互斥功能不同外基本接口都很接近,具有类似下面的声明形式:

class mutex: boost::noncopyable
{
public:
	void lock();
    bool try_lock();
    void unlock();
    
    bool time_lock(system_time const & abs_time);
    template<typename TimeDuration>
    bool timed_lock(TimeDuration const & relative_time);
    
    typedef unspecified-type scoped_lock;
    typedef unspecified-type scoped_try_lock;
};

mutex对象在创建后表示了一个互斥量,成员函数lock()用于线程阻塞等待直至获得互斥量的所有权(即锁定);try_lock()尝试锁定互斥量,如果锁定成功返回true,否则返回false,它是非阻塞的;当线程使用完共享资源后,应该及时使用unlock()解除对互斥量的锁定。

成员函数timed_lock()只属于timed_mutexrecursive_timed_mutex,它的行为结合了lock()try_lock(),阻塞等待一定的时间试图锁定互斥量,如果时间到还未锁定则发回false。等待的时间可以是绝对时间(一个UTC的时间点),也可以是从当前开始的相对时间(时间长度)。

1.4.2 互斥量的用法

mutex的基本用法如下:

mutex mu;								//声明一个互斥量对象
try
{
    mu.lock();							//锁定互斥量
    cout << "some operations" << endl;	//临界区操作
    mu.unlock();						//解锁互斥量
}
catch(...)								//必须使用try-catch块保证解锁互斥量
{
    mu.unlock();
}

直接使用mutex的成员函数来锁定互斥量不够方便,而且在发生异常导致退出作用域等情况下很可能会忘记解除锁定;因此thread库又提供了一系列RAII型的lock_guard类,由于辅助锁定互斥量。它们在构造时锁定互斥量,在析构时自动解锁,从而保证了互斥量的正确操作,避免遗忘解锁,就像一个智能指针。

mutex类使用内部类型定义scoped_lockscoped_try_lock定义了两种lock_guard对象,分别对应执行lock()try_lock()

使用lock_guard类可以取消麻烦的try-catch块,上面的代码可以改为:

mutex mu;
mutex::scoped_lock lock(mu);			//使用RAII型的lock_guard
cout << "some operations" << endl;

1.4.3 互斥量实例

下面使用mutex实现一个原子操作的计数器basic_atom,它可以安全地在多线程环境下正确计数:

template<typename T>
class basic_atom:noncopyable
{
private:
	T n;
	typedef mutex mutex_t;						//互斥量类型定义
	mutex_t mu;
public:
	basic_atom(T x = T()):n(x){}				//构造函数
	T operator++()								//前置式递增操作符
	{
		mutex_t::scoped_lock lock(mu);			//锁定互斥量
		return ++n;
	}
	operator T(){return n;}						//类型转换操作符定义
};

basic_atom是一个模板类,因此它可以配合模板参数提供不同范围的计数,并且它提供了隐式类型转换操作,用起来像一个普通整数,例如:

typedef basic_atom<int> atom_int;
atom_int x;
cout << ++x;

1.5 线程对象

thread类是thread库的核心类,负责启动和管理线程对象,在概念和操作上都与POSIX线程很相似,它的类摘要如下:

class thread
{
public:
    //构造函数
    thread();
    template <class F> explicit thread(F f);
    template <class F,class A1,class A2,...>
    thread(F f,A1 a1,A2 a2,...);
  	
    //线程管理
    bool joinable() const;
    void join();
    bool timed_join(const system_time& wait_until);
    template<typename TimeDuration>
    bool timed_join(TimeDuration const& rel_time);
    void detach();
    void interrupt();
    bool interruption_requested() const;
    
    //静态成员函数
    static void yield();
    static void sleep(const system_time& xt);
    static unsigned hardware_concurrency();
    
    //支持各种比较操作符
    id get_id() const;
    bool operator==(const thread& other) const;
};

在使用thread对象时需要注意它是不可拷贝的,虽然它没有从boost::noncopyable继承,但thread内部把拷贝构造函数和赋值操作都声明为私有的,不能对它进行赋值或者拷贝构造。

thread通过特别的机制支持转移语义,因为我们可以编写创建线程的工厂函数,封装thread的创建细节,返回一个thread对象。例如,下面的模板函数可以创建一个thread对象:

template<typename F>
thread make_thread(F f)
{return thread(f);}

1.6 创建线程

从某种程度来说,线程就是在进程的另一个空间里运行的一个函数,因此线程的创建需要传递给thread对象一个无参的可调用物(函数或函数对象),它必须具有operator()以供线程执行。

如果可调用物不是无参的,那么thread的构造函数也支持直接传递所需的参数,这些参数将被拷贝并发生调用时传递给函数。这是一个非常体贴方便地重载构造函数,比传统的使用void*来传递参数要好很多。thread的构造函数支持最多传递九个参数,这通常足够用了。

在传递参数时需要注意,thread使用的是参数的拷贝,因此要求可调用物和参数类型都支持拷贝。如果希望传递给线程引用值就需要使用ref库进行包装,同时必须保证被引用的对象在线程执行期间一直存在,否则会引发未定义行为。

1.6.1 启动线程

当成功创建一个thread对象后,线程就立刻开始执行,thread不提供类似start()begin()那样的方法。

假设我们有如下的一个函数,它向标准输出流打印字符串:

mutex io_mu;				//io流是个共享资源,不是线程
							//安全的,需要锁定的
void printing(atom_int& x,const string& str)
{
    for(int i = 0;i < 5;++i)
    {
        mutex::scoped_lock lock(io_mu);			//锁定io流操作
        cout << str << ++x << endl;
    }
}

int main()
{
    atom_int x;						//原子操作的计数器
    
    //使用临时thread对象启动线程
    thread(printing,ref(x),"hello");				//向函数传递多个参数
    thread(printing,ref(x),"boost");				//使用ref库传递引用
    this_thread::sleep(posix_time::seconds(2));		//等待2秒钟
}

在当线程启动后,我们必须调用sleep()来等待线程执行结束,否则会因为main()return语句导致主线程结束,而其他的线程还没有机会运行而一并结束。

通常不应该使用这种"死"等线程结束的方法,因为不可能精确地知道线程会执行多少时间,我们需要用其他更好的方法等待线程结束。

1.6.2 join 和 timed_join

thread的成员函数joinable()可以判断thread对象是否标识了一个可执行对象的线程体。如果joinable()返回true,我们就可以调用成员函数join()或者timed_join()来阻塞等待线程执行结束。两者的区别如下:

  • join()一直阻塞等待,直到线程结束;
  • time_join()阻塞等待线程结束,或者阻塞等待一定的时间段,然后不管线程是否结束都返回。注意,它不必阻塞等待指定的时间长度,如果在这段时间里线程运行结束,即是时间未到它也会返回。

使用join()可以这样操作thread对象:

atom_int x;
thread t1(printing,ref(x),"hello");
thread t2(printing,ref(x),"boost");

t1.timed_join(posix_time::seconds(1));			//最多等待1秒然后返回
t2.join();										//等待t2线程结束再返回,不管执行多少时间

1.6.3 与线程执行体分离

可以使用成员函数detach()thread与线程执行体手动分离,此后thread对象不代表任何线程体,失去对线程体的控制。例如:

thread t1(printing,ref(x),"hello");			//启动线程
t1.detach();								//与线程执行体分离,但线程继续运行

thread与线程执行体分离时,线程执行体将不受影响地继续执行,直到运行结束,或者随主线程一起结束。

当线程执行完毕或者thread对象被销毁时,thread对象也会自动与线程执行体分离,因此,当不需要操作线程体时,我们可以使用临时对象来启动一个线程,就像之前示范的那样。

1.6.4 使用bind与function

有时在thread的构造函数中写传递给调用函数的参数很麻烦,尤其是在使用大量线程对象的时候。这时我们可以使用Boostbindfunction库:

  • bind库可以把函数所需的参数绑定到一个函数对象;
  • function则可以存储bind表达式的结果,供程序以后使用。

例如:

thread t3(bind(printing,rex(x),"thread"));			//bind表达式
function<void()> f = bind(printing,5,"mutex");
thread(f);											//使用function对象

1.7 操作线程

通常情况下一个非空的thread对象唯一地标识了一个可执行的线程体,是joinable()的,成员函数get_id()可以返回线程id对象。

线程id提供了完整的比较操作符和流输出操作,因此可以被放入标准容器用来管理线程,thread类也通过线程id支持线程间的比较操作。例如:

thread t1(...),t2(...);					//创建两个线程对象
cout << t1.get_id() << endl;			//输出t1的id
assert(t1 != t2);						//比较两个线程对象
t1.detach();							//分离t1代表的线程执行体,但线程仍然继续执行
assert(t1.get_id() == thread::id());	//t1不再标识任何线程

thread类还提供了三个很有用的静态成员函数:yield()sleep()hardware_concurrency(),它们用来在线程中完成一些特殊的工作。

  • yield()函数指示当前线程放弃时间片,允许其他的线程运行。
  • sleep()让线程睡眠等待一小段时间,注意它要求参数是一个system_time UTC时间点而不是时间长度。
  • hardware_concurrency()可以获得硬件系统可并行的线程数量,即CPU数量或者CPU内核数量,如果无法获取信息则返回0。

例如,下面的代码令当前线程睡眠1秒钟,然后输出可并行的线程数量:

thread::sleep(get_system_time() + posix_time::seconds(1));
cout << thread::hardware_concurrency() << endl;

thread库也在子名字空间this_thread里提供了3个自由函数——get_id()yield()sleep()用于操作当前线程。它们的功能同thread类的同名函数,分别用来获得线程id、方式时间片和睡眠等待,但this_threadsleep()函数不仅可以使用绝对的UTC时间点,也可以使用时间长度。

例如:

this_thread::sleep(posix_time::seconds(2));					//睡眠2秒钟
cout << this_thread::get_id();
this_thread::yield();

1.8 中断线程

thread的成员函数interrupt()允许正在执行的线程被中断,被中断的线程会抛出一个thread_interupted异常,它是一个空类,不是std::exception或者boost::exception的子类。thread_interrupted异常应该在线程执行函数里捕获并处理,如果线程不处理这个异常,那么默认的动作是终止线程。

下面的代码定义了一个函数to_interrupt(),它的行为类似之前的printing()函数,但它使用this_thread::sleep()睡眠1秒钟再输出字符串:

void to_interrupt(atom_int& x,const string& str)
{
    try
    {
        for(int i = 0;i < 5;++i)
        {
            this_thread::sleep(posix_time::seconds(1));		//睡眠1秒钟
            mutex::scoped_lock lock(io_mu);					//锁定io输出流
            cout << str << ++x << endl;
        }
    }
    catch(thread_interrupted&)						//捕获中断异常
    {
        cout << "thread_interrupted" << endl;		//显示消息
    }
}
int main()
{
    atom_int x;
    thread t(to_interrupt,ref(x),"hello");
    this_thread::sleep(posix_time::seconds(2));		//睡眠2秒钟
    t.interrupt();									//要求线程中断执行
    t.join();										//因为线程已经中断,所以join()立即返回
}

程序的运行结果如下:

hello1
hello2
thread_interrupted

1.8.1 线程的中断点

线程不是在任意时间都可以被中断的。如果我们将to_interrupt()函数中的sleep()睡眠等待去掉,那么计时在主线程中调用interrupt()线程也不会被中断。

thread库预定义了若干个线程的中断点,只有当前线程执行到中断点的时候才能被中断,一个线程可以拥有任意多个中断点。

thread库预定义了共9个中断点,它们都是函数,如下:

  • thread::join();
  • thread::timed_join();
  • condition_variable::wait();
  • condition_variable::timed_wait();
  • condition_variable_any::timed_wait();
  • thread::sleep();
  • this_thread::sleep();
  • this_thread::interruption_point()。

这些中断点中的前8个都是某种形式的等待函数,表明线程在阻塞等待的时候可以被中断。而最后一个位于子名字空间this_threadinterruption_point()则是一个特殊的中断点函数,它并不等待,只是起到一个标签的作用,表示线程执行到这个函数所在的语句就可以被中断。

例如,我们把to_interrupt()函数改为使用interruption_point()

void to_interrupt(atom_int& x,const string& str)
{
    try
    {
        for(int i = 0;i < 5; ++i)
        {
            mutex::scoped_lock lock(io_mu);			//锁定io流操作
            cout << str << ++x << endl;
            this_thread::interruption_point();		//这里允许中断
        }
    }
    catch(thread_interrupted&)						//捕获中断异常
    {...}
}
int main()
{
    atom_int x;
    thread t(to_interrupt,ref(x),"hello");			//启动线程
    t.interrupt();									//然后立即中断线程
    t.join();
}

main()不再等待一段时间,而是启动线程后立即调用interrupt()来中断线程:

那么线程会输出一条"hello1"字符串后遇到中断点,并且被thread对象所中断执行。

1.8.2 启用/禁用线程中断

缺省情况下线程都是允许中断的,但thread库允许控制线程的中断行为。

thread库在子名字空间this_thread提供了一组函数和类来共同完成线程的中断启用和禁用:

  • interruption_enabled()函数检测当前线程是否允许中断;

  • interruption_requested()函数检测当前线程是否被要求中断;

  • disable_interruption是一个RAII类型的对象,它在构造时关闭线程的中断,析构时自动恢复现线程的中断状态。

    disable_interruption的生命期内线程始终是不可中断的,除非使用了restore_interruption对象。

  • restore_interruption只能在disable_interruption的作用域内使用,它在构造时临时打开线程的中断状态,在析构时有关闭中断状态。

仍然以之前的to_interrupt()函数为例,我们为它增加中断的启用和禁用:

void to_interrupt(atom_int& x,const string& str)
{
    try
    {
        using namespace this_thread;				//打开this_thread名字空间
    	assert(interruption_enable());				//此时允许中断
    	for(int i = 0;i < 5; ++i)
    	{
            disable_interruption di;				//关闭中断
            assert(!interruption_enabled());		//此时中断不可用
            mutex::scoped_lock lock(io_mu);			//锁定io流操作
            cout << str << ++x << endl;
            cout << this_thread::interruption_requested() << endl;
            this_thread::interruption_point();		//中断点被禁用

            restore_interruption ri(di);			//临时恢复中断
            assert(interruption_enabled());			//此时中断可用
            cout << "can interrupted" << endl;
            cout << this_thread::interruption_requested() << endl;
            this_thread::interruption_point();		//可被中断
		}											//离开作用域,di/ri都被析构
                                                    //恢复线程最初的可中断状态
        assert(interruption_enabled());				//此时允许中断
    }
    catch(thread_interrupted&)
    {...}
}

main()函数的运行结果如下:

hello1
1
can interrupted
1
thread_interrupted

运行结果中的两行"1"是函数this_thread::interruption_requested()的输出结果,它表明线程已经收到了中断请求,但因为第一次线程不允许中断,故线程继续执行,直到restore_interruption对象临时恢复了可中断的时候线程再被中断。

1.9 线程组

thread库提供类thread_group用于管理一组线程,就像是一个线程池,它内部使用std::list<thread*>来容纳创建的thread对象,类摘要如下:

class thread_group:private noncopyable
{
public:
    template<typename F>
    thread* create_thread(F threadfunc);
    void add_thread(thread* thrd);
    void remove_thread(thread* thrd);
    void join_all();
    void interrupt_all();
    int size() const;
};

thread_group的接口很小,用法也很简单。

成员函数create_thread()是一个工厂函数,可以创建thread对象并运行线程,同时加入内部的list。我们也可以在thread_group外部创建线程对象,使用add_thread()加入到线程组。

如果不需要某个线程,remove_thread(),可以删除list里的thread对象。

join_all()interrupt_all()用来对list里的所有线程对象操作,等待或者中断这些线程。

例如:

thread_group tg;
tg.create_thread(bind(printing,ref(x),"C++"));
tg.create_thread(bind(printing,ref(x),"boost"));
tg.join_all();

使用thread_group,我们可以为程序建立一个类似于全局线程池的对象,统一管理程序中使用的thread,它可以使用单件库编程一个单件,以提供一个全局的访问点,例如:

typedef singleton_default<thread_group> thread_pool;
thread_pool::instance().create_thread(...);

不过Boost的单件库不提供完全的线程安全,如果你要在多线程中操作这个单件线程组需要小心,或者自己封装一个线程安全的单件对象。

1.10 条件变量

条件变量是thread库提供的另一种用于等待的同步机制,可以实现线程间的通信,它必须与互斥量配合使用,等待另一个线程中某个事件的发生(满足某个条件),然后线程才能继续执行。

thread库提供两种条件变量对象condition_variablecondition_variable_any,一般情况下我们应该使用condition_variable_any,它能够适应更广泛的互斥量类型。

condition_variable_any的类摘要如下:

class condition_variable_any
{
public:
    void notify_one();
    void notify_all();
    
    template<typename lock_type>
    void wait(lock_type& lock);
    
    template<typename lock_type,typename predicate_type>
    void wait(lock_type& lock,predicate_type predicate);
    
    template<typename lock_type,typename duration_type>
    bool timed_wait(lock_type& lock,duration_type const& rel_time);
};

条件变量的使用方法很简单:

拥有条件变量的线程先锁定互斥量,然后循环检查某个条件,如果条件不满足,那么就调用条件变量的成员函数wait()等待直至条件满足。其他线程处理条件变量要求的条件,当条件满足时调用它的成员函数notify_one()notify_all(),以通知一个或者所在等待条件变量的线程停止等待继续执行。

1.10.1 条件变量的用法

我们使用标准库的容器适配器stack来实现一个用于生产者-消费者模式的后进先出型缓冲区,以演示条件变量的用法。

缓冲区buffer使用了两个条件变量cond_putcond_get,分别用于处理put动作和get动作,如果缓冲区满则cond_put持续等待,当cond_put得到通知(缓冲区不满)时线程写入数据,然后通知cond_get条件变量可以取数据。cond_get的处理例程与cond_put类似。具体实现代码如下:

#include <stack>
class buffer
{
private:
    mutex mu;												//互斥量,配合条件变量使用
    condition_variable_any cond_put;						//写入条件变量
    condition_variable_any cond_get;						//读取条件变量
    stack<int> stk;											//缓冲区对象
    int un_read,capacity;
    bool is_full()											//缓冲区满判断
    {return un_read == capacity;}
    bool is_empty()											//缓冲区空判断
    {return un_read == 0;}
public:    
	buffer(size_t):un_read(0),capacity(n){}					//构造函数
    void put(int x)											//写入数据
    {
        {													//开始一个局部域
            mutex::scoped_lock lock(mu);					//锁定互斥量
            while(is_full())								//检查缓冲区是否满
            {
                {											//局部域,锁定cout输出一条信息
                    mutex::scoped_lock lock(io_mu);
                    cout << "full waiting..." << endl;
                }
                cond_put.wait(mu);							//条件变量等待
            }												//条件满足,停止等待
        	stk.push(x);									//压栈,写入数据
            ++un_read;
        }									//解锁互斥量,条件变量的通知不需要互斥量锁定
        cond_get.notify_one();								//通知可以读取数据
    }
    void get(int *x)										//读取数据
    {
        {													//局部域开始
            mutex::scoped_lock lock(mu);					//锁定互斥量
            while(is_empty())								//检查缓冲区是否空
            {
                {											//向cout输出信息
                    mutex::scoped_lock lock(io_mu);
                    cout << "empty waiting..." << endl;
                }
                cond_get.wait(mu);							//条件变量等待
            }												//条件满足,停止等待
            --un_read;
            *x = stk.top();									//读取数据
            stk.pop();										//弹栈
        }
        cond_put.notify_one();								//通知可以写入数据
    }
};

buffer buf(5);												//一个缓冲区对象
void producer(int n)										//生产者
{
    for(int i = 0;i < n;++i)
    {
        {													//输出信息
            mutex::scoped_lock lock(io_mu);
            cout << "put:" << i << endl;
        }
        buf.put(i);											//写入数据
    }
}
void consumer(int n)										//消费者
{
    int x;
    for(int i = 0;i < n;i++)
    {
        buf.get(&x);
        mutex::scoped_lock lock(io_mu);
        cout << "get " << x << endl;
    }
}
int main()
{
    thread t1(producer,20);									//一个生产者线程
    thread t2(consumer,10);									//两个消费者线程
    thread t3(consumer,10);
    
    t1.join();												//等待t1线程结束
    t2.join();												//等待t2线程结束
    t3.join();												//等待t3线程结束
}

程序运行后交替出现putget以及waiting信息,显示在多线程环境下生产者和消费者都正确地执行了预定的功能。

也可以使用标准库的容器适配器queuecircular_buffer类,来实现先进先出型和循环队列型缓冲区。

1.10.2 其他用法

条件变量的wait()函数有一个有用的重载形式:wait(lock_type& lock,predicate_type predicate),它比普通的形式多接受一个谓词函数(或函数对象),当谓词predicate不满足时间持续等待,相当于:

while(!predicate())
    wait(lock);

使用这个重载形式可以写出更简洁清晰的代码,通常需要配合bind来简化谓词函数的编写。例如,buffer类的两个条件变量的等待可以改写成:

cond_put.wait(mu,!bind(&buffer::is_full,this));
cond_get.wait(mu,!bind(&buffer::is_empty,this));

在这里我们不得不对bind表达式使用逻辑非操作符(叹号),因为wait()的语义与定义的is_full()is_empty()正好相反。如果要使用更自然的语义则需要重新定义条件判断函数,改为使用not_full()not_empty()

thread库还提供一个typedef:condition,它是condition_variable_any的同义词,用于对旧代码提供兼容。但它并不在头文件<boost/thread.hpp>中,如果非要使用这个typedef,需要包含头文件<boost/thread/condition.hpp>

1.11 共享互斥量

共享互斥量shared_mutex不同于mutexrecursive_mutex,它允许线程获取多个共享所有权和一个专享所有权,实现了读写锁的机制,即多个读线程一个写线程。

share_mutex具有mutex的全部功能,可以把它像mutex一样使用lock()unlock()来获得专享所有权,但它代价要比mutex高很多。如果要获得共享所有权需要使用lock_shared()try_lock_shared(),相应地要使用unlock_shared()来释放共享所有权。

shared_mutex没有提供内部的lock_guard类型定义,因此在使用shared_mutex时我们必须直接使用lock_guard对象,读锁定时使用shared_lock<shared_mutex>,写锁定时使用unique_lock<shared_mutex>

我们使用代码来示范shared_mutex的用法。首先定义一个读写数据类rw_data,它使用shared_mutex实现多个读者一个作者:

class rw_data
{
private:
    int m_x;									//用于读写的数据
    shared_mutex rw_mu;							//共享互斥量
public:
    rw_data():m_x(0){}							//构造函数
    void write()								//写数据
    {
        unique_lock<shared_mutex> ul(rw_mu);	//写锁定
        ++m_x;
    }
    void read(int *x)							//读数据
    {
        shared_lock<shared_mutex> sl(rw_mu);	//读锁定
        *x = m_x;
    }
};
void writer(rw_data &d)							//写线程
{
    for(int i = 0;i < 20; ++i)
    {
        this_thread::sleep(posix_time::millisec(10));		//睡眠
        d.write();
    }
}
void reader(re_data &d)							//读线程
{
    int x;
    for(int i = 0;i < 10; ++i)
    {
        this_thread::sleep(posix_time::millisec(5));		//睡眠
        d.read(&x);
        mutex::scoped_lock lock(io_mu);
        cout << "reader:" << x << endl;
    }
}

int main()
{
    rw_data d;
    thread_group pool;								//线程组
    
    pool.create_thread(bind(reader,ref(d)));		//读线程1
    pool.create_thread(bind(reader,ref(d)));		//读线程2
    pool.create_thread(bind(reader,ref(d)));		//读线程3
    pool.create_thread(bind(reader,ref(d)));		//读线程4
    pool.create_thread(bind(writer,ref(d)));		//写线程1
    pool.create_thread(bind(writer,ref(d)));		//写线程2
    
    pool.join_all();								//等待线程结束
}

运行程序可以看到多个读者同时获得数据。

1.12 future

很多情况下线程不仅仅要执行一些工作,它还可能要返回一些计算结果,一个简单的解决方法是被调线程操作一个全局变量,主调线程不断地检查是否有值或者阻塞等待,这种解法相当笨拙。

thread库使用future范式提供了一种异步操作线程返回值的方法,因为这个返回值在线程开始执行时还是不可用的,是一个"未来"的"期待值",所以被称为future(期货)。

future使用packaged_taskpromise两个模板类来包装异步调用,用unique_futureshared_future来获取异步调用结果(即future值)。接下来我们先用packaged_taskunique_future这两个最常用也是最易用的类来阐述future的用法。

1.12.1 packaged_task和unique_future

packaged_task好像是一个reference_wrapper或者function对象,它提供operator(),包装了一个可回调物,然后它就可以被任意的线程调用执行,最后的future值可以用成员函数get_future()获得。

unique_future用来存储packaged_task异步计算得到的future值,它只能持有结果的唯一的一个引用。成员函数wait()timed_wait()的行为类似thread.join(),可以阻塞等待packaged_task的执行,直至获得future值。成员函数is_ready()has_value()has_exception()分别用来测试unique_future是否可用,是否有值和是否发生了异常,如果一切正常,那么可以使用get()获得future值。

下面代码示范了future特性的用法,使用packaged_taskunique_future计算斐波拉契数列的值:

//递归计算斐波拉契数列
int fab(int n)
{
    if(n == 0 || n == 1)
    {return 1;}
    return fab(n-1)+fab(n-2);
}
int main()
{
    //声明 packaged_task 对象,用模板参数指明返回值类型
    //packaged_task只接受无参函数,因此需要使用bind
    packaged_task<int> pt(bind(fab,10));
    
    //声明 unique_future 对象,接受packaged_task的future值,
    //同样要用模板参数指明返回值类型
    unique_future<int> uf = pt.get_future();
    
    //启动线程计算,必须使用boost::move()来转移packaged_task对象
    //因为packaged_task是不可拷贝的
    thread(boost::move(pt));
    uf.wait();								//unique_future等待计算结果
    assert(uf.is_ready() && uf.has_value());
    cout << uf.get();						//输出计算结果69
}

1.12.2 使用多个futre对象

为了支持多个future对象的使用,future还提供wait_for_any()wait_for_all()两个自由函数,它们可以阻塞等待多个future对象,直到任意一个或者所有future对象都可用(is_ready())。这两个函数有多个重载形式,可以接受一对表示future容器区间的迭代器或者最多5个future对象。例如:

#include <boost/array.hpp>
#include <boost/foreach.hpp>
int main()
{
    typedef packaged_task<int> pti_t;			//类型定义
    typedef unique_future<int> ufi_t;			//类型定义
    
    array<pti_t, 5> ap;							//使用boost::array
    array<ufi_t, 5> au;
    for(int i = 0;i < 5; ++i)					//启动五个future调用
    {
        ap[i] = pti_t(bind(fab,i + 10));		//计算future
        au[i] = ap[i].get_future();				//获取future
        thread(move(ap[i]));					//启动线程开始计算
    }
    wait_for_all(au.begin(),au.end());			//等待所有计算结束
    BOOST_FOREACH(ufi_t& uf,au)					//foreach循环打印计算结果
    {
        cout << uf.get() << endl;				//输出89,144,233,377,610
    }
}

如果使用wait_for_any(),那么等待和输出的代码可以是:

wait_for_any(au[3],au[4],au[2]);				//等待任意一个future值
BOOST_FOREACH(ufi_t& uf, au)
{
    if(uf.is_ready() && uf.has_value())			//检测哪个future有值
    {cout << uf.get() << endl;}
}

1.12.3 promise

promise也用于处理异步调用返回值,但它不同于packaged_task,不能包装一个函数,而是包装一个值,这个值可以作为函数的输出参数,适用于从函数参数返回值的函数。

promise的用法与packaged_task类似,在线程中用set_value()设置要返回的值,用成员函数get_future()获得future值赋给future对象。

示范promise用法的代码如下:

void fab2(int n,promise<int> *p)					//使用promise作为输出参数
{p->set_value(fab(n));}
int main(int argc,char* argv[])
{
    promise<int> p;									//promise变量
    unique_future<int> uf = p.get_future();			//赋值future对象
    
    thread(fab2,10,&p);								//启动计算线程
    
    uf.wait();										//等待future计算结果
    cout << uf.get() << endl;
}

1.13 高级议题

1.13.1 使用thread库的部分功能

有的时候我们不需要使用thread库的全部功能,比如仅使用mutex提供互斥操作,把代码嵌入到其他多线程程序中。那么我们完全可以不用编译thread库,仅包含哪些需要功能的头文件即可,这样可以获得更好的可移植性。

thread库中不需要编译就可以使用的头文件如下:

#include <boost/thread/condition_variable.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/recursive_mutex.hpp>
#include <boost/thread/thread_time.hpp>					//需要编译date_time库
#include <boost/thread/shared_mutex.hpp>
#include <boost/thread/barrier.hpp>

如果仅仅想使用互斥量功能,那么还可以使用未文档化的超轻量级互斥量boost::detail::lightweight_mutex,它位于头文件<boost/detail/lightweight_mutex.hpp>lightweight_mutex同样是可移植的,而且具有内部类型定义scoped_lock提供互斥量的自动锁定与释放。

1.13.2 锁定函数

thread库在名字空间boost中提供了两个自由函数lock()try_lock(),可以一次锁定多个互斥量,而且不会出现死锁,它们的声明如下:

template<typename Lockable1,...>							//最多支持5个互斥量
void lock(Lockable1& l1,...);

template<typename ForwardIterator>							//使用迭代器遍历互斥量容器
void lock(ForwardIterator begin,ForwardIterator end);
//try_lock()的形式与lock()类似

lock()不具有lock_guard的退出作用域自动解锁的特性,因此在锁定互斥量后必须手工解锁互斥量(没有对应的unlock()函数),但lock()函数保证:锁定后发生异常时解除对互斥量的锁定。

lock()函数的用法如下:

lock(mu1,mu2);												//锁定两个互斥量
...;														//临界区操作
mu1.unlock();												//逐个解除锁定
mu2.unlock();

1.13.3 仅初始化一次

为了保证在多线程环境中某些仅要求被调用一次的函数被正确调用(通常是一些用于初始化的函数),thread库提供了仅初始化一次机制,使多个线程在操作初始化函数时只能有一个线程成功执行,不会发生错误。

这个机制首先需要使用一个once_flag对象,并把它初始化为值BOOST_ONCE_INIT,它将作为初始化的标志。然后再使用模板函数:call_once()来调用初始化函数,完成仅执行一次的初始化。

例如,假设我们有如下的全局变量和一个初始化函数:

static int g_count;					//静态全局变量,更好的方式是使用匿名名字空间
void init_count()
{
    cout << "should call once." << endl;
    g_count = 0;
}

然后我们在一个函数call_func()中调用call_once(),这个call_func()函数将被多线程执行,以验证初始化函数的执行情况。注意once_flag对象的赋值必须是线程安全的,在这里我们是在全局域初始化。

once_flag of = BOOST_ONCE_INIT;			//一次初始化标志
void call_func()
{
    call_once(of,init_count);			//执行一次初始化
}

在使用call_once()函数时我们必须预先声明once_flag对象,而不能使用临时变量,否则会引发编译错误,即不能写如下的代码:

call_once(once_flag(BOOST_ONCE_INIT),init_count);			//错误!

最后,我们在main()函数中启动多个线程来验证:仅初始化一次机制:

int main(int argc,char* argv[])
{
    (thread(call_func));						//我们必须用括号括住临时对象
    (thread(call_func));						//否则编译器会认为这是个空thread对象声明
    
    this_thread::sleep(posix_time::seconds(1));	//等待1秒钟
}

程序的运行结果正如我们所预料的那样,init_count()仅执行了一次,屏幕上将会输出:

should call once.

call_once()也可以用来执行成员函数,单件线程池的取实例函数:

once_flag of = BOOST_ONCE_INIT;
call_once(of,&thread_pool::instance);

1.13.4 barrier

barrier(护栏)是thread库基于条件变量提供的另一种同步机制,可以用于多个线程同步,当线程执行到barrier时必须等待,直到所有的线程都到达这个点时才能继续执行。barrier的另一个名字rendezvous(约会地点)更形象地描述了这种行为。

示范barrier用法的代码如下:

barrier br(5);								//定义一个5个线程的barrier
void printing(atom_int& x)
{
    {
        mutex::scoped_lock lock(io_mu);		//锁定cout输出流
        cout << "thread" << ++x << "arrived barrier." << endl;
    }
    br.wait();								//在barrier处等待,必须5个线程都到这里才能继续执行
    mutex::scoped_lock lock(io_mu);			//锁定cout流
    cout << "thread run." << endl;
}
int main(int argc,char* argv[])
{
    atom_int x;
    thread_group tg;								//一个线程组
    for(int i = 0;i < 5;++i)						//创建5个线程
    {
        tg.create_thread(bind(printing,ref(x)));	//使用bind
    }
    tg.join_all();									//等待线程结束
}

1.13.5 线程本地存储

有时候函数使用了局部静态变量或者全局静态变量,因此不能用于多线程环境,因为无法保证静态变量在多线程环境下重入时的正确操作。

thread库使用thread_specific_ptr实现了可移植的线程本地存储机制(thread local storage,或者是 thread specific storage(线程专有存储),简称 tss),使这样的变量用起来就像是每个线程独立拥有,可以简化多线程应用,提高性能。

thread_specific_ptr是一种智能指针,因此它的接口与shared_ptr很相似,它重载了operator*operator->,可以用get()获得真实指针,也有reset()release()函数。

thread_specific_ptr的值初始时通常是空指针(NULL),因此需要使用get()来检测。很遗憾,thread_specific_ptr没有定义隐式的bool转换,不能直接在bool语境中检查是否为空。

示范thread_specific_ptr用法的代码如下:

void printing()
{
    thread_specific_ptr<int> pi;					//线程本地存储一个整数
    pi.reset(new int());							//直接用reset()函数赋值
    
    ++(*pi);										//递增
    mutex::scoped_lock lock(io_mu);					//锁定io流操作
    cout << "thread v=" << *pi << endl;
}
int main(int argc,char* argv[])
{
    (thread(printing));								//启动两个线程
    (thread(printing));
    this_thread::sleep(posix_time::seconds(1));
}

程序的运行结果:

thread v=1
thread v=1

很明显,thread_specific_ptr使每个线程都拥有了一份自己的数据拷贝,它使得编写可重入的函数的工作大大简化了。

1.13.6 线程结束时执行操作

this_thread名字空间提供了一个at_thread_exit(func)函数,它允许"登记"一个线程在结束的时候,执行可调用物func,无论线程是否被中断。例如:

void end_msg(const string& msg)							//定义线程结束时被调用的函数
{cout << msg << endl;}
void printing(atom_int& x,const string& str)
{
    ...
    at_thread_exit(bind(end_msg,"end"));				//使用bind得到函数对象
}

但如果线程被特定的操作系统API强行中止,或者程序调用标准C函数exit()(例如main()函数正常结束)、abort(),那么线程也会强制结束,at_thread_exit()登记的函数不会被调用。

1.13.7 lazy future

promisepackaged_task都支持回调函数,可以让future延后在需要的时候获得值,而不必主动启动线程计算。promisepackaged_task使用成员函数set_wait_callback()设置回调函数,当future对象使用get_futrue()函数时启动线程调用回调函数,计算出future值。

示范lazy future用法的代码如下:

void lazy_call_back(packaged_task<int>& task)				//回调函数
{
    try
    {
        task();												//启动task
    }
    catch(task_already_started&)							//如果task已经启动会抛出异常
    {}
}
int main()
{
    packaged_task<int> task(bind(fab,10));					//task
    task.set_wait_callback(lazy_call_back);					//设置回调函数
    unique_future<int> uf = task.get_future();				//future 值
    
    cout << uf.get();										//获取future值,调用回调函数计算
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值