是否可以将事件处理程序放在与调用者不同的线程上?

时间:2009-01-28 03:32:04

标签: c# multithreading events delegates

假设我有一个名为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对象不可用。

7 个答案:

答案 0 :(得分:3)

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

我使用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.
}

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

答案 1 :(得分:2)

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

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

对不起,这是我能想到半睡半醒的最好的。上床睡觉的时间......

答案 2 :(得分:2)

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

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

它仍无法与COM一起使用。我怀疑是因为STA线程。承载COM OCX的.NET组件引发的事件永远不会被我的工作线程处理,所以我在WaitOne()上遇到了死锁。

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

答案 3 :(得分:0)

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

所以,它可能依赖于消息泵发生的事情。 Application.Run具有非GUI应用程序的重载。您可以考虑获取启动和启动的线程以查看是否可以解决问题。

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

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

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

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

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

这是获得你想做的最简单的方法。 (我在自己的应用程序中有非常相似的代码,所以我知道它有效)

答案 6 :(得分:0)

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

t.DoTask(latitude, longitude);

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

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