多线程编程就是,在同一程序同一时间内允许执行不同函数的离散处理队列。 这使得一个长时间去进行某种特殊运算的函数在执行时不阻碍其他的函数变得十分重要。
一旦一个应用程序启动,它仅包含一个默认线程。 此线程执行 main()
函数。 在 main()
中被调用的函数则按这个线程的上下文顺序地执行。 这样的程序称为单线程程序。
反之,那些创建新的线程的程序就是多线程程序。 他们不仅可以在同一时间执行多个函数,而且这在如今多核盛行的时代显得尤为重要。 既然多核允许同时执行多个函数,这就使得对开发人员相应地使用这种处理能力提出了要求。 然而线程一直被用来当并发地执行多个函数,开发人员现在不得不仔细地构建应用来支持这种并发。 多线程编程知识也因此在多核系统时代变得越来越重要。
本章将介绍C++ Boost库 Boost.Thread,它可以开发独立于平台的多线程应用程序。在这个库最重要的一个类就是 boost::thread
,它是在 boost/thread.hpp
里定义的,用来创建一个新线程。
1、多线程管理
下面我们用示例来说明如何使用boost库的多线程:
#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();
}
一旦上述示例中的变量 t 被创建,在构造boost::thread对象时传入的thread()
函数就在其所在线程中立即执行。 同时在 main()
里也并发地继续向下执行。为了防止程序终止,这里调用了boost::thread的 join()
方法。 join()
方法是一个阻塞调用:它可以暂停当前线程,直到boost::thread的thread()函数运行结束。
需要特别说明的是,一个特定的线程可以通过boost::thread的变量访问,通过这个变量等待着它的使用 join()
方法终止。 但是,即使boost::thread的变量越界或者析构了,该线程也将继续执行,即 一个线程总是在一开始就绑定到一个类型为 boost::thread
的变量,但是一旦创建,就不在取决于它。 甚至还存在着一个叫 detach()
的方法,允许类型为 boost::thread
的变量从它对应的线程里分离。 当然了,像 join()
的方法之后也就不能被调用,因为这个变量不再是一个有效的线程。
在一个线程对象上调用 interrupt()
会中断相应的线程,,中断意味着一个类型为 boost::thread_interrupted
的异常将会被抛出,但只有在线程到达中断点时才会抛出,Boost.Thread定义了一系列的中断点,例如 sleep()
函数。
另外,boost::thread 提供了get_id()方法,它会返回一个当前线程的ID号。 boost::thread 类提供了一个静态方法 hardware_concurrency()
,它能够返回基于CPU数目或者CPU内核数目的刻在同时在物理机器上运行的线程数。
2、多线程同步
当使用多线程访问某共享资源时,必须用到线程的同步操作,而保证线程同步常用的工具就是互斥锁。下面简单介绍几种常用的互斥锁的使用。
Boost.Thread提供多个的互斥锁,boost::mutex
是其中最简单的一个,它通过它的lock()及unlock()成员函数进行加锁及解锁。而另一种boost::lock_guard锁使用起来更为简单,其实它就是对boost::mutex进行了一层封装,在其内部构造和析构函数分别自动调用 lock()
和 unlock(),所以,在使用时,只需要在需要同步的位置进行
boost::lock_guard<boost::mutex> mylock(mymutex);声明即可,其中mymutex为boost::mutex类型的变量。
还有一种比较重要的互斥锁是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);
//定义unique_lock变量
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::try_to_lock
类型的值,对应的构造函数就会调用 try_lock()
方法,如果不传,则调用lock()。 owns_lock()
可以检查是否可获得互斥体。最后,timed_lock()
试图获得在一定的时间(绝对时间)内获取互斥体。 和 try_lock()
一样,返回bool
类型的值意味着成功是否。
和boost::lock_guard 一样, boost::unique_lock
的析构函数也会相应地释放互斥量。也可以手动地用 unlock()
释放互斥量,先调用 release()
函数解除boost::unique_lock
和互mutex之间的关联。但此时,必须显式地调用 unlock()
方法来释放互斥量,因为 boost::unique_lock
的析构函数不再做这件事情。
以上讲的都是互斥锁也叫独占锁,实际上还有一种共享锁-boost::shared_lock ,即允许多个线程同时对临界区进行访问,一般应用在只对临界区进行读操作时使用,个人觉得是个鸡肋,所以不再进行详细介绍。
下面介绍一种现在还是比较流行的线程同步方法,进结合条件变量与互斥锁共同完成:
#include <boost/thread.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
boost::mutex mutex;
boost::condition_variable_any cond;
std::vector<int> random_numbers;
void fill()
{
std::srand(static_cast<unsigned int>(std::time(0)));
for (int i = 0; i < 3; ++i)
{
boost::unique_lock<boost::mutex> lock(mutex);
random_numbers.push_back(std::rand());
cond.notify_all();
cond.wait(mutex);
}
}
void print()
{
std::size_t next_size = 1;
for (int i = 0; i < 3; ++i)
{
boost::unique_lock<boost::mutex> lock(mutex);
while (random_numbers.size() != next_size)
cond.wait(mutex);
std::cout << random_numbers.back() << std::endl;
++next_size;
cond.notify_all();
}
}
int main()
{
boost::thread t1(fill);
boost::thread t2(print);
t1.join();
t2.join();
}
直接看这段代码可能会有些疑惑,下面我们来分析这段代码:首先要明白其中的 notify_all()
函数会唤醒那些正在通过调用wait()
等待此通知的线程,其次是cond.wait(mutex)语句会完成两个操作,一是
会释放相应的被参数传入的互斥量,即调用mutex的unlock()函数,二就是阻塞等待其他线程调用cond.notify_all();而print()中的while循环主要是保证fill()先对临界区进行操作
3、线程的私有数据
boost::thread_specific_ptr
允许为当前进程保存一个对象的地址,然后只允许当前进程获得这个地址。 然而,当一个线程已经成功保存这个地址,其他的线程就会可能就失败。
#include <boost/thread.hpp>
#include <iostream>
#include <cstdlib>
#include <ctime>
boost::mutex mutex;
int init_number_generator()
{
static boost::thread_specific_ptr<int> tls;
//判断当前tls是否已经分配内存
if (!tls.get())
{
//申请内存
tls.reset(new int(0));
}
//对其进行自加操作
(*tls)++;
return *tls;
}
void random_number_generator()
{
int i = init_number_generator();
boost::lock_guard<boost::mutex> lock(mutex);
std::cout << i << std::endl;
}
int main()
{
boost::thread t[3];
for (int i = 0; i < 3; ++i)
{
t[i] = boost::thread(random_number_generator);
Sleep(1000);
}
for (int i = 0; i < 3; ++i)
t[i].join();
system("pause");
}
使用boost::thread_specific_ptr的 reset()
方法,可以把它的地址保存到 tls 里面。通过 get()
函数检查是否已经保存了一个地址。分析程序可以看出,变量tls是static类型,函数执行完毕后不会被释放,所以理论结果是1,2,3,但实际输出确实1,1,1。由此可以看出,每个线程都有一个独立的tls变量