简体   繁体   中英

c# threading async problem

I have some code that when called calls a webservice, queries a database and fetches a value from local cache. It then combines the return values from these three actions to produce its result. Rather than perform these actions sequentially I want to perform them asynchronously in parallel. Here's some dummy/example code :

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));

The problem is that the last line throws an error because dbResult is still null when it gets executed. As soon as queryDB.EndInvoke is called the WaitHandle is signalled and execution continues BEFORE the result of queryDB.EndInvoke is assigned to dbResult. Is there a neat/elegant way round this ?

Note : I should add that this affects dbResult simply because the queryDB is the last wait handle to be signalled.

Update : While I accepted Philip's answer which is great, following Andrey's comments, I should add that this also works :

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));

Unfortunately, the WaitHandle will always be signaled before the EndInvoke() call returns. Which mean you can't rely on this.

If you cannot use 4.0, a system of threads or manual waithandles is probably going to be in order (or the dreaded Sleep() hack!). You can also have the Invoked method be what sets your results (so EndInvoke happens after the result value is set), but that means moving the results to a shared location, and not local variables - probably requiring a small redesign.

Or If you can use 4.0, I would - System.Threading.Tasks is chock full 'o great stuff. You could rewrite to:

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));

I would go with 3 threads here and avoid Invoke() . To me, threads are more readable, and you can even put its code into a anonymous method inside the Thread.Start() .

After starting, you should .Join() all 3 threads here, and you'll be sure that your results are ready.

It would be something like:

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();

First i will explain why does it happen, then tell how to fix it.

Let's write simple program:

        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);

output is:

1 at 3
3 at 1

Res1 0
2 at 3
Res2 5

which is not that wanted. This happens because internally Begin/End Invoke works this way:

  1. Execute delegate
  2. Signal WaitHandle
  3. Execute callback

Since this happens on thread other then main it is possible (and very likely) that thread switch happens right between 2 and 3.

To fix it you should do:

        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);

and result will be both correct and deterministic.

I'd be tempted to put the queries into three methods that can be called asynchronously and fire a "complete" event when finished. Then when each event comes back update a status and when all three are "true" perform your output.

It might not be neat/elegant, but it is straight forward and with asynchronous calls that's what you want.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM