多线程程序设计(八)——Worker Thread
本文摘要了《Java多线程设计模式》一书中提及的 Worker Thread 模式的适用场景,并针对书中例子(若干名称有微调)给出一份 C++ 参考实现及其 UML 逻辑图,也列出与之相关的模式。
◆ 适用场景
控制执行任务的线程的数量,进而提高计算资源的利用率。
◆ 解决方案
用数组或集合存放一定数量的线程,以控制线程总量。让其中的线程去处理被委托的任务。
◆ 参考实现
例子模拟了 3 个客户端(Client)线程不断地向任务通道(Channel)中放入工作任务请求(Request);后台的 5 个工人(Worker)线程会分别从 Channel 中抓取一件任务来处理,如果没有任务要处理,则工人就会等待。
class Request
{
    ...
    void execute()
    {
        string msg;
        ...
        std::printf("%s", msg.c_str());
        std::this_thread::sleep_for(milliseconds(std::rand() % 1000));
    }
};
调用 Request::execute() 用以模拟一个工作任务,最后会让当前线程等待 0~1000 毫秒。
class Channel
{
    ...
    vector<thread>
    __threadpool__;
    vector<Request>
    __requestqueue__;                         #1
    int
    __head__, __tail__, __count__;
    ...
    void
    put_request(Request req)
    {
        unique_lock<mutex> lk(__mtx__);
        __cv__.wait(lk,                                             #2
                    [this] {
                        bool full = __count__ >= __MAX_REQUEST_NO__;
                        if (full)
                            std::printf("Request queue is full. Waiting...\n");
                        return !full;
                    });
        __requestqueue__[__tail__] = req;                       #3
        __tail__ = (__tail__ + 1) % __MAX_REQUEST_NO__;
        ++__count__;
        __cv__.notify_all();
    }
    Request
    take_request()
    {
        unique_lock<mutex> lk(__mtx__);
        __cv__.wait(lk,                                             #4
                    [this] {
                        bool empty = __count__ <= 0;
                        if (empty)
                            std::printf("No requests in queue. Waiting...\n");
                        return !empty;
                    });
        Request req = __requestqueue__[__head__];                   #5
        __head__ = (__head__ + 1) % __MAX_REQUEST_NO__;
        --__count__;
        __cv__.notify_all();
        return req;
    }
};
vector<thread> &
Channel::start_workers()            #6
{
    for (int i = 0; i < __threadpool__.size(); ++i) {
        string name("Worker-");
        name += std::to_string(i);
        __threadpool__[i] = thread(&Worker::run, Worker(name, *this));
    }
    return __threadpool__;
}
在 Channel 中用定长的 vector 模拟了任务缓冲区,并通过 __head__, __tail___ 控制任务的开始和结束位置,使其成为循环队列(#1)。某个 Client 线程向 Channel 中放置任务(put_request)时,如果任务缓冲区已满,则会进入等待状态(#2);当任务缓冲区有空了,一旦此线程被唤醒,就会将它的 Request 放入缓冲区中(#3)。某个 Worker 线程从 Channel 中获取任务(take_request)时,如果任务缓冲区已空,则会进入等待状态(#4);当任务缓冲区有任务了,一旦此线程被唤醒,就会将 Request 取出(#5),后续调用 Request::execute() 执行任务。Channel::start_workers() 会启动所有的 Worker 线程(#6)。
class Worker
{
    ...
    void run()
    {
        string msg;
        msg += __name__;
        msg += "'s id: ";
        msg += this_thread_id();
        std::printf("%s\n", msg.c_str());
        while (true) {
            Request req = __channel__.take_request();
            req.execute();
        }
    }
};
Worker::run() 是 Worker 线程的入口,它不断地从 Channel 获取 Request 并执行。
class Client
{
    ...
    void run()
    {
        for (int i = 1; true; ++i) {
            __channel__.put_request(Request(__name__, i));
            std::this_thread::sleep_for(milliseconds(std::rand() % 600));
        }
    }
};
Client::run() 是 Client 线程的入口,它不断地向 Channel 放置 Request,然后等待 0~600 毫秒。
int
main(int argc, char * argv[])
{
    ...
    Channel channel(5);
    vector<thread> & workers = channel.start_workers();
    Client alice("Alice", channel);
    Client bobby("Bobby", channel);
    Client chris("Chris", channel);
    thread ct1(&Client::run, &alice);
    thread ct2(&Client::run, &bobby);
    thread ct3(&Client::run, &chris);
    ...
    
}
创建任务通道时启动了 5 条工作线程,并启动 3 个 Client线程,模拟整个发出和执行 Request 的过程。

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

◆ 验证测试
笔者在实验环境一中编译代码(-std=c++11)成功后运行可执行文件,
$ g++ -std=c++11 -lpthread worker_thread.cpp
$ ./a.out
运行结果如下:
...
Worker-0's id: 1992123472
Worker-2's id: 1975338064
1975338064 executes [Request from Bobby No. 1 ]
1992123472 executes [Request from Alice No. 1 ]
Worker-1's id: 1983730768
1983730768 executes [Request from Chris No. 1 ]
Worker-4's id: 1958552656
No requests in queue. Waiting...
Worker-3's id: 1966945360
No requests in queue. Waiting...
No requests in queue. Waiting...
1958552656 executes [Request from Alice No. 2 ]
...
1992123472 executes [Request from Chris No. 73 ]
1966945360 executes [Request from Bobby No. 77 ]
Request queue is full. Waiting...
1975338064 executes [Request from Bobby No. 78 ]
1958552656 executes [Request from Alice No. 80 ]
1958552656 executes [Request from Alice No. 81 ]
1983730768 executes [Request from Alice No. 82 ]
Request queue is full. Waiting...
Request queue is full. Waiting...
1975338064 executes [Request from Chris No. 74 ]
...
可以看到由不同 Worker 线程在不停地执行来自不同 Client 的请求,有时也会出现缓冲区为空和缓冲区已满的情况。
◆ 相关模式
- 想要获取工人线程的处理结果时,可以使用 Future 模式。
 - 想要将代表请求的实例传递给工人线程时,可使用 Producer-Consumer 模式。
 
◆ 最后
完整的代码请参考 [gitee] cnblogs/18831093 。更多模式请参考多线程程序设计。
致《Java多线程设计模式》的作者结城浩。写作中也参考了《C++并发编程实战》中的若干建议,致作者 Anthony Williams 和译者周全等。
受限于作者的水平,读者如发现有任何错误或有疑问之处,请追加评论或发邮件联系 green-pi@qq.com。作者将在收到意见后的第一时间里予以回复。 本文来自博客园,作者:green-cnblogs,转载请注明原文链接:https://www.cnblogs.com/green-cnblogs/p/18831093 谢谢!
                    
                
                
            
        
浙公网安备 33010602011771号