多线程程序设计(三)——Guarded Suspension
本文摘要了《Java多线程设计模式》一书中提及的 Guarded Suspension 模式的适用场景,并针对书中例子(若干名称有微调)给出一份 C++ 参考实现及其 UML 逻辑图,也列出与之相关的模式。
◆ 适用场景
当线程访问的共享数据没有准备好时,让该线程进入等待状态,直到数据被准备好后由其他线程唤醒等待中的线程。
◆ 解决方案
使用 std::condition_variable 对象判断共享数据是否准备好。为了获取数据而等待的线程在等待期间必须解锁互斥元,并在被唤醒后重新锁定互斥元。通过 std::unique_lock 与 std::mutex 的组合来提供此类灵活的操作。
◆ 参考实现
例子模拟了一个客户端线程不断地向请求队列中发送请求,而一个服务器线程不断从请求队列中获取请求的例子。作为共享数据的请求队列(Request_Queue)是能存放和获取请求(Request)的类。如果队列当前没有请求,服务器线程会等待,直到客户端线程发送请求。
class Request
{
...
string
to_string() const
{
...
}
};
Request 是用来表示请求的简单类。
class Client
{
...
Request_Queue &
__queue__;
...
void
run() #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); #2
std::this_thread::sleep_for(milliseconds(std::rand()%1000));
}
}
};
class Server
{
...
Request_Queue &
__queue__;
...
void
run() #3
{
for (int i = 0; i < 10000; ++i) {
shared_ptr<Request> req = __queue__.get_request(); #4
std::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;
}
void
put_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 和译者周全等。
受限于作者的水平,读者如发现有任何错误或有疑问之处,请追加评论或发邮件联系 green-pi@qq.com。作者将在收到意见后的第一时间里予以回复。 本文来自博客园,作者:green-cnblogs,转载请注明原文链接:https://www.cnblogs.com/green-cnblogs/p/18764824 谢谢!

浙公网安备 33010602011771号