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

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


这是一个很好的视频,可以帮助你理解AutoResetEvent。http://www.youtube.com/watch?v=xaaRBh07N34 - Jaider
7个回答

3

最近我在处理异步调用和线程事件以及将它们返回到主线程时遇到了一些问题。

我使用了同步上下文来跟踪这些事情。下面的(伪)代码展示了目前对我有效的内容。

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启动一个定时器对象,以在适当的时间间隔内检查数据变量的值。一旦数据有了一个值,你就可以再触发另一个事件,告诉你现在数据有了一个值(当然要关闭定时器)。
这是一个相当丑陋的hack,但从理论上讲它可能有效。
抱歉,这是我半夜想到的最好方法了。该睡觉了...

听起来好像可以行得通。不过,就像你所说的那样,非常“hacky” :) - Rob Gray

2
我解决了异步转同步问题,至少使用了所有的.NET类。 链接 但它仍无法与COM一起工作。我怀疑是由于STA线程。由托管COM OCX的.NET组件引发的事件从未被我的工作线程处理,因此在WaitOne()上出现死锁。
也许其他人会欣赏这个解决方案 :)

在SO上总结解决方案,使用与问题中相同的简化,这不是一种礼貌的做法吗? - jwg

0
如果Task是WinForms组件,它可能非常了解线程问题,并在主线程上调用事件处理程序 - 这似乎是您所看到的情况。因此,它可能依赖于消息泵发生或其他一些东西。Application.Run有适用于非GUI应用程序的重载。您可以考虑启动一个线程并进行泵操作,以查看是否可以解决该问题。我还建议使用Reflector查看组件的源代码,以确定它正在做什么。

0
你已经接近成功了。你需要将DoTask方法运行在不同的线程上,这样WaitOne调用就不会阻止工作的进行。可以尝试像这样做:
Action<int, int> doTaskAction = t.DoTask;
doTaskAction.BeginInvoke(latitude, longitude, cb => doTaskAction.EndInvoke(cb), null);
taskDone.WaitOne();

看起来很有前途。我明天会试一下,然后投相应的票 :) - Rob Gray
不幸的是,如果我在当前线程上挂接事件处理程序(如上所示),并在不同的线程上调用DoTask,则事件处理程序仍会因taskDone.WaitOne()而阻塞。 - Rob Gray
这是某种 COM 组件吗?在工作线程上创建它的实例,以便它不会尝试进行 STA 线程调度。 - Hans Passant
处理程序被调用的线程取决于其被调用的位置,而不是它被连接的位置 - 你可能有其他问题导致DoTask方法无法正常工作。 - Scott Weinstein
我相信你可以使用超时值(以毫秒或TimeSpan形式)调用WaitOne()。你可以将其放在while循环中,并确保每次在超时时离开WaitOne()时调用Application.DoEvents(),以便UI线程可以更新自己。 - dviljoen

0

在重新阅读了Scott W的回答后,我对其进行的评论似乎有点晦涩。所以让我更明确一些:

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

WaitOne(200)会导致它每秒返回5次控制到您的UI线程(您可以根据需要进行调整)。DoEvents()调用将刷新窗口事件队列(处理所有窗口事件处理,如绘画等)。在您的类中添加两个成员(一个布尔标志“done”在此示例中,以及一个返回数据“street”在您的示例中)。

这是实现您想要完成的最简单的方法。(我自己的应用程序中有非常相似的代码,所以我知道它有效)


哦!此外,WaitOne的返回值可以告诉您它是因为超时还是被信号触发而返回。您可以将其用于“完成”。 - dviljoen
我不会那样做。有关我的担忧的更多信息,请参见http://blogs.msdn.com/jfoscoding/archive/2005/08/06/448560.aspx。 - ollifant
Application.DoEvents存在的问题是它需要Application,而Application仅适用于WinForms应用程序。在类库中调用Application.Run等方法是行不通的。 - Rob Gray
如果没有应用程序实例,那么你没有在运行。 - dviljoen
System.Windows.Forms.Application。我不使用Windows Forms应用程序。您建议我尝试Application.Run(ApplicationContext)(也在System.Windows.Forms命名空间中)。考虑到它不是WinForms应用程序,似乎有点奇怪。 - Rob Gray

0

你的代码几乎是正确的... 我只是做了些改动

t.DoTask(latitude, longitude);

for

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接