C#多线程异步问题

7

我有一些代码,当调用时会调用一个Web服务,查询数据库并从本地缓存中获取值。然后它将这三个操作的返回值组合起来生成结果。我想要的是并行异步执行这些操作,而不是按顺序执行。以下是一些虚拟/示例代码:

var waitHandles = new List<WaitHandle>();

var wsResult = 0;
Func<int> callWebService = CallWebService;
var wsAsyncResult = callWebService.BeginInvoke(res => { wsResult = callWebService.EndInvoke(res); }, null);
waitHandles.Add(wsAsyncResult.AsyncWaitHandle);

string dbResult = null;
Func<string> queryDB = QueryDB;
var dbAsyncResult = queryDB.BeginInvoke(res => { dbResult = queryDB.EndInvoke(res); }, null);
waitHandles.Add(dbAsyncResult.AsyncWaitHandle);

var cacheResult = "";
Func<string> queryLocalCache = QueryLocalCache;
var cacheAsyncResult = queryLocalCache.BeginInvoke(res => { cacheResult = queryLocalCache.EndInvoke(res); }, null);
waitHandles.Add(cacheAsyncResult.AsyncWaitHandle);

WaitHandle.WaitAll(waitHandles.ToArray());          
Console.WriteLine(string.Format(dbResult, wsResult, cacheResult));

问题在于最后一行会抛出一个错误,因为在执行时dbResult仍然为null。一旦调用queryDB.EndInvoke,WaitHandle被标记,执行继续,而queryDB.EndInvoke的结果被分配给dbResult之前。有没有巧妙/优雅的方法解决这个问题?注意:我应该补充说明,这是因为queryDB是最后一个被标记的等待句柄,所以会影响到dbResult。更新:虽然我接受了Philip的答案,但根据Andrey的评论,我应该补充说明这也可以工作:
var waitHandles = new List<WaitHandle>();

var wsResult = 0;
Func<int> callWebService = CallWebService;
var wsAsyncResult = callWebService.BeginInvoke(null, null);
waitHandles.Add(wsAsyncResult.AsyncWaitHandle);

string dbResult = null;
Func<string> queryDB = QueryDB;
var dbAsyncResult = queryDB.BeginInvoke(null, null);
waitHandles.Add(dbAsyncResult.AsyncWaitHandle);

var cacheResult = "";
Func<string> queryLocalCache = QueryLocalCache;
var cacheAsyncResult = queryLocalCache.BeginInvoke(null, null);
waitHandles.Add(cacheAsyncResult.AsyncWaitHandle);

WaitHandle.WaitAll(waitHandles.ToArray());

var wsResult = callWebService.EndInvoke(wsAsyncResult);
var dbResult = queryDB.EndInvoke(dbAsyncResult);
var cacheResult = queryLocalCache.EndInvoke(cacheAsyncResult);

Console.WriteLine(string.Format(dbResult, wsResult, cacheResult));

不是答案,但升级到Fx4会让这个过程变得更容易。 - H H
4个回答

3

很遗憾,在EndInvoke()调用返回之前,WaitHandle将始终被标记。这意味着您不能依赖它。

如果您无法使用4.0,那么线程系统或手动等待处理可能是必要的(或可怕的Sleep() hack!)。您还可以使调用的方法设置结果(因此在设置结果值之后发生EndInvoke),但这意味着将结果移动到共享位置而不是本地变量,可能需要进行小的重新设计。

或者如果您可以使用4.0,我会建议这样做- System.Threading.Tasks 中有很多好东西。您可以重写为:

var tasks = new List<Task>();

var wsResult = 0;
string dbResult = null;
var cacheResult = "";

tasks.Add( new Task( ()=> wsResult = CallWebService()));
tasks.Add( new Task( ()=> dbResult = QueryDB()));
tasks.Add( new Task( ()=> cacheResult = QueryLocalCache()));

tasks.ForEach( t=> t.Start());
Task.WaitAll( tasks.ToArray());

Console.WriteLine(string.Format(dbResult, wsResult, cacheResult));

@Andrey 总是正确的;在调用返回之前会发出信号——因为方法本身是发出等待信号的,所以它不能在发出信号之前返回。但这并不意味着等待线程会立即获得控制权。 - Philip Rieck
对不起,我的意思是线程切换是不确定的,但信号是确定的 :) - Andrey

1

我会在这里使用3个线程并避免使用Invoke()。对我来说,线程更易读,甚至可以将其代码放入Thread.Start()内的匿名方法中。

启动后,您应该在此处.Join()所有3个线程,这样您就可以确保结果已准备好。

它可能是这样的:

Thread t1=new Thread( delegate() { wsResult = CallWebService(); } );
Thread t2=new Thread( delegate() { dbResult = QueryDb(); } );
Thread t3=new Thread( delegate() { cacheResult = QueryLocalCache(); } );
t1.Start(); t2.Start(); t2.Start();
t1.Join(); t2.Join(); t3.Join();

也许我误解了,但这不是 BeginInvoke 所做的吗?它启动一个新线程。如果我创建自己的线程,我仍然需要找到一种类似于 waithandles 的机制来等待每个线程完成,对吧? - BertC
3
创建新线程有很多看不见的开销,包括为新线程分配/保留堆栈空间等。当你在后台线程中几乎没有什么要做的时候,设置线程所需的时间几乎比执行线程本身所需的时间更长。线程池通过为不同的工作重复使用相同的线程来消除这些开销。 - dthorpe
IMO; BeginInvoke() -> 完全无法阅读;有人愿意发布一个使用ThreadPool的示例吗? - Daniel Mošmondor

1

首先我会解释为什么会发生这种情况,然后告诉如何修复它。

我们来编写一个简单的程序:

        var wsResult = 0;
        Func<int> callWebService = () => {
            Console.WriteLine("1 at " + Thread.CurrentThread.ManagedThreadId);
            return 5;
        };
        var wsAsyncResult = callWebService.BeginInvoke(res => {
            Console.WriteLine("2 at " + Thread.CurrentThread.ManagedThreadId);
            wsResult = callWebService.EndInvoke(res);
        }, null);
        wsAsyncResult.AsyncWaitHandle.WaitOne();
        Console.WriteLine("3 at " + Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine();
        Console.WriteLine("Res1 " + wsResult);
        Thread.Sleep(1000);
        Console.WriteLine("Res2 " + wsResult);

输出为:

1 at 3
3 at 1

Res1 0
2 at 3
Res2 5

这不是我们想要的。这是因为内部的Begin/End Invoke工作方式如下:

  1. 执行委托
  2. 信号等待句柄
  3. 执行回调函数

由于这发生在除主线程外的线程上,因此很可能会出现在2和3之间进行线程切换。

为了解决这个问题,您应该这样做:

        var wsResult = 0;
        Func<int> callWebService = () => {
            Console.WriteLine("1 at " + Thread.CurrentThread.ManagedThreadId);
            return 5;
        };
        var wsAsyncResult = callWebService.BeginInvoke(null, null);
        wsAsyncResult.AsyncWaitHandle.WaitOne();
        wsResult = callWebService.EndInvoke(wsAsyncResult);

结果将是正确且确定性的。


0
我会考虑将查询放入三个方法中,可以异步调用并在完成时触发“完成”事件。然后,当每个事件返回时更新状态,并在所有三个都为“true”时执行输出。
这可能不是很整洁/优雅,但它很直接,而且使用异步调用正是你想要的。

谢谢Chris。我曾经考虑过这个,但感觉很笨重,好像我得自己写WaitHandle信号量,这似乎违背了它们存在的初衷。 - BertC

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