文章目录
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_SOURCE
和BOOST_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_mutex
:multiple_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_mutex
和recursive_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_lock
和scoped_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
的构造函数中写传递给调用函数的参数很麻烦,尤其是在使用大量线程对象的时候。这时我们可以使用Boost
的bind
和function
库:
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_thread
的sleep()
函数不仅可以使用绝对的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_thread
的interruption_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_variable
和condition_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_put
和cond_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线程结束
}
程序运行后交替出现put
、get
以及waiting
信息,显示在多线程环境下生产者和消费者都正确地执行了预定的功能。
也可以使用标准库的容器适配器queue
或circular_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
不同于mutex
和recursive_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_task
和promise
两个模板类来包装异步调用,用unique_future
和shared_future
来获取异步调用结果(即future
值)。接下来我们先用packaged_task
和unique_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_task
和unique_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
promise
和packaged_task
都支持回调函数,可以让future
延后在需要的时候获得值,而不必主动启动线程计算。promise
和packaged_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值,调用回调函数计算
}