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

class

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

sequence

◆ 验证测试

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

posted @ 2025-04-19 09:58  green-cnblogs  阅读(35)  评论(0)    收藏  举报