【问题标题】:Best Design Practices for Passing Around Multithread Data?传递多线程数据的最佳设计实践?
【发布时间】:2021-08-26 05:48:39
【问题描述】:

我正在尝试创建一个简洁高效的设计,将事件传递给后台线程进行评估,然后将选定的结果返回给游戏线程。

这是我最初的设计

//Occurrence object passed from director on game thread to background thread OccurrenceQueue

    //Execute BackgroundThread::EvaluateQueues()

        //If OccurrenceQueue.Dequeue()

            //Score Occurrence, then return via Occurrence.GetOwner()->PurposeSelected(Occurrence)

            //EvaluateQueues()

这导致了从事件中选择一系列目的的干净循环。所以现在我想把它移到后台线程。以下是我到目前为止所学到的:

  • 线程安全(在 UE 中)绝对不需要修改 UObject 来自其他线程的数据(根据我的阅读,这是由于他们的自定义 GC)

    • 您可以锁定对象和/或设计,以便背景中的对象
      线程没有被游戏线程触及,但仍有风险
      由于背景未延长寿命而导致的意外行为 线程和同步问题
  • 您不能简单地在游戏线程现有对象上执行函数 将调用栈移回游戏线程

    • 调用 Occurrence.GetOwner()->PurposeSelected(Occurrence) 从 后台线程保留在后台线程中

      • 这是我希望更好地理解的主要主题
    • 这也适用于 UE 中的代表

  • UE 中的 TQueue 可以安全地跨线程使用

从我上面了解到的情况来看,我目前的设计似乎在逻辑上是不可能的。

到目前为止,这些是我的选择:

  • 使用两个队列

    • 一个在后台线程上出列和评分

    • 另一个通过tick使游戏线程上的结果出队

  • 使用游戏线程中存在的委托,通过tick调用OccurrenceEvaluated.Broadcast()

    • 对结果进行评分时,将 Occurrence.GetOwner()->PurposeSelected(Occurrence) 绑定到 OccurrenceEvaluated
  • 我已经看到 c++ 使用称为 Future() 的东西(或类似的东西)来执行异步任务,并且似乎 UE 与 TFuture && TFuture.IsReady() 有类似的东西,但我还没有深入研究了解它以及它如何返回数据

    • 与 FAsyncTask 相同

我很犹豫是否要实现任何利用滴答检查数据是否已从后台线程更新/返回的设计。

任何人都可以建议相关的设计实践,或澄清从后台线程返回执行到主线程的性质(我很难找到正确的问题来研究/了解这方面的信息)?

【问题讨论】:

    标签: c++ multithreading queue unreal-engine4


    【解决方案1】:

    我找到了一个完美的解决方案。由于这些事件对时间不是特别敏感,我只是使用虚幻引擎的 AsyncTask() 从我的后台线程中安排游戏线程上的异步任务。正如@Pepjin Kramer 指出的那样,它与 std::async 相同。

    如此简单,基本上就是一记耳光。

    【讨论】:

      【解决方案2】:

      早安,

      线程可以帮助您更快/更灵敏地完成工作,但也有很多陷阱。在这种情况下的简短回答我使用这些结构

      #include <future>
      #include <thread>
      #include <vector> 
      
      int main()
      {
           // make data shared
           auto data = std::make_shared<std::vector<int>>();
           data -> push_back(42);
      
           // future will be a future<int> deduced from return from vector
           // async will run lambda in other thread
           // capture data by value! It copies the shared_ptr and will
           // increase its reference count.
           auto future = std::async(std::launch::async, [data]
           {
              auto value = data[0];
              return value;
              // shared_ptr data goes out of scope and reference count to data is decreased
           });
      
           // synchronize with thread and get "calculated" value
           auto my_value = future.get();
          // now data on the main thread goes out of scope reference count is decreased. Only when both the thread are done AND this function exits then data is deleted.
          return 0;
      } 
      
      • 大多数对象占用内存,并且在分配期间内存不会在 1 个时钟周期内更新。您可以使用 sdt::mutex 和 std::scoped_lock 保护内存。尝试寻找那些。
      • 你经常需要在线程之间同步信息/处理,看看std::condition_variable(但它们有陷阱https://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables
      • C++ 有很好的支持类,std::thread 和 std::async/std::future。我个人喜欢 std::future 的解决方案,因为您可以返回值和异常 (!) 当您调用 future.get() 时来自其他线程
      • 了解 lambda 和捕获,它们对于与 std::thread/std::async 一起使用至关重要
      • 对象的生命周期!在线程之间共享对象时,您必须确保一个线程不会删除其他线程正在使用的对象。使用线程时不要使用原始指针和/或新建/删除。在线程之间共享信息时,我个人经常使用 std::make_shared/std::shared_ptr's to data/objects。
      • 另一件棘手的事情是,有时您无法确定创建另一个线程后是否已开始工作。例如。当 std::async 返回时,会创建一个线程,但不能保证它已经真正启动(操作系统调度等)。如果您想确定它已经启动,那么在异步调用返回后,您将不得不等待您在线程函数开始时设置的 condition_variable。

      我希望这些评论可以帮助您入门。

      【讨论】:

      • 感谢您的回复!我可以说我理解您提到的大部分要点(生命周期、以线程安全的方式使用对象等)。似乎虚幻引擎的 future.get() 版本可能是最接近我想要的解决方案。我今天开始研究它。我不太确定如何表达它,但主要是我试图了解如何将调用堆栈从一个线程移动到另一个线程。就像我在游戏线程的后台线程上调用 Run() 一样,它会在游戏线程上执行,反之亦然。如果我能理解,我可以围绕这种理解进行设计。
      • 你不能真正在线程之间移动调用栈。最接近的可能是使用(复制)捕获的变量(您可以将其视为堆栈的副本)制作 lambda。 docs.unrealengine.com/4.26/en-US/API/Runtime/Core/Async/Async 似乎接近 std::async,它只是有这个在完成时调用的额外回调。所以这可能是一个好的开始,尽管我从未使用过虚幻;)
      • 我不这么认为,我希望也许有一个设计技巧可以解决这个问题,以便传回数据。我只是在查看 FRunnableThread 的源代码(这是我用来处理队列的后台线程),看看我是否可以弄清楚他们如何在游戏线程上创建对象但在后台线程上运行,但这有点超出我的理解。希望 FAsyncTask 成功,如果我找到一个解决方案,我会确保添加一个解决方案。谢谢!
      • 解决方案将取决于您要对后台线程执行的操作。我可以想象游戏线程是一个观察者,只是不时检查后台是否准备好新数据。或者后台只是计算新值,在线程都可以访问的内存中更新它们,并且游戏线程只有在准备好时才读取它们(数据当然受互斥锁保护)。祝你好运找到解决方案!
      • 是的,我确实创建了一个工作解决方案来做这件事,一旦后台线程处理了事件,它将它绑定到游戏线程中存在的委托。我可以让委托在每个滴答声中广播,也可以手动告诉游戏线程何时滴答作响,但我真的不喜欢它的设计。它有效,只是对我来说不干净。我是自学成才的,所以有很多我不知道的设计范式,而且由于我缺乏传统的学习,这让我觉得不专业。我试图避免蜱虫,到目前为止我有
      猜你喜欢
      • 2010-09-05
      • 2014-06-04
      • 1970-01-01
      • 1970-01-01
      • 2011-11-23
      • 2016-09-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多