跨进程加载的 DLL - 如何使某些操作“类似于单例”

Posted

技术标签:

【中文标题】跨进程加载的 DLL - 如何使某些操作“类似于单例”【英文标题】:DLL loaded across processes - how to make certain actions "singleton-like" 【发布时间】:2017-03-21 06:58:54 【问题描述】:

背景

我正在开发一个C++ windows 应用程序。

我的应用程序将打开多个进程。每个进程都会加载一个Logger DLL,它提供了一个接口来注册日志事件并决定何时以及如何将日志刷新到文件中。

问题

假设我在Logger DLL 中定义了一个简单的决定:每次注册 100 个日志事件时,将它们刷新到文件中。

如何确保仅由 DLL 的一个实例执行一次刷新?

对于跨进程共享 DLL 且其中一项操作只需跨进程执行一次的情况,最佳实践方法是什么?

注意事项

定义共享内存至关重要。每个进程都需要了解其他进程的日志事件。因此,我们不能让每个 DLL 实例存储自己的日志并单独刷新它们。

【问题讨论】:

您必须已经在保护内存访问,大概是使用互斥对象。同样的机制不能保护刷新缓存事件的决定吗? @HarryJohnston 是的。它可以保护刷新缓存事件的决定 同时,但是,其他 DLL 实例如何知道其中一个实例过去已经做出了此决定。我想你可以说这没关系,因为在每个实例中都实现了相同的逻辑。因此,正如单个实例没有两次执行相同“工作”的风险一样,多个实例也没有两次执行相同“工作”的风险 通常情况下,如果将事件刷新到文件中并没有真正将它们从共享内存中删除,那么存储在共享内存中的变量之一就是跟踪哪些事件已经被写入。即使只有一个 DLL,您也必须需要 类似的东西,您目前如何确定要编写哪些事件? 您能否澄清您问题的最后一段,因为对此存在一些争论。进程 A 必须能够看到来自进程 B 的事件内容是实际要求吗?还是您只是认为这样可以更轻松地将事件写入文件? process 1想要从Logger DLL获取最后50个日志事件,那么Logger DLL必须从所有已经注册的进程返回最后50个日志事件事件。我想澄清某种“共享内存”是必不可少的。 【参考方案1】:

您的问题并没有说明日志架构是什么,例如,事件是附加到文件中,还是每次都写入一个新文件?当您将事件写入文件时,它们是否会从共享内存中删除?

在大多数情况下,您已经(以一种或另一种方式)跟踪哪些事件已写入,哪些仍需要写入,因此确定是否有一百个未决事件是微不足道的。您只需要将这些信息放在共享内存块中,并使用适当的同步屏障访问它。

但是,为了解决您提出的问题:

假设我在 Logger DLL 中定义了一个简单的决定:每次注册 100 个日志事件时,将它们刷新到一个文件中。

您当然可以在共享内存中使用简单的计数器来完成此操作。如果由于某种原因你不能把它放在现有的共享内存块中,你可以创建一个单独的。


如果您已经受到互斥锁的保护,这很简单:

void count_new_event()

    if ((*counter)++ == 100) 
    
       flush_events();
       (*counter) = 0;
    
   

如果您想避免声明互斥锁,您可以改为执行以下操作:

void count_new_event()

    for (;;)
    
        DWORD count = InterlockedCompareExchange(counter, 0, 99);
        if (count == 99)
        
            // The count was 99, so this is the 100th call
            // The call to InterlockedCompareExchange reset the counter
            flush_events();
            break;
        
        else
        
            if (InterlockedCompareExchange(counter, count + 1, count) == count)
            
                // We've successfully incremented the counter
                break;
            
            else
            
                // Oops, someone got in before us; try again
                continue;
            
        
    

请注意,我没有测试过这段代码,它也不一定是最有效的方法。但是它应该在大多数情况下都可以工作,并且如果您还没有互斥锁,它会比声明互斥锁更有效。

还请注意,我假设flush_events 将处理从不同进程对其调用重叠的情况。这可能就像让它声明互斥锁一样简单。如果不是,例如,如果一次刷新 100 个事件的效率可能不足以跟上新事件的速度,那么跟踪哪些事件需要写入几乎肯定会更好。


如果您想定期刷新日志,例如每分钟一次,而不是每一百个事件,您可以执行类似的操作。只需在共享内存中记录最后一次刷新的时间戳,并将其与当前时间进行比较。如果不想认领互斥体,可以使用InterlockedCompareExchange更新时间戳;对InterlockedCompareExchange 的调用将只在一个进程中成功,所以这就是执行刷新的进程。

【讨论】:

您如何分享这些活动?我知道如何将 DWORD 放入共享内存区域,但如何将 std::vector<Event>(或 std::vector<std::wstring>,如果您愿意)后备存储放入所述共享内存中?向量本身的新放置在这里没有帮助,排序的任何东西都没有,即使是自定义定位器也不能做到这一点。您的解决方案迫使 OF 手动手动管理数据。我更喜欢IPC。优化有时不会打扰任何人,并通过共享一个字节数组来为它付出代价,OP 必须在其之上强制结构和意义......不是那么好。 @conio,这个问题明确指出事件必须存储在共享内存中,在我看来,这部分已经完成了。 OP 可能有也可能没有充分的理由做出该决定,但这不是问题的一部分。 (但由于他必须以任何一种方式序列化数据,通过管道或套接字发送数据与将其放入存储内存中真的有什么优势吗?) 重读笔记。必须共享状态应该只有一个(即标题中的单例)。它可以位于日志进程之间的共享内存中,也可以位于 dmi 建议的架构中的日志服务器进程中,或者位于 Astral 平面中。要求是每 100 个来自所有源的事件组合刷新一次(并且显然不要丢失事件并且不要多次写入相同的事件)。一个进程不需要能够读取其他进程的日志消息,即使它是一种可能的实现的副作用... ... 实际要求,只是刷新确实发生了。没有说明这不能由服务器进程完成,或者不能有额外的进程。如果是,您会在对 dmi 的评论中使用该论点,而不是谈论性能。 @conio,请参阅 OP 对我的澄清请求的回复。他们说 DLL 确实需要能够从其他进程中查询日志条目,显然它必须能够将这些信息提供给客户端应用程序。【参考方案2】:

我的建议是设置如下内容:

    引入了一个日志管理器(它可以是一个专用的独立进程)。他的角色是:

    提供监听套接字 时间管理 注册/注销客户端 管理哪些实例可以将日志刷新到日志文件。

    每个 DLL 实例:

    有自己的日志缓冲区 连接到 Manager 的套接字(自己注册) 从管理器接收命令以刷新缓冲区。 在收到触发器后,将自己的数据附加到日志文件中,并通知经理工作已完成。

您也可以使用共享内存(或其他 API)来代替套接字。我个人觉得插座更方便。

【讨论】:

在这个设计中,Process 1如何通过Process 2“看到”日志事件注册? 进程 2 连接到进程 1 并等待命令刷新。收到命令后,进程 2 打开日志文件并将所有内部缓冲区写入那里,并向进程 1 发送响应。然后进程 1 向下一个进程(即进程 3)发出“启动”命令 为什么我的应用程序中的进程需要相互了解?他们应该只知道记录器 DLL。通过将 Logger 责任转移到流程来克服这个问题并不理想。 WDYT? 这是基于这种情况下的典型设计的建议。您还可以为此使用共享内存在 DLL 中实现一种单例模式,但我认为这会大大增加复杂性。 成熟的 IPC 比使用共享内存和简单的同步对象要复杂得多,而且效率要低得多。您需要一个消息协议、一个单独的侦听线程或异步 I/O 处理、某种基础设施来仅启动一次管理器......我的答案解决了 OPs 问题,如三行代码中所述,如果您计算声明,则为五行并释放互斥锁。 :-)

以上是关于跨进程加载的 DLL - 如何使某些操作“类似于单例”的主要内容,如果未能解决你的问题,请参考以下文章

C++ 跨进程通信

通过 IPC 跨进程传递对 COM 对象的引用?

跨进程共享状态变量

C++ 命名管道 与Winform跨进程通信

WinForm实现跨进程通信的方法

WinForm实现跨进程通信的方法