消费后台线程生产者生成的项目时如何保持 UI 响应?

问题描述 投票:0回答:1

我已将长时间运行的同步操作卸载到后台线程。需要一段时间才能开始,但最终它开始很好地生产物品。

问题是如何使用 - 同时保持响应式 UI(即响应 Paint 和 UserInput 消息)。

一个无锁示例设置了一个

while
循环;我们消费物品,而它们是要消费的物品:

// You call this function when the consumer receives the
// signal raised by WakeConsumer().
void ConsumeWork()
{
   Thing item;
   while ((item = InterlockedGetItemOffTheSharedList(sharedList)) != nil)
   {
      ConsumeTheThing(item);
   }
}

问题在于,后台线程一旦启动,就可以非常快速地生成项目。这意味着我的

while
循环永远不会有机会停止。这意味着它永远不会返回消息队列来响应挂起的绘画和鼠标输入事件。

我已将异步多线程应用程序置于同步等待状态,因为它位于内部:

while (StuffToDo)
{
   Consume(item);
}

发布消息

另一个想法是让后台线程

PostMessage
每次有项目可用时都会向主线程发送消息:

ProduceItemsThreadMethod()    
{
   Preamble();
   while (StuffToProduce())
   {
       Thing item = new Item();
       SetupTheItem(item);
       InterlockedAddItemToTheSharedList(item);
       PostMessage(hwndMainThreadListener, WM_ItemReady, 0, 0);
   }
}  

这样做的问题是任何发布的消息总是比任何消息都具有更高的优先级:

  • 绘制消息
  • 鼠标移动消息

因此,只要有可用的发布消息,我的应用程序就不会响应绘制和输入消息。

while GetMessage(out msg)
{
   DispatchMessage(out msg);
}

每次调用

GetMessage
都会返回一条新的
WM_ItemReady
消息。我的 Windows 消息处理将被 ItemReady 消息淹没 - 阻止我在添加所有项目之前处理绘画。

我已将异步多线程应用程序置于同步等待状态。

限制发布消息的数量没有帮助

上面的内容实际上比第一个变体更糟糕,因为我们用发布的消息淹没了主线程。我们想要做的只是在主线程尚未处理我们发布的上一条消息时才发布一条消息。我们可以创建一个标志,用于指示我们是否已经发布了消息,以及主线程是否仍未处理它

ProduceItemsThreadMethod()    
{
   Preamble();
   while (StuffToProduce())
   {
       Thing item = new Item();
       SetupTheItem(item);
       InterlockedAddItemToTheSharedList(item);

       //Only post a message if the main thread has a message waiting
       int oldFlagValue = Interlocked.Exchange(g_ItemsReady, 1);
       if (oldFlagValue == 0) 
           PostMessage(hwndMainThreadListener, WM_ItemReady, 0, 0);
   }
}  

在主线程中,当我们处理完排队的项目时,我们会清除 “ItemsReady” 标志:

void ConsumeWork()
{
   Thing item;
   while ((item = InterlockedGetItemOffTheSharedList(sharedList)) != nil)
   {
      ConsumeTheThing(item);
   }

   Interlocked.Exchange(g_ItemsReady, 0); //tell the thread it can post messages to us again
}

问题又是线程填充列表的速度比我们消耗它的速度快;因此,为了处理用户输入,我们永远不会对

ConsumeWork()
函数进行任何更改。

一旦

ConsumeWork
返回,后台生产者线程就会生成一条新的
WM_ItemReady
消息。下次我打电话的时候
GetMessage

while GetMessage(out msg)
{
   DispatchMessage(out msg);
}

这将是一条

WM_itemReady
消息。我会陷入循环。

我已将异步多线程应用程序置于同步等待状态。

限制自己的项目数量并没有帮助

我们可以尝试在处理 100 个项目后强制退出

while
循环:

void ConsumeWork()
{
   int itemsProcessed = 0;
   Thing item;
   while ((item = InterlockedGetItemOffTheSharedList(sharedList)) != nil)
   {
      ConsumeTheThing(item);
      itemsProcessed += 1;
      if (itemsProcessed >= 250)
         break;
   }

   Interlocked.Exchange(g_ItemsReady, 0); //tell the thread it can post messages to us again
}

这与前一个版本存在同样的问题。虽然我们将离开

while
循环,但我们收到的下一条消息将再次是 WM_ItemReady:

while (GetMessage(...) != 0)
{
   TranslateMessge(...);
   DispatchMessage(...);
}

那是因为

WM_PAINT
消息只有在没有其他消息时才会出现。该线程渴望创建一个新的 WM_ItemReady 消息并将其发布到我的队列中。

自己泵送消息循环?

当看到人们手动发送消息来修复无响应的应用程序时,有些人会内心哭泣。因此,让我们尝试手动发送消息来修复无响应的应用程序!

void ConsumeWork()
{
   Thing item;
   while ((item = InterlockedGetItemOffTheSharedList(sharedList)) != nil)
   {
      ConsumeTheThing(item);
      ManuallyPumpPaintAndInputEvents();
   }

   Interlocked.Exchange(g_ItemsReady, 0); //tell the thread it can post messages to us again
}

我不会详细介绍该函数,因为它会导致重入问题。如果我的库的用户碰巧尝试关闭他们所在的窗口,用它破坏我的帮助器类,我会突然回到已被破坏的类中执行:

ConsumeTheThing(item);
ManuallyPumpPaintAndInputEvents(); //processes WM_LBUTTONDOWN messages will closes the window which destroys me
InterlockedGetItemOffTheSharedList(sharedList) //sharedList no longer exist BOOM

我一路走下去

我一直在兜圈子,试图解决如何在使用后台线程时保持响应式 UI 的问题。我在这个问题上修改了四个解决方案,在提出这个问题之前还修改了其他三个解决方案。

我不可能是第一个在用户界面中使用生产者-消费者模型的人。

如何维护响应式 UI?

如果我可以发布一条优先级低于 Paint、Input 和 Timer 的消息就好了 :(

windows multithreading producer-consumer
1个回答
0
投票

答案使用计时器。

我已经知道这一点大约17年了,当双核第一次成为一种事物时,多线程代码突然变得同步,因为主线程无法完成任何事情,因为我们过去只在主线程之后发送通知已处理最后的通知。由于没有 “消息”,您可以将其发布到 “到行尾”,主线程就会锁定。

大约 8 年前,我希望找到在 Windows 上编写生产者-消费者的正确方法。

原来没有正确的方法;你只需要破解它即可。

© www.soinside.com 2019 - 2024. All rights reserved.