线程是在同一程序同一时间内允许执行不同函数的离散处理队列,这使得在一个长时间进行某种特殊运算的函数在执行时不阻碍其他的函数时变得十分重要。线程实际上允许同时执行两种函数,而这两者不必相互等待。
一旦一个应用程序启动,它仅包含一个默认线程。此线程执行main()函数。在main()中被调用的函数则按这个线程的上下文顺序地执行,这样的程序称为单线程程序。
反之,那些创建新的线程的程序就是多线程程序。他们不仅可以在同一时间执行多个函数,而且这在如今多核盛行的时代显得尤为重要。既然多核允许同时执行多个函数,这就使得对开发人员相应地使用这种处理能力提出了要求。然而线程一直被用来当并发地执行多个函数,开发人员现在不得不仔细地构建应用来支持这种并发。多线程编程知识也因此在多核系统时代变得越来越重要。
本章介绍的是C++ Boost库Boost.Thread,它可以开发独立于平台的多线程应用程序。
二、线程管理
这个库中最重要的一个类就是boost::thread,它在boost/thread.hpp里定义,用来创建一个新线程。下面的示例来说明如何运用它。
#include <boost/thread.hpp>
#include <iostream>
void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
}
void thread()
{
for (int i = 0; i < 5; ++i)
{
wait(1);
std::cout << i << std::endl;
}
}
int main()
{
boost::thread t(thread);
t.join();
}
新建线程里执行的那个函数的名称被传递到 boost::thread 的构造函数。一旦上述示例中的变量t被创建,该thread函数就在其所在线程中被立即执行,同时在main()里也并发地执行该thread。
示例中,为了防止程序终止,就需要对新建线程调用join方法。join方法是一个阻塞调用:它可以暂停当前线程,直到调用join的线程运行结束。这就使得main函数一直会等待到thread运行结束。
正如上面例子中看到的,一个特定的线程可以通过诸如t的变量访问,通过这个变量等待着它的使用join方法终止。 但是,即使t越界或者析构了,该线程也将继续执行。一个线程总是在一开始就绑定到一个类型为 boost::thread 的变量,但是一旦创建,就不在取决于它。 甚至还存在着一个叫detach的方法,允许类型为 boost::thread 的变量从它对应的线程里分离。当然,像 join的方法之后也就不能被调用,因为这个变量不再是一个有效的线程。
任何一个函数内可以做的事情也可以在一个线程内完成。所以,一个线程只不过是一个函数,除了它是同时执行的。在上述例子中,使用一个循环把5个数字写入标准输出流。为了减缓输出,每一个循环中调用wait函数让执行延迟了一秒。wait可以调用一个名为sleep的函数,这个函数也来自于 Boost.Thread,位于 boost::this_thread 命名空间内。
sleep()可以在预计的一段时间或一个特定的时间点后才让线程继续执行。通过传递一个类型为 boost::posix_time::seconds 的对象,在这个例子里我们指定了一段时间。 boost::posix_time::seconds 来自于 Boost.DateTime 库,它被 Boost.Thread 用来管理和处理时间的数据。
虽然前面的例子说明了如何等待一个不同的线程,但下面的例子演示了如何通过所谓的中断点让一个线程中断。
#include <boost/thread.hpp>
#include <iostream>
void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
}
void thread()
{
try
{
for (int i = 0; i < 5; ++i)
{
wait(1);
std::cout << i << std::endl;
}
}
catch (boost::thread_interrupted&)
{
}
}
int main()
{
boost::thread t(thread);
wait(3);
t.interrupt();
t.join();
}
在一个线程对象上调用 interrupt() 会中断相应的线程。 在这方面,中断意味着一个类型为 boost::thread_interrupted 的异常,它会在这个线程中抛出。 然后这只有在线程达到中断点时才会发生。
如果给定的线程不包含任何中断点,简单调用interrupt就不会起作用。 每当一个线程中断点,它就会检查interrupt是否被调用过。只有被调用过了, boost::thread_interrupted 异常才会相应地抛出。
Boost.Thread定义了一系列的中断点,例如sleep() 函数,由于sleep() 在这个例子里被调用了五次,该线程就检查了五次它是否应该被中断。然而sleep()之间的调用,却不能使线程中断。
一旦该程序被执行,它只会打印三个数字到标准输出流。这是由于在main里3秒后调用 interrupt()方法。 因此,相应的线程被中断,并抛出一个 boost::thread_interrupted 异常。这个异常在线程内也被正确地捕获,catch 处理是空的。由于thread()函数在处理程序后返回,线程也被终止。这反过来也将终止整个程序,因为 main() 等待该线程使用join终止该线程。
Boost.Thread定义包括上述 sleep()函数等十个中断。 有了这些中断点,线程可以很容易及时中断。然而,他们并不总是最佳的选择,因为中断点必须事前读入以检查 boost::thread_interrupted 异常。
为了提供一个对 Boost.Thread 里提供的多种函数的整体概述,下面的例子将会再介绍两个。
#include <boost/thread.hpp>
#include <iostream>
int main()
{
std::cout << boost::this_thread::get_id() << std::endl;
std::cout << boost::thread::hardware_concurrency() << std::endl;
}
使用 boost::this_thread命名空间,能提供独立的函数应用于当前线程,比如前面出现的sleep() 。另一个是 get_id():它会返回一个当前线程的ID号。它也是由 boost::thread 提供的。
boost::thread 类提供了一个静态方法 hardware_concurrency() ,它能够返回基于CPU数目或者CPU内核数目的刻在同时在物理机器上运行的线程数。在常用的双核机器上调用这个方法,返回值为2。 这样的话就可以确定在一个多核程序可以同时运行的理论最大线程数。
三、同步
虽然多线程的使用可以提高应用程序的性能,但也增加了复杂性。如果使用线程在同一时间执行几个函数,访问共享资源时必须相应地同步。一旦应用达到了一定规模,这涉及相当一些工作。本段介绍了Boost.Thread提供同步线程的类。
#include <boost/thread.hpp>
#include <iostream>
void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
}
boost::mutex mutex;
void thread()
{
for (int i = 0; i < 5; ++i)
{
wait(1);
mutex.lock();
std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl;
mutex.unlock();
}
}
int main()
{
boost::thread t1(thread);
boost::thread t2(thread);
t1.join();
t2.join();
}
多线程程序使用所谓的互斥对象来同步。Boost.Thread提供多个的互斥类,boost::mutex是最简单的一个,它的使用就像linux下的二进制互斥量。互斥的基本原则是当一个特定的线程拥有资源的时候防止其他线程夺取其所有权,一旦释放,其他的线程可以取得所有权。这将导致线程等待至另一个线程完成处理一些操作,从而相应地释放互斥对象的所有权。
上面的示例使用一个类型为 boost::mutex 的mutex全局互斥对象。thread()函数获取此对象的所有权才在 for 循环内使用 lock()方法写入到标准输出流的。一旦信息被写入,使用unlock()方法释放所有权。
main() 创建两个线程,同时执行thread ()函数。利用 for 循环,每个线程数到5,用一个迭代器写一条消息到标准输出流。然而,标准输出流是一个全局性的被所有线程共享的对象,该标准不提供任何保证 std::cout 可以安全地从多个线程访问。 因此,访问标准输出流必须同步:在任何时候,只有一个线程可以访问 std::cout。
由于两个线程试图在写入标准输出流前获得互斥体,实际上只能保证一次只有一个线程访问 std::cout。不管哪个线程成功调用 lock() 方法,其他所有线程必须等待,直到 unlock() 被调用。
获取和释放互斥体是一个典型的模式,是由Boost.Thread通过不同的数据类型支持。 例如,不直接地调用 lock() 和 unlock(),使用 boost::lock_guard 类也是可以的。
#include <boost/thread.hpp>
#include <iostream>
void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
}
boost::mutex mutex;
void thread()
{
for (int i = 0; i < 5; ++i)
{
wait(1);
boost::lock_guard<boost::mutex> lock(mutex);
std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl;
}
}
int main()
{
boost::thread t1(thread);
boost::thread t2(thread);
t1.join();
t2.join();
}
boost::lock_guard 在其内部构造和析构函数分别自动调用lock() 和 unlock() 。 访问共享资源是需要同步的,因为它显示地被两个方法调用。 boost::lock_guard 类是另一个出现在我之前第2个系列智能指针单元的RAII用语。
除了boost::mutex 和 boost::lock_guard 之外,Boost.Thread也提供其他的类支持各种同步。其中一个重要的就是 boost::unique_lock ,相比较 boost::lock_guard 而言,它提供许多有用的方法。
#include <boost/thread.hpp>
#include <iostream>
void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
}
boost::timed_mutex mutex;
void thread()
{
for (int i = 0; i < 5; ++i)
{
wait(1);
boost::unique_lock<boost::timed_mutex> lock(mutex, boost::try_to_lock);
if (!lock.owns_lock())
lock.timed_lock(boost::get_system_time() + boost::posix_time::seconds(1));
std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl;
boost::timed_mutex *m = lock.release();
m->unlock();
}
}
int main()
{
boost::thread t1(thread);
boost::thread t2(thread);
t1.join();
t2.join();
}
上面的例子用不同的方法来演示 boost::unique_lock 的功能。 当然了,这些功能的用法对给定的情景不一定适用;boost::lock_guard 在上个例子的用法还是挺合理的。 这个例子就是为了演示 boost::unique_lock 提供的功能。
boost::unique_lock 通过多个构造函数来提供不同的方式获得互斥体。这个期望获得互斥体的函数简单地调用了lock()方法,一直等到获得这个互斥体。所以它的行为跟 boost::lock_guard 的那个是一样的。
如果第二个参数传入一个 boost::try_to_lock 类型的值,对应的构造函数就会调用 try_lock方法。这个方法返回 bool 型的值:如果能够获得互斥体则返回true,否则返回 false。相比lock函数,try_lock会立即返回,而且在获得互斥体之前不会被阻塞。
上面的程序向boost::unique_lock 的构造函数的第二个参数传入boost::try_to_lock。然后通过 owns_lock() 可以检查是否可获得互斥体。如果不能, owns_lock() 返回false。这也用到 boost::unique_lock 提供的另外一个函数: timed_lock() 等待一定的时间以获得互斥体。 给定的程序等待长达1秒,应较足够的时间来获取更多的互斥。
其实这个例子显示了三个方法获取一个互斥体:lock() 会一直等待,直到获得一个互斥体。try_lock()则不会等待,但如果它只会在互斥体可用的时候才能获得,否则返回 false。最后,timed_lock()试图获得在一定的时间内获取互斥体。和try_lock()一样,返回bool 类型的值意味着成功是否。