本文摘要了《Java多线程设计模式》一书中提及的 Guarded Suspension 模式的适用场景,并针对书中例子(若干名称有微调)给出一份 C++ 参考实现及其 UML 逻辑图,也列出与之相关的模式。
◆ 适用场景
当线程访问的共享数据没有准备好时,让该线程进入等待状态,直到数据被准备好后由其他线程唤醒等待中的线程。
◆ 解决方案
使用 std::condition_variable 对象判断共享数据是否准备好。为了获取数据而等待的线程在等待期间必须解锁互斥元,并在被唤醒后重新锁定互斥元。通过 std::unique_lock 与 std::mutex 的组合来提供此类灵活的操作。
◆ 参考实现
例子模拟了一个客户端线程不断地向请求队列中发送请求,而一个服务器线程不断从请求队列中获取请求的例子。作为共享数据的请求队列(Request_Queue)是能存放和获取请求(Request)的类。如果队列当前没有请求,服务器线程会等待,直到客户端线程发送请求。
class Request
{...stringto_string() const{...}};
Request 是用来表示请求的简单类。
class Client
{...Request_Queue &__queue__;...voidrun() #1{for (int i = 0; i < 10000; ++i) {shared_ptr<Request> req(new Request(("#" + std::to_string(i))));std::printf("%s requests:\t%s\n", __name__.c_str(), req->to_string().c_str());__queue__.put_request(req); #2std::this_thread::sleep_for(milliseconds(std::rand()%1000));}}};class Server
{...Request_Queue &__queue__;...voidrun() #3{for (int i = 0; i < 10000; ++i) {shared_ptr<Request> req = __queue__.get_request(); #4std::printf("%s handles:\t%s\n", __name__.c_str(), req->to_string().c_str());std::this_thread::sleep_for(milliseconds(std::rand()%333));}}};
Client::run() 作为客户端线程的初始函数(#1),向请求队列 Request_Queue 中存放 Request(#2)。Server::run() 作为服务器线程的初始函数(#3),从 Request_Queue 中获取 Request(#4)。
class Request_Queue
{...queue<shared_ptr<Request>>__requests__;mutex__mtx__;condition_variable__cv__;...shared_ptr<Request>get_request() #1{unique_lock<mutex> lk(__mtx__); #2__cv__.wait(lk, #3[this] {bool empty = __requests__.empty();if (empty)std::printf("No requests in queue. Waiting...\n");return !empty;});shared_ptr<Request> req = __requests__.front(); #4__requests__.pop();return req;}voidput_request(shared_ptr<Request> req) #5{lock_guard<mutex> lk(__mtx__);__requests__.push(req);__cv__.notify_all(); #6}};
Request_Queue 的 get_request 函数(#1)提供获取请求的接口。使用 unique_lock 锁定互斥元 mutex,紧接着就在 condition_variable 对象上等待用 lambda 函数判断是否有可用的 Request。如果有 Request(empty = false)则 Server 将从 wait() 中退出,然后取出第一个 Request(#4);如果没有 Request(empty = true), Server 将进入等待状态,并解锁互斥元。Request_Queue 的 put_request 函数(#5)提供存放请求的接口。使用 lock_guard 锁定互斥元,放入 Request,然后 Client 唤醒等待中的线程(#6)。
以下类图展现了代码主要逻辑结构,
以下顺序图展现了线程并发中的交互。
◆ 验证测试
笔者在实验环境一中编译代码(-std=c++11)成功后运行可执行文件,
$ g++ -std=c++11 -lpthread guarded_suspension.cpp
$ ./a.out
运行结果如下:
...
Alice requests: [ Request #2 ]
Bobby handles: [ Request #2 ]
No requests in queue. Waiting...
Alice requests: [ Request #3 ]
Bobby handles: [ Request #3 ]
Alice requests: [ Request #4 ]
Bobby handles: [ Request #4 ]
No requests in queue. Waiting...
Alice requests: [ Request #5 ]
Bobby handles: [ Request #5 ]
No requests in queue. Waiting...
Alice requests: [ Request #6 ]
Bobby handles: [ Request #6 ]
No requests in queue. Waiting...
...
可以看到客户端线程和服务器线程交替地发送或处理请求。
◆ 相关模式
- 对于如何保护共享数据的思路,要参考 Single Threaded Execution 模式。
- 如果共享数据没有准备好而要直接退出,就使用 Balking 模式。
◆ 最后
完整的代码请参考 [gitee] cnblogs/18764824 。
致《Java多线程设计模式》的作者结城浩。写作中也参考了《C++并发编程实战》中的若干建议,致作者 Anthony Williams 和译者周全等。