多线程程序设计(三)——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)。

以下类图展现了代码主要逻辑结构,

class

以下顺序图展现了线程并发中的交互。

sequence

◆ 验证测试

笔者在实验环境一中编译代码(-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 和译者周全等。

posted @ 2025-03-14 20:44  green-cnblogs  阅读(14)  评论(0)    收藏  举报