【问题标题】:Is it possible to put an event handler on a different thread to the caller?是否可以将事件处理程序放在与调用者不同的线程上?
【发布时间】:2009-01-28 03:32:04
【问题描述】:

假设我有一个名为 Tasking 的组件(我无法修改),它公开了一个方法“DoTask”,该方法执行一些可能冗长的计算并通过事件 TaskCompleted 返回结果。通常这是在用户获得结果后关闭的窗口形式中调用的。

在我的特定场景中,我需要将一些数据(数据库记录)与 TaskCompleted 中返回的数据相关联,并使用它来更新数据库记录。

我研究了使用 AutoResetEvent 来通知事件何时被处理。问题是 AutoResetEvent.WaitOne() 将阻塞并且永远不会调用事件处理程序。通常 AutoResetEvents 被称为是一个单独的线程,所以我猜这意味着事件处理程序与调用的方法位于同一线程上。

本质上,我想通过阻塞直到处理事件并将结果放置在双方都可以访问的位置将异步调用(结果通过事件返回)转换为同步调用(即从另一个类调用 DoSyncTask)事件处理程序和调用启动异步调用的方法的方法。

public class SyncTask
{
    TaskCompletedEventArgs data;
    AutoResetEvent taskDone;

public SyncTask()
{
    taskDone = new AutoResetEvent(false);
}

public string DoSyncTask(int latitude, int longitude)
{
    Task t = new Task();
    t.Completed = new TaskCompletedEventHandler(TaskCompleted);
    t.DoTask(latitude, longitude);
    taskDone.WaitOne(); // but something more like Application.DoEvents(); in WinForms.
    taskDone.Reset();
    return data.Street;
}

private void TaskCompleted(object sender, TaskCompletedEventArgs e)
{
    data = e;
    taskDone.Set(); //or some other mechanism to signal to DoSyncTask that the work is complete.
}
}

In a Windows App the following works correctly.

public class SyncTask
{
    TaskCompletedEventArgs data;

public SyncTask()
{
    taskDone = new AutoResetEvent(false);
}

public string DoSyncTask(int latitude, int longitude)
{
    Task t = new Task();
    t.Completed = new TaskCompletedEventHandler(TaskCompleted);
    t.DoTask(latitude, longitude);
    while (data == null) Application.DoEvents();

    return data.Street;
}

private void TaskCompleted(object sender, TaskCompletedEventArgs e)
{
    data = e;
}
}

我只需要在窗口服务中复制该行为,其中 Application.Run 未被调用且 ApplicationContext 对象不可用。

【问题讨论】:

标签: c# multithreading events delegates


【解决方案1】:

我最近在线程上进行异步调用和事件并将它们返回到主线程时遇到了一些麻烦。

我使用SynchronizationContext 来跟踪事情。下面的(伪)代码显示了目前对我有用的东西。

SynchronizationContext context;

void start()
{
    //First store the current context
    //to call back to it later
    context = SynchronizationContext.Current; 

    //Start a thread and make it call
    //the async method, for example: 
    Proxy.BeginCodeLookup(aVariable, 
                    new AsyncCallback(LookupResult), 
                    AsyncState);
    //Now continue with what you were doing 
    //and let the lookup finish
}

void LookupResult(IAsyncResult result)
{
    //when the async function is finished
    //this method is called. It's on
    //the same thread as the the caller,
    //BeginCodeLookup in this case.
    result.AsyncWaitHandle.WaitOne();
    var LookupResult= Proxy.EndCodeLookup(result);
    //The SynchronizationContext.Send method
    //performs a callback to the thread of the 
    //context, in this case the main thread
    context.Send(new SendOrPostCallback(OnLookupCompleted),
                 result.AsyncState);                         
}

void OnLookupCompleted(object state)
{
    //now this code will be executed on the 
    //main thread.
}

我希望这会有所帮助,因为它为我解决了问题。

【讨论】:

    【解决方案2】:

    也许您可以让 DoSyncTask 启动一个计时器对象,以某个适当的时间间隔检查您的数据变量的值。一旦数据有了值,您就可以触发另一个事件来告诉您数据现在有了值(当然还要关闭计时器)。

    相当丑陋的黑客,但它可以工作......理论上。

    对不起,这是我能想到的最好的半睡半醒。该睡觉了……

    【讨论】:

    • 听起来可行。就像你说的那样,非常“hacky”:)
    【解决方案3】:

    我找到了解决异步到同步问题的方法,至少使用了所有 .NET 类。

    http://geekswithblogs.net/rgray/archive/2009/01/29/turning-an-asynchronous-call-into-a-synchronous-call.aspx

    它仍然不适用于 COM。我怀疑是因为 STA 线程。由承载 COM OCX 的 .NET 组件引发的事件从未由我的工作线程处理,因此我在 WaitOne() 上遇到了死锁。

    其他人可能会欣赏这个解决方案:)

    【讨论】:

    • 使用与问题中相同的简化方式在 SO 上总结解决方案不是很有礼貌吗?
    【解决方案4】:

    如果 Task 是一个 WinForms 组件,它可能会非常了解线程问题并在主线程上调用事件处理程序——这似乎就是您所看到的。

    因此,它可能依赖于正在发生的消息泵或其他事情。 Application.Run 具有适用于非 GUI 应用程序的重载。您可以考虑让一个线程启动和抽水,看看是否能解决问题。

    我还建议使用 Reflector 来查看组件的源代码,以了解它在做什么。

    【讨论】:

      【解决方案5】:

      你几乎得到它。您需要 DoTask 方法在不同的线程上运行,以便 WaitOne 调用不会阻止工作的完成。像这样的:

      Action<int, int> doTaskAction = t.DoTask;
      doTaskAction.BeginInvoke(latitude, longitude, cb => doTaskAction.EndInvoke(cb), null);
      taskDone.WaitOne();
      

      【讨论】:

      • 看起来很有希望。明天我会试一试并相应投票:)
      • 不幸的是,如果我在当前线程上连接事件处理程序(如上)并在不同的线程上调用 DoTask,事件处理程序仍然被 taskDone.WaitOne() 阻塞;
      • 这是某种 COM 组件吗?在工作线程上创建它的实例,这样它就不会尝试编组到 STA 线程。
      • 调用处理程序的线程取决于调用处理程序的位置,而不是连接的位置 - 您可能有其他问题阻止 DoTask 方法正常工作
      • 我相信您可以使用超时值调用 WaitOne()(以毫秒为单位或使用 TimeSpan)。你可以把它放在一个 while 循环中,并确保调用 Application.DoEvents() 你让 WaitOne 超时的所有事情,这样 UI 线程就可以更新自己。
      【解决方案6】:

      在我重新阅读之后,我对 Scott W 答案的评论似乎有点神秘。所以让我更明确一点:

      while( !done )
      {
          taskDone.WaitOne( 200 );
          Application.DoEvents();
      }
      

      WaitOne( 200 ) 将使其每秒 5 次将控制权返回给您的 UI 线程(您可以根据需要进行调整)。 DoEvents() 调用将刷新 windows 事件队列(处理所有 windows 事件处理的队列,如绘画等)。向您的班级添加两个成员(在本例中一个布尔标志“done”,在您的示例中一个返回数据“street”)。

      这是完成您想做的事情的最简单方法。 (我自己的应用程序中有非常相似的代码,所以我知道它可以工作)

      【讨论】:

      • 哦!此外,WaitOne 的返回值可以告诉您它是因为超时还是被发出信号而返回的。您可以将其用于“完成”。
      • 我不会那样做的。有关我的担忧的更多信息,请参阅blogs.msdn.com/jfoscoding/archive/2005/08/06/448560.aspx
      • Application.DoEvents 的问题是它需要应用程序,该应用程序仅适用于 WinForms 应用程序。在类库中调用 Application.Run 等不起作用。
      • 如果没有应用程序实例,那么您没有运行。
      • System.Windows.Forms.Application。我没有使用 Windows 窗体应用程序。您是否建议我尝试 Application.Run(ApplicationContext) (也在 System.Windows.Forms 命名空间中)。考虑到它不是一个 winforms 应用程序,这似乎有点奇怪。
      【解决方案7】:

      你的代码几乎是正确的......我只是改变了

      t.DoTask(latitude, longitude);
      

      new Thread(() => t.DoTask(latitude, longitude)).Start();
      

      TaskCompleted 将在与DoTask 相同的线程中执行。这应该可以。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-03-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多