《C++ Concurrency in Action》笔记20 带有消息传递的多线程实现--ATM

来源:互联网 发布:淘宝如何设置关联旺旺 编辑:程序博客网 时间:2024/06/05 19:19

一个使用子线程执行任务的简单实现:

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;std::packaged_task<result_type(A&&)>task(std::move(f)));std::future<result_type> res(task.get_future());std::thread t(std::move(task), std::move(a));t.detach();return res;}

将任务打包发给新建的子线程,返回一个future。这里的spawn_task()函数限制了参数的个数。还有一种更加完美的实现,就是利用bind和模板的不定参数,将参数限制取消,可以适合任意个数和类型的参数。可以参考我的另一篇文章:

C++11 多线程之传递package_tast讨论

函数式编程不是避免共享数据的唯一手段,另一种手段是CSP(Communicating Sequential Processes)。此时,线程间从概念上来说是彼此独立的,没有共享数据,但是可以在彼此之间建立一个传递消息的通讯管道。

带有消息传递的同步操作

CSP的思路很简单:如果没有共享数据,那么所有线程就是完全独立的,纯粹基于如何响应它收到的消息。每个线程都成为了一个状态机:当它收到一个消息,它以某种方式更新状态,并可能会向其他线程发送消息。一种实现这种线程的方式是以实现有限状态机的形式,但这不是唯一的方式;状态机可以被隐式的实现在应用的结构内。在任何情况下这都是一种更好的方式,但是依靠具体的需求以及专业的编程团队。单独实现每个线程,独立的处理数据可能从共享数据并发中去除很多复杂性,因此将程序变得简单,减少bug产生的几率。

真正的顺序通讯处理完全不需要共享数据,所有的通讯都通过队列发送消息,但是因为C++多线程共享了一个地址空间,所以不太可能满足这个要求。这就需要定制一些规则:作为应用或库的开发者,我们有责任确保线程间不共享数据。当然,消息队列必须是共享的,但是细节可以被包装到库中。

想象一下你要实现一个ATM程序,代码需要处理取钱的用户和银行之间的交互,控制机器接收银行卡,显示适当的信息,处理按键,发出纸币,以及返回银行卡。

一种方式是,将所有代码放置在3个线程中:一个处理机器,一个处理ATM逻辑,一个负责与银行的通讯。这些线程完全可以通过队列通讯,而不需要共享数据。例如,当用户放入一张银行卡并按下一个按键时,处理机器的线程可以给处理逻辑的线程发送一条消息,然后逻辑线程可以发送一条消息给处理银行通讯的线程以确定有多少存款可以处理,等等。

一种处理ATM逻辑的模式是状态机模式。线程每时每刻都在等待消息,并处理它。可能返回一个经过转换的结果,然后周期进行下去。下图是书上原图,展示了一种简单的实现逻辑:


显然,一个真正的ATM逻辑要复杂的多,但是这个实现足以阐明我们要说的方法。

为了个ATM逻辑设计一个状态机,你可以实现一个类,定义一个成员函数指针代表不同状态。每个成员函数可以等待特定的消息集合,并处理他们,包括将一种状态转换为另一种状态。每种状态由一个结构体表示。

原书后面的附录贴出了这个ATM逻辑的完整代码,因为篇幅关系,这里不列出来了。

这个程序使用了一些至少我看起来不太常见的用法,咋一看去感觉思绪混乱,但是仔细研究就会发现个中细节实在令人叹为观止,值得好好学习。大概总结了以下几点,供以后需要的时候回头查看:

1.atm状态机中保存有1个自己创建的消息队列(messaging::receiver),以及2个消息队列句柄(messaging::sender),这两个消息队列句柄保存了指向另外两个线程的消息队列的指针:

messaging::receiver incoming;messaging::sender bank;messaging::sender interface_hardware;

incoming由atm类自己创建,它的消息由主线程的I/O负责填充;bank的内部指针指向负责银行状态机的对象(运行在另一个线程);interface_hardware的内部指针指向负责用户接口的对象(运行在自另一个线程)。bank、interface_hardware就是atm对象和另两个状态机对象的通讯纽带。

2.类模板TemplateDispatcher的实例对象由dispatcher::handle()创建,负责保存一个消息类型和处理这个消息的函数(或可调用对象),并且保存了上一个TemplateDispatcher对象的指针,这样多个TemplateDispatcher对象构成一个单向链表,这个链表的首元素是一个dispatcher对象,所有元素都共用一个消息队列。TemplateDispatcher对象在析构时会调用wait_and_dispatch函数:

void wait_and_dispatch(){for (;;){auto msg = q->wait_and_pop();if (dispatch(msg))break;}}bool dispatch(std::shared_ptr<message_base> const& msg){if (wrapped_message<Msg>* wrapper =dynamic_cast<wrapped_message<Msg>*>(msg.get())){f(wrapper->contents);return true;}else{return prev->dispatch(msg);}}

wait_and_dispatch()函数循环执行消息队列的wait_and_pop()函数,取出消息队列中的最前面的消息,如果发现这个消息是自己可以处理的消息类型就使用保存的函数(或可调用对象)去处理这个消息,并结束处理。如果发现并非是自己可以处理的消息类型,则将消息交给上一个TemplateDispatcher对象继续处理,循环往复,直到有TemplateDispatcher对象能够处理,或者最终返回到首元素dispatcher对象。dispatcher对象会检查队列中是否具有close_queue类型的消息,如果有就抛出一个close_queue异常,这个异常会被atm::run()函数(也就是atm所在线程的线程函数)捕获到导致结束线程。如果没有检测到close_queue消息,那么此时队列中已经没有任何消息,wait_and_dispatch函数会继续等待队列中的新消息,并再次做同样的处理。

注意dispatch()函数,内部是通过使用dynamic_cast()函数来判断当前消息是否是指定的类型。这里用于判断消息类型的依据仅仅是TemplateDispatcher类模板的一个类型参数,并没有使用我们常见的保存整数或者枚举值的方式,既简化了代码,又减少了内存占用,实现得非常巧妙。

3.所有状态机类都使用了类的成员指针作为状态,以便在循环中切换当前执行的函数,例如atm类中的定义:

void (atm::*state)();

state是一个指向atm类的成员函数的指针,这个成员函数没有返回值,也没有参数。atm类的线程函数是这样定义的:

void run(){state = &atm::waiting_for_card;try{for (;;){(this->*state)();}}catch (messaging::close_queue const&){}}

循环调用this->*state()函数,那么当前state指针所指向的函数就会被循环调用,如果想切换当前执行的函数只需将新的成员函数的指针赋值给state即可。

4.整个程序中除了为I/O做了mutex保护外,只有消息队列中使用了mutex和condition_variabale保护,而且是封装到消息队列类中的,因此代码看起来非常简洁。


阅读全文
0 0
原创粉丝点击