4.4.1 使用期望的函数化编程
术语函数化编程(functional programming)引用于一种编程方式,这种方式中的函数结果只依赖于传入函数的参数,并不依赖外部状态。
函数化编程的好处,并不限于那些将纯粹作为默认方式的语言。C++是一个多范型的语言。在C++11中这种方式要比C++98简单许多,因为C++11支持lambda表达式,还加入了boost和TR1中的std::bind,以及自动可以自行推断类型的自动变量。期望作为拼图的最后一块,它使得函数化编程模式并发化在C++中成为可能;一个期望在对象可以在线程间互相传递,并允许计算其中一个结果依赖于另外一个的结果,而非对共享数据的显示访问。
清单4.12快速排序——顺序实现版
template<typename T>
std::list<T> sequential_quick_sort(std::list<T> input)
{
if(input.empty())
return input;
std::list<T> result;
result.splice(result.begin(),input,input.begin()); //1
T const& pivot = *result.begin(); //2
auto divide_point = std::partition(input.begin(),input.end(), //3
[&](T const& t){return t < pivot});
std::list<T> lower_part;
lower_part.splice(lower_part.begin(),input,input.begin(),divide_point);//4
auto new_lower(sequential_quick_sort(std::move(lower_part))); //5
auto new_higher(sequential_quick_sort(std::move(input))); //6
result.splice(result.end(),new_higher); //7
result.splice(result.begin(),new_lower); //8
return result;
因为使用函数化模式,所以使用期望很容易转换为并行的版本,如下列清单所示:
4.13快速排序——“期望”并行版
template<typename T>
std::list<T> sequential_quick_sort(std::list<T> input)
{
if(input.empty())
return input;
std::list<T> result;
result.splice(result.begin(),input,input.begin());
T const& pivot = *result.begin();
auto divide_point = std::partition(input.begin(),input.end(),
[&](T const& t){return t < pivot});
std::list<T> lower_part;
lower_part.splice(lower_part.begin(),input,input.begin(),divide_point);
std::future<std::list<T> > new_lower(
std::async(&partial_sort_copy<T>,std::move(lower_part))); //1
auto new_higher(partial_sort_copy(std::move(input))); //2
result.splice(result.end(),new_higher); //3
result.splice(result.begin(),new_lower); //4
return result;
}
你也可以写一个spawn_task()函数对std::packaged_task和std::thread做简单的包装;你需要为函数创建std::packaged_task对象,可以从这个对象获取期望;或在线程中执行它,返回期望。虽然本身并不提供太多的好处,但是它会为转换成一个更复杂的实现铺平道路,将会实现向一个队列添加任务,而后使用线程池的方式运行它们:
template<typename F,typename A>
std::future<std::result_of<F(A&&)>::type>
spawn_task(F&& f,A&& a)
{
typedef std::result_of<F(A&&)>::type result_type; //推导F(A&&)返回的类型
std::packaged_task<result_type(A&&)> task(std::move(f));//包装到packaged_task中
std::future<result_type> resk(task.get_future()); //获取future状态
std::thread t(std::move(resk),std::move(f)); //传入单独的线程
t.detach();
return res;
}
4.4.2 使用消息传递的同步操作
每个线程都有一个状态机,当线程收到信息,它将会以某种方式更新状态,并且可能像其他线程发出一条或者多条信息,对于消息的处理依赖于线程的初始化状态。
如下是ATM实现的一种方式(状态机方式):线程的每一个状态都会等待一条可接受的信息,这条信息包含需要处理的内容。这可能让线程过度到一种新的状态,并且循环继续。
如下图展示:有状态参与的简单实现。在这个简化实现中,系统等待一张卡插入。当有卡插入时,系统将会等待输入它的PIN(密码之类),每次输入一个数字,用户可以删除最后输入数字,当输入完成,PIN就需要验证。当验证有问题,你的程序就需要终止,你要为用户退出卡,并且继续等待其他卡片插入到机器中。当PIN通过,你的程序要等待用户取消交易或者选择取款。当用户取消交易,你的程序就可以结束了,并返回卡片。当用户选择取一定量的现金,你的程序需要吐出现金和返还卡前等待银行的确认,或显示余额不足,并返回卡片。
清单4.15 ATM的逻辑类的简单实现
struct card_inserted
{
std::string account;
};
class atm
{
messaging::receiver incoming;
messaging::sender bank;
messaging::sender interface_hardware;
void (atm::*state)();
std::string account;
std::string pin;
void waiting_for_card() //1
{
interface_hardware.send(display_enter_card()); //2
incoming.wait(). //3
handle<card_inserted>(
[&](card_inserted const& msg) //4
{
account = msg.account;
pin = "";
interface_hardware.send(display_enter_pin());
state = &atm::getting_pin;
}
);
}
void getting_pin();
public:
void run() //5
{
state = &atm::waiting_for_card; //6
try()
{
for(;;)
{
(this->*state)(); //7
}
}
catch(messaging::close_queue const&)
{
}
}
};
运行成员函数开始5,其将会初始化waiting_for_card 6状态,然后反复执行当前的状态成员函数7。waiting_for_card函数1很简单:它发送一条信息到接口上,让终端显示等待卡片信息2,之后就等待传入一条消息进行处理3。这里处理的消息类型只能是card_inserted类的,这里使用lambda函数4进行处理。handle函数调用时连接到wait函数上的,当收到的信息不匹配,消息自动被丢弃,并且线程继续等待,直到收到匹配的消息。
lambda函数自身,只是将用户的账号信息缓存到一个成员变量中区,并清除PIN信息,再发送一条消息到硬件接口,让显示的界面提示用户输入PIN,然后线程状态改为获取PIN。当消息处理结束,状态函数将返回,然后主循环会调用新的状态函数7。
清单4.16 简单ATM实现中的getting_pin状态函数
void atm::getting_pin()
{
incoming.wait()
.handle<digit_pressed>( //1
[&](digit_pressed const& msg)
{
unsigned const pin_length = 4;
pin += msg.digit;
if(pin.length() == pin_length)
{
bank.send(verify_pin(account,pin,incoming));
state = &atm::verify_pin;
}
}
)
.handle<clear_last_pressed>( //2
[&](clear_last_pressed const& msg)
{
if(!pin.empty())
{
pin.resize(pin.length()-1);
}
}
)
.handle<cancel_pressed>( //3
[&](cancel_pressed const& msg)
{
state = &atm::done_processing;
}
);
}
这次需要处理三种消息类型,所以wait函数后面接了三个handle函数调用1,2,3.每个handle都对应的消息类型作为模板参数,并且将消息传入一个lambda函数中。因为这里的调用都被连接在一起,wait的实现知道它是等待一条digit_pressed消息,或是一条clear_last_pressed消息,亦或是一条cancel_pressed消息,其他消息类型将会被丢弃。
在一个并发系统中,这种编程方式可以极大的简化任务的设计,因为每一个线程都完全被独立对待。因此在使用多线程分离关注点,需要你明确如何分配线程之间的任务。