0x00 摘要
在分析比特币源码时,最开始就简单学习了一些boost thread的简单用法和最基本的互斥锁,但看到后面发现还用到了更深层次的并发编程知识,于是又回来完整的学习一下,这里做个学习的总结。本文的内容主要包括线程组(Thread group)以及各种互斥锁(lock_guard
, unique_lock
)以及更高级一点的condition variable
。
0x01 Thread Group
Thread group
其实和thread没有太大的区别,就是把线程放到一起使用简单的操作进行批量处理,根据http://www.boost.org/doc/libs/1_53_0/boost/thread/detail/thread_group.hpp,总共有以下几个操作,
- thread* create_thread(F threadfunc); //创建新线程
- void add_thread(thread* thrd); //添加线程
- void remove_thread(thread* thrd); // 删除线程
- void join_all(); // 等待所有线程结束
- void interrupt_all(); // 中断所有线程
- size_t size() const; // 返回线程数量
0x02 Mutex
多线程访问共享变量时,如果不对共享变量做一定的限制,由于线程的随机性,可能就会造成无法预知的结果,而这其中最简单也是目前最常用的方式就是互斥锁,也就是每次只允许一个线程访问,如果已经有一个线程在访问共享变量其他线程再访问时则会进入阻塞状态,从而保证程序的确定性与一致性,一个简单的例子如下,
#include "boost/thread.hpp"
#include <iostream>
using namespace std;
boost::mutex mutex;
int id = 0;
void work(){
mutex.lock();
id++;
cout << << "id: " << id << endl;
mutex.unlock();
}
int main(){
boost::thread_group threads;
for(int i=0;i<10;i++){
threads.create_thread(&work);
}
threads.join_all();
return 0;
}
0x03 lock_guard
lock_guard
相比于mutex
而言使用方法更为简单一点,
#include "boost/thread.hpp"
#include <iostream>
using namespace std;
boost::mutex mutex;
int id = 0;
void work(){
{ // 可以额外添加个大括号,来限制lock_guard的作用域
boost::lock_guard<boost::mutex> lock(mutex);
id++;
cout << << "id: " << id << endl;
}
}
int main(){
boost::thread_group threads;
for(int i=0;i<10;i++){
threads.create_thread(&work);
}
threads.join_all();
return 0;
}
lock_guard
能够在变量创建时自动加锁,在离开作用域(被析构)时自动解锁,所以使用更加方便,也不用担心忘记解锁。
0x03 unique lock
unique_lock
在用法上和lock_guard
类似,在一般的情况下使用lock_guard
就够了,但是unique_lock
比lock_guard
多了许多其他的特性,比如说在创建变量过一段时间后再进行锁定,也可以在析构函数执行之前就进行解锁,同时还可以配合condition_variable
在特定条件下进行解锁。
0x04 condition variable
#include <iostream>
#include <string>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
boost::mutex mtx;
boost::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void Worker() {
boost::unique_lock<boost::mutex> lock(mtx);
// 等待主线程发送数据。
cv.wait(lock, [] { return ready; }); // 等价于 while(!ready){ cv.wait(lock); }
// 等待后,继续拥有锁。
std::cout << "工作线程正在处理数据..." << std::endl;
// 睡眠一秒以模拟数据处理。
boost::this_thread::sleep_for(boost::chrono::seconds(1));
data += " 已处理";
// 把数据发回主线程。
processed = true;
std::cout << "工作线程通知数据已经处理完毕。" << std::endl;
// 通知前,手动解锁以防正在等待的线程被唤醒后又立即被阻塞。
lock.unlock();
cv.notify_one();
}
int main() {
boost::thread worker(Worker);
// 把数据发送给工作线程。
{
boost::lock_guard<boost::mutex> lock(mtx);
std::cout << "主线程正在准备数据..." << std::endl;
// 睡眠一秒以模拟数据准备。
boost::this_thread::sleep_for(boost::chrono::seconds(1));
data = "样本数据";
ready = true;
std::cout << "主线程通知数据已经准备完毕。" << std::endl;
}
cv.notify_one();
// 等待工作线程处理数据。
{
boost::unique_lock<boost::mutex> lock(mtx);
cv.wait(lock, [] { return processed; });
}
std::cout << "回到主线程,数据 = " << data << std::endl;
worker.join();
return 0;
}
这段程序很好的描述了condition_variable
的功能,使用这个功能主要有以下三个步骤:
- 申明变量:一个
boost::condition_variable cv
和一个boost::unique_lock<boost::mutex> lock(mtx);
类型,注意必须是unique_lock
类型,不能是其他的类型。 - 阻塞当前线程: 有两种形式,一种是使用匿名函数
cv.wait(lock, []{return ready;});
另一种是直接使用循环while(!ready){cv.wait(lock);}
。 修改变量并唤醒线程:
ready = true; cv.notify_one();
也可以同时唤醒多个线程cv.notify_all()
。需要注意的是,在wait被唤醒之后进程依然拥有lock,只不过这时候就回归了
lock_guard
功能,离开作用域后就自动解锁。
0x05 scoped lock
scoped_lock
就是typedef unique_lock<mutex> scoped_lock;
所以实际上和unique_lock
具有相同的功能。