多线程调用的封装技巧

很多时候, 我们想把一项操作放入后台线程去执行, 可能是为了提高操作体验(UI表现的流畅), 或者是性能(充分利用多核的计算能力)等

为了方便, 我在这里先定义一个简化的线程模型:

  • 所有的操作都定义为命令(Command)
  • 后台线程监听一个命令队列, 如果有命令就执行, 没有就等待
  • 如果收到结束通知, 则结束该线程

比如我们有两种操作:

void PrintA()
{
    printf("thread[%x]: aaa\n", this_thread::get_id().hash());
}
void PrintB()
{
    printf("thread[%x]: bbb\n", this_thread::get_id().hash());
}

这两种操作会在后台线程去执行, 用代码表示如下:

enum CommandType
{
    CommandA,
    CommandB
};

atomic_int8_t IsOver = 0;
concurrent_queue<CommandType> CommandQueue;

void SendCommand(CommandType cmd)
{
    CommandQueue.push(cmd);
}

atomic_int8_t IsOver = 0;
concurrent_queue<CommandType> CommandQueue;

void CommandThreadProc()
{
    printf("thread[%x]: start\n", this_thread::get_id().hash());

    chrono::seconds time(1);
    CommandType command;
    while (IsOver == 0)
    {
        if (CommandQueue.try_pop(command))
        {
            switch (command)
            {
            case CommandA:
                PrintA();
                break;
            case CommandB:
                PrintB();
                break;
            }
        }
        this_thread::sleep_for(time);
    }

    printf("thread[%x]: end\n", this_thread::get_id().hash());
}

运行:

int _tmain(int argc, _TCHAR* argv[])
{
    printf("thread[%x]: start\n", this_thread::get_id().hash());

    thread t(CommandThreadProc);

    SendCommand(CommandA);
    SendCommand(CommandB);

    this_thread::sleep_for(chrono::seconds(10));
    IsOver = true;
    t.join();
    printf("thread[%x]: end\n", this_thread::get_id().hash());
    system("pause");
    return 0;
}

Nebula3中使用的就是类似这样的模型, 把各种参数封装成Command, 发到后台线程去执行, 然后写一堆的swith-case去判断是什么命令, 再执行相应的操作 这种方式的好处就是简单, 而且也把操作细节隐藏在内部线程里了, 不过从编码的角度来看, 相当烦琐

改进一下, 把操作定义在外部, 然后把Command进行抽象, 这样可以免掉很多的条件判断和重复性编码:

struct Command
{
    virtual void DoCommand() = 0;
};

atomic_int8_t IsOver = 0;
concurrent_queue<Command*> CommandQueue;

void SendCommand(Command* cmd)
{
    CommandQueue.push(cmd);
}

void CommandThreadProc()
{
    printf("thread[%x]: start\n", this_thread::get_id().hash());

    chrono::seconds time(1);
    Command* command = nullptr;
    while (IsOver == 0)
    {
        if (CommandQueue.try_pop(command))
        {
           command->DoCommand();
           delete command;
           command = nullptr;
        }
        this_thread::sleep_for(time);
    }

    printf("thread[%x]: end\n", this_thread::get_id().hash());
}

这样定义后只需要派生抽象Command就好, 增加新的操作后台线程的代码无需变动:

struct CommandA : public Command
{
    virtual void DoCommand() override
    {
        PrintA();
        printf("\t%s\n", __FUNCTION__);
    }
};

struct CommandB : public Command
{
    virtual void DoCommand() override
    {
        PrintB();
        printf("\t%s\n", __FUNCTION__);
    }
};

/****************main****************/
    SendCommand(new CommandA());
    SendCommand(new CommandB());
/************************************/

但是这样还要是重复去定义很多个Command的子类. 在学习WPF时, 发现他们可以直接使用Dispatcher.BeginInvoke把某个函数发到后台去执行, 后来想了想, 其实就是把函数封装成对象发过去了. C++借助成员函数指针什么的也可以实现类似的机制:

struct FunctionCommand : public Command
{
    function<void()> fun;
    FunctionCommand(function<void()> f) : fun(f) {}

    virtual void DoCommand() override
    {
        fun();
        printf("\t%s\n", __FUNCTION__);
    }
};

/****************main****************/
    SendCommand(new FunctionCommand(PrintA));
    SendCommand(new FunctionCommand(PrintB));
/************************************/

这个方案看起来已经挺完美了, 但是, 还是不够灵活, 因为如果是在现有代码上重构, 一样需要封装很多函数出来. 在阅读Unreal代码时发现, 里面用了几个很巧妙的宏, 可以把代码片段封装成对象, 这样就免去了定义函数的代码量. 简化一下代码就是这样:

#define SEND_COMMAND(TypeName, Code) \
    { \
        struct TypeName##Command : public Command \
        { \
            virtual void DoCommand() override \
            { \
                Code; \
                printf("\t%s\n", __FUNCTION__); \
            } \
        }; \
        SendCommand(new TypeName##Command()); \
    }

/****************main****************/
    SEND_COMMAND(Print,
    {
        PrintA();
    });
    SEND_COMMAND(Print,
    {
        PrintB();
    });
    SEND_COMMAND(Print,
    {
        PrintB();
        PrintA();
    });
/************************************/

可以看到宏参数可以是多行的, 所以一段代码可以当成宏的一个参数传入, 然后封装成对象, 真是让人想不到的办法!

完整的main函数和执行结果如下:

int _tmain(int argc, _TCHAR* argv[])
{
    printf("thread[%x]: start\n", this_thread::get_id().hash());

    thread t(CommandThreadProc);

    SendCommand(new CommandA());
    SendCommand(new CommandB());

    SendCommand(new FunctionCommand(PrintA));
    SendCommand(new FunctionCommand(PrintB));

    SEND_COMMAND(Print,
    {
        PrintA();
    });
    SEND_COMMAND(Print,
    {
        PrintB();
    });
    SEND_COMMAND(Print,
    {
        PrintB();
        PrintA();
    });

    this_thread::sleep_for(chrono::seconds(10));
    IsOver = true;
    t.join();
    printf("thread[%x]: end\n", this_thread::get_id().hash());
    system("pause");
	return 0;
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏原创

教你如何用AST语法树对代码“动手脚”

作为程序猿,每天都在写代码,但是有没有想过通过代码对写好的代码”动点手脚”呢?今天就与大家分享——如何通过用AST语法树改写Java代码。 先抛一个问题:如何将...

67760
来自专栏大愚Talk

Redis的数据类型——探究竟

接上篇 为什么要用Redis,今天来聊聊具体的Redis数据类型与命令。本篇是深入理解Redis的一个重要基础,请坐稳,前方 长文预警。

11510
来自专栏walterlv - 吕毅的博客

.NET/C# 使用反射调用含 ref 或 out 参数的方法

2018-09-02 06:59

22310
来自专栏逆向技术

异常处理第一讲(SEH),筛选器异常,以及__asm的扩展,寄存器注入简介

异常处理第一讲(SEH),筛选器异常,以及__asm的扩展 一丶__Asm的扩展知识 ①丶使用关键字,解决局部变量申请问题 昨天已经介绍了__asm的基本用法,...

323100
来自专栏网络

编码在网络安全中的应用和原理

前言:现在的网站架构复杂,大多都有多个应用互相配合,不同应用之间往往需要数据交互,应用之间的编码不统一,编码自身的特性等都很有可能会被利用来绕过或配合一些策略,...

32360
来自专栏小灰灰

基于ForkJoin构建一个简单易用的并发组件

基于ForkJoin构建一个简单易用的并发组件 在实际的业务开发中,需要用到并发编程的知识,实际使用线程池来异步执行任务的场景并不是特别多,而且一般真的遇到了需...

42680
来自专栏JAVA后端开发

activiti多实例节点的任意跳转

activiti是原来不支持节点跳转的,他要求有线才能走,但实际业务中,需要支持动态跳转到各个节点。 一开始,这里的做法是动态构造一条虚拟线的,相关代码如下:

62550
来自专栏C/C++基础

CVTE2016春季实习校招技术一面回忆(C++后台开发岗)

2016.3.15,参加了CVTE的技术面,很不幸,我和我的两位小伙伴均跪在了一面。先将当日的面试内容汇总如下,供后来者参考。我们三人各自也都总结了失败的原因,...

7910
来自专栏木宛城主

Unity应用架构设计(10)——绕不开的协程和多线程(Part 1)

在进入本章主题之前,我们必须要了解客户端应用程序都是单线程模型,即只有一个主线程(Main Thread),或者叫做UI线程,即所有的UI控件的创建和操作都是...

41060
来自专栏PhpZendo

PHP 多任务协程处理

上周 有幸和同事一起在 SilverStripe 分享最近的工作事宜。今天我计划分享 PHP 异步编程,不过由于上周我聊过 ReactPHP;我决定讨论一些不一...

18710

扫码关注云+社区

领取腾讯云代金券