.NET UI上下文中的异步






4.98/5 (23投票s)
使用IAsyncResult、BackgroundWorker、TPL和“async”语法进行UI和并发编程。
目录
简介
我们今天编写的大部分代码仍然是同步的,但有时多线程应用程序是不可避免的。当今的平台(软件框架和主要是硬件)已经有所改进,以适应这种日益增长的需求。但仅依赖System.Threading
命名空间 [^]中的原始类型并不能使编程变得容易,特别是当它涉及到具有线程固有亲和性的UI元素时。因此,微软试图通过建立一些模式 [^]并添加编译器支持来推进.NET异步编程。第一个模式基于.NET 1.1中的委托IAsyncResult
[^],然后它似乎在.NET 2.0中通过使用基于事件的异步 [^]和BackgroundWorkers [^]稳定下来,直到.NET 4.0中出现了任务并行库 [^](TPL),最后是基于它的async语言语法 [^]。后者很可能会在下一版Visual Studio中出现,可能是2012年。
背景
这篇文章更像是一个练习,展示如何从一些粗糙的同步代码开始,并使用我上面提到的模式对其进行并行化。它无意成为我上面列出的任何主题的入门介绍。在我写这篇文章的时候,Visual Studio 2012仍处于beta阶段,并且为了使用Visual Studio 2010,你需要安装Visual Studio Async CTP (SP1 Refresh) [^]才能运行代码。我无意挑选赢家和输家,只是展示一些使用.NET/C#处理并发和UI的方法(VB应该非常相似)。请做好花时间安装此包的准备。在这项练习中,我将尝试为代码实现三个目标:
- UI应保持响应,并在任何操作完成时进行更新
- 代码应并行且独立地执行其操作
- 所有并发操作完成后,应通知用户
在本文中,为了保持简单,我不会讨论纯粹的多线程 [^]实现、取消或超时。我们将观察结果,并以不同的方式看到实现上述目标所需的工作。
工作原理
与大多数在线信息关注某种冗长的IO操作(如下载)不同,我将创建自己的“冗长”函数,该函数接受一个字符串作为输入参数并返回另一个字符串。我将借此机会稍微窥探一下不同并行实现的内部工作原理,通过返回操作的持续时间和最重要的是“冗长”操作执行时的ManagedThreadId
[^]。我将创建多个并发活动,而不是只让UI线程运行一个并行活动。函数代码以及最终将为每个操作传递的参数和过程如下所示:
string[] _myArguments = { "7", "4", "bad", "3" };
private static string MyFunction(string myArg)
{
Stopwatch duration = Stopwatch.StartNew();
try
{
int pause = int.Parse(myArg);
Thread.Sleep(pause * 1000);//make seconds
duration.Stop();
return string.Format(
"MyFunction with argument '{0}' lasted {1} ms on thread {2}",
myArg, duration.ElapsedMilliseconds,
Thread.CurrentThread.ManagedThreadId);
}
catch (Exception ex)
{
duration.Stop();
throw new ApplicationException(string.Format(
"Exception '{3}' thrown for argument '{0}'." +
"It lasted {1} ms on thread {2}",
myArg, duration.ElapsedMilliseconds,
Thread.CurrentThread.ManagedThreadId,
ex.Message), ex);
}
}
操作的持续时间(以秒为单位)由输入参数确定,如果参数不是数字字符串(例如,“bad”),则会抛出异常,以覆盖这种情况。
从原始的同步代码开始
让我们从一个简单的同步代码循环开始,稍后我们将尝试使用不同的技术使其并行化。下面的代码在结果可用时更新文本框。
private void _buttonSynch_Click(object sender, EventArgs e)
{
DisableUIControls(true);
_textBox.Text = string.Format(
"Using synchronous calls: Main thread Id is {0}\r\n",
Thread.CurrentThread.ManagedThreadId);
Stopwatch duration = Stopwatch.StartNew();
foreach (string arg in _myArguments)
{
Application.DoEvents();
try
{
_textBox.Text += string.Format(
"Result on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
MyFunction(arg));
}
catch (Exception ex)
{
_textBox.Text += string.Format(
"Exception thrown on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId, ex.Message);
}
}
duration.Stop();
_textBox.Text += string.Format(
"The UI thread {0} was blocked for {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
DisableUIControls(false);
}
以下是结果:
Using synchronous calls: Main thread Id is 1
Result on thread 1:"MyFunction with argument '7' lasted 7000 ms on thread 1"
Result on thread 1:"MyFunction with argument '4' lasted 3999 ms on thread 1"
Exception thrown on thread 1:"Exception 'Input string was not
in a correct format.' thrown for argument 'bad'.It lasted 5 ms on thread 1"
Result on thread 1:"MyFunction with argument '3' lasted 2999 ms on thread 1"
The UI thread 1 was blocked for 14047 ms
如果不是因为Application.DoEvents()
[^],UI在执行事件处理程序14秒期间会冻结。我使用DoEvents
来尝试在循环过程中获得UI的一点响应能力,并显示每个方法调用的完成,但等待应用程序响应数秒仍然会给用户带来非常糟糕的体验。任务是顺序执行的,循环的总时长将是所有迭代的总和。虽然前两个目标失败了,但至少关于通知用户的第三个目标成功了。当然,我们可以做得更好。
异步使用委托,与UI线程并行
现在很清楚,拥有一个响应迅速的UI需要涉及一些额外的线程 [^],这些线程允许UI线程将耗时的操作卸载到其他线程,并在事件处理程序中花费尽可能少的时间。此外,非UI线程在需要时应能够回发到主UI线程。最早出现在.NET 1.1中的异步编程模式是使用委托 [^]。它提供的功能比底层的线程模型 [^]并没有多多少,只是增加了一些编译器支持 [^]。最明显的弱点可能是缺乏从非UI线程到UI线程的调用编组。由于UI元素对其自己的线程具有亲和性,您需要自己处理它,以避免随机抛出InvalidOperatinException
。为了做到这一点,我依赖于Control.Invoke
[^],如下面的辅助方法所示:
void UpdateTextAsync(string format, int threadId, object txt)
{
this.Invoke(new Action(() =>
_textBox.Text += string.Format(format, threadId, txt)));
}
在UI处理程序中,目标是将“繁重”的工作分配给一个或多个后台线程,并尽快返回。这就是为什么在下面的代码中BeginInvoke
会在后台启动一个线程,并在回调 [^]中完成所有工作后进行回发。不幸的是,对于Delegate
来说,即使是回调也在与刚刚运行委托处理程序的同一个线程上执行,正如我们将在下面的结果中看到的。因此,没有办法在没有像UpdateTextAsync
这样的辅助方法的情况下安全地调用UI元素。
private void _buttonDelegates_Click(object sender, EventArgs e)
{
DisableUIControls(true);
_textBox.Text = string.Format("Using delegates: Main thread Id is {0}\r\n",
Thread.CurrentThread.ManagedThreadId);
Stopwatch duration = Stopwatch.StartNew();
Action asyncMonitor = new Action(monitorActionsAsync);
AsyncCallback finalCallback = new AsyncCallback((IAsyncResult ar) =>
{
try
{
UpdateTextAsync("Final callback from " +
"thread {0} :\"Everything lasted {1} ms\"\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds.ToString());
}
catch (Exception ex)
{
UpdateTextAsync("Final callback from " +
"thread {0} :\"Exception thrown in finalCallback :{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId, ex.Message );
}
finally
{
this.Invoke(new Action(() => DisableUIControls(false)));
//DisableUIControls(false); could throw InvalidOperatinException
}
});
asyncMonitor.BeginInvoke(finalCallback, duration);
_textBox.Text += string.Format("The UI thread {0} was blocked for {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
}
虽然上面显示的处理程序在UI线程上花费的时间非常少,但真正的繁重工作是由一些在后台执行的线程完成的。在下面的代码中,请注意,阻塞此线程不会影响UI响应能力,因为它不是UI线程。该线程将仅等待它启动的所有线程完成其活动,在这种情况下是执行MyFunction
。
void monitorActionsAsync()
{
Func<string, string> mydelegate = new Func<string, string>(MyFunction);
AsyncCallback asyncCallback = new AsyncCallback((IAsyncResult ar) =>
{
try
{
// Retrieve the delegate.
Func<string, string> caller =
(Func<string, string>)((AsyncResult)ar).AsyncDelegate;
UpdateTextAsync("Result callback from thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
caller.EndInvoke(ar));
}
catch (Exception ex)
{
UpdateTextAsync("Result callback from thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
"Exception thrown in asyncCallback :" + ex.Message);
}
finally
{
ar.AsyncWaitHandle.Close();
}
});
//thread spawning
IEnumerable<WaitHandle> waitHandles = _myArguments.Select
(
arg=>mydelegate.BeginInvoke(arg, asyncCallback, null).AsyncWaitHandle
);
WaitHandle.WaitAll(waitHandles.ToArray());
}
我认为大多数人会同意这段代码相当冗长和复杂,尽管匿名委托和lambda表达式有所帮助,但它们并没有挽救局面,无法让代码更易于阅读和理解。以下是结果:
Using delegates: Main thread Id is 1
The UI thread 1 was blocked for 4 ms
Result callback from thread 6:"Exception thrown in
asyncCallback :Exception 'Input string was not in a correct format.'
thrown for argument 'bad'.It lasted 6 ms on thread 6"
Result callback from thread 5:"MyFunction with argument '4' lasted 3999 ms on thread 5"
Result callback from thread 6:"MyFunction with argument '3' lasted 2999 ms on thread 6"
Result callback from thread 4:"MyFunction with argument '7' lasted 6999 ms on thread 4"
Final callback from thread 3 :"Everything lasted 7029 ms"
首先要注意的是,事件处理程序在UI线程上运行的时间非常短(4毫秒)。这就是在后台线程与其并行执行时使UI保持响应的原因。第二个观察结果是,所有委托都在不同的线程上(与回调相同)同时执行,并在最长的线程完成(7秒后)时收到了最终通知。可以说,我们实现了之前设定的所有目标。如果您需要关于使用的线程数量的更多优化,可以将monitorActionsAsync
更改为在线程启动后并在WaitHandle.WaitAll
之前同步调用MyFunction
,同时将其他线程像以前一样在BeginInvoke
启动的各自线程上执行。但这只是留给您自己享用的一个练习。
使用基于事件的异步模式实现UI并行
虽然您刚才已经看到,使用Control.Invoke
[^]可以克服线程亲和性通信限制,但自.NET 2.0以来,微软提供了BackgroundWorker [^]来处理这些问题。此外,新功能使这种模式对开发UI应用程序更具吸引力。
BackgoundWorker
[BackgoundWorker
[^]]没有与后台工作人员通信的功能,只能与UI线程通信。因此,与Delegate
的回调不同,它的公开事件在UI线程上执行,我们不应担心在实现中直接调用UI元素。为了获得所有工作线程已完成的通知,您可以尝试在RunWorkerCompleted事件
[^]中查询每个工作人员的IsBusy
[^]属性。我发现使用RunWorkerCompleted Event
中的递减计数器更容易获得相同的结果。下面是我的实现:
private void _buttonBgWorkers_Click(object sender, EventArgs e)
{
DisableUIControls(true);
_textBox.Text = string.Format(
"Using background workers: Main thread Id is {0}\r\n",
Thread.CurrentThread.ManagedThreadId);
int BusyWorkersCount = _myArguments.Length;
Stopwatch duration = Stopwatch.StartNew();
foreach (string arg in _myArguments)
{
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += new DoWorkEventHandler((object snd, DoWorkEventArgs ev) =>
{
ev.Result = MyFunction(ev.Argument as string);
});
string storedstr = arg;
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
(object snd, RunWorkerCompletedEventArgs ev) =>
{
if (ev.Error != null)
{
// First, handle the case where an exception was thrown.
_textBox.Text += string.Format(
"Exception postback on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId, ev.Error.Message);
}
else
{
// Finally, handle the case where the operation succeeded.
_textBox.Text += string.Format(
"Result postback on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId, ev.Result);
}
if (--BusyWorkersCount == 0)
{//when last thread finished, show it
duration.Stop();
_textBox.Text += string.Format(
"Final postback on thread {0}: Everything lasted {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
DisableUIControls(false);
}
});
bw.RunWorkerAsync(arg);
}
_textBox.Text += string.Format("The UI thread {0} was blocked for {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId, duration.ElapsedMilliseconds);
}
与异步委托实现相比,您可以发现代码更容易理解,以及lambda表达式如何通过共享BusyWorkersCount
在不同线程之间实现代码的紧凑。公平地说,我必须说这个算法也与前一种方法有很大的不同。该操作的结果如下所示:
Using background workers: Main thread Id is 1
The UI thread 1 was blocked for 7 ms
Exception postback on thread 1:"Exception 'Input string was not
in a correct format.' thrown for argument 'bad'.It lasted 5 ms on thread 5"
Result postback on thread 1:"MyFunction with argument '4' lasted 3999 ms on thread 4"
Result postback on thread 1:"MyFunction with argument '3' lasted 2999 ms on thread 5"
Result postback on thread 1:"MyFunction with argument '7' lasted 6999 ms on thread 3"
Final postback on thread 1: Everything lasted 7009 ms
结果表明,我们实现了最初设定的目标:事件处理程序在UI线程上运行的时间仅为7毫秒,这使得UI响应迅速,而后台工作者则与其并行执行。此外,所有工作者都在不同的线程上同时执行,并在最长的操作完成(7秒后)时收到了最终通知。此实现似乎与前一个性能相当。
使用TPL实现UI并发
.NET 4.0引入了任务并行库 [^](TPL),其中几乎所有内容都围绕着Task
[^]的概念。这很可能成为未来处理并行和并发的首选方式。因此,我创建了这个辅助函数,它将MyFunction
包装成一个面向任务的方法,该方法将在本文中用于TPL和async语法实现,作为一种简写表示法:
Task<string> MyFunctionAsync(string arg)
{
return Task.Factory.StartNew(() => MyFunction(arg));
}
下面的代码使用ContinueWith
[^]和ContinueWhenAll
[^]方法并行执行任务,然后通知UI线程完成。
private void _buttonTasks_Click(object sender, EventArgs e)
{
DisableUIControls(true);
_textBox.Text = string.Format("Using Tasks: Main thread Id " +
"is {0}\r\n", Thread.CurrentThread.ManagedThreadId);
TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Stopwatch duration = Stopwatch.StartNew();
Task[] tasks = (from arg in _myArguments
select MyFunctionAsync(arg)
.ContinueWith(resultTask =>
{
if (resultTask.Exception != null)
_textBox.Text += string.Format(
"Exception postback on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
resultTask.Exception.InnerException.Message);
else
_textBox.Text += string.Format(
"Result postback on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId, resultTask.Result);
},
uiScheduler)).ToArray();
Task.Factory.ContinueWhenAll(tasks,
(Task[] result) =>
{
duration.Stop();
_textBox.Text += string.Format("Final postback on " +
"thread {0}:Everything lasted {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
DisableUIControls(false);
}, CancellationToken.None, TaskContinuationOptions.None, uiScheduler);
_textBox.Text += string.Format("The UI thread {0} was blocked for {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId, duration.ElapsedMilliseconds);
}
请注意,使非UI线程和UI线程之间的过渡看起来无缝的是TaskScheduler
uiScheduler
对象,它作为“魔术”成分出现在ContinueWith
和ContinueWhenAll
调用的参数中。将此代码与上面基于类似机制的异步委托实现进行对比,您将看到代码表达力得到了很大的改进。运行此代码会产生以下结果:
Using Tasks: Main thread Id is 1
The UI thread 1 was blocked for 18 ms
Exception postback on thread 1:"Exception 'Input string was not in
a correct format.' thrown for argument 'bad'.It lasted 5 ms on thread 5"
Result postback on thread 1:"MyFunction with argument '4' lasted 3999 ms on thread 4"
Result postback on thread 1:"MyFunction with argument '3' lasted 2999 ms on thread 5"
Result postback on thread 1:"MyFunction with argument '7' lasted 6999 ms on thread 3"
Final postback on thread 1:Everything lasted 7018 ms
同样,结果表明事件处理程序在UI线程上仅花费了几毫秒(18毫秒),确保了UI响应能力,并且后台任务与其并行执行。看起来通常每个任务都有不同的线程与之关联,但在抛出异常时,线程会被重用。最终通知来自专门为其创建的任务回发到主UI线程。与之前的模式一样,我们实现了所有需要的目标。
在UI上下文中使用的新的“async”语法
正如我之前提到的,Visual Studio 2010不能“开箱即用”地支持新的异步语言语法,但它的后继版本很可能支持。在此期间,如果您需要利用异步编程 [^],您必须下载并安装Visual Studio Async CTP (SP1 Refresh) [^]。我提醒您,本文**不是**对.NET异步 [^]的介绍。为此,您已经在MSDN杂志[MSDN杂志
[^]]或CodeProject上的此处 [^]找到了一些不错的文章,并且肯定会有更多文章。
如何轻松地将同步代码“异步化”
其主要宣传的优点之一是将同步代码转换为异步的便捷性 [^]。这些新的异步语言特性仅仅是建立在现有任务并行库之上的语法糖。我之前已经处理了从MyFunction
到MyFunctionAsync
的转换,所以让我们以并排的方式对我们原始的相关同步代码进行“异步迁移”,并重点介绍所需的代码更改。
同步代码 | 异步代码 |
---|---|
void _buttonSynch_Click(object sender, EventArgs e)
{
DisableUIControls(true);
_textBox.Text = string.Format(
"Using synchroneous calls: " +
"Main thread Id is {0}\r\n",
Thread.CurrentThread.ManagedThreadId);
Stopwatch duration = Stopwatch.StartNew();
foreach (string arg in _myArguments)
{
try
{
_textBox.Text += string.Format(
"Result on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
MyFunction(arg));
}
catch (Exception ex)
{
_textBox.Text += string.Format(
"Exception thrown on thread " +
"{0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
ex.Message);
}
}
duration.Stop();
_textBox.Text += string.Format(
"The UI thread {0} was " +
"blocked for {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
DisableUIControls(false);
}
|
async void ExecuteAsyncSerial1()
{
DisableUIControls(true);
_textBox.Text = string.Format(
"Using async serial 1: " +
"Main thread Id is {0}\r\n",
Thread.CurrentThread.ManagedThreadId);
Stopwatch duration = Stopwatch.StartNew();
foreach (string arg in _myArguments)
{
try
{
_textBox.Text += string.Format(
"Result on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
await MyFunctionAsync(arg););
}
catch (Exception ex)
{
_textBox.Text += string.Format(
"Exception thrown on thread " +
"{0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
ex.Message);
}
}
duration.Stop();
_textBox.Text += string.Format(
"Final callback on " +
"thread {0} after {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
DisableUIControls(false);
}
|
为了完成“转换”,请不要忘记在包含此新代码的方法前面加上async
关键字。接下来,让我们看看如何从窗口处理程序调用它,并显示其余的相关调用代码:
private void _buttonAsyncSerial1_Click(object sender, EventArgs e)
{
Stopwatch duration = Stopwatch.StartNew();
ExecuteAsyncSerial1();//lasts only miliseconds
_textBox.Text += string.Format(
"The UI thread {0} was blocked for {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
}
此时,有些人可能会觉得“async”语法是任何同步到异步转换的“必杀技”。让我们来看看运行此代码所获得的结果:
Using async serial 1: Main thread Id is 1
The UI thread 1 was blocked for 18 ms
Result on thread 1:"MyFunction with argument '7' lasted 6999 ms on thread 3"
Result on thread 1:"MyFunction with argument '4' lasted 3999 ms on thread 4"
Exception thrown on thread 1:"Exception 'Input string was not in a correct format.'
thrown for argument 'bad'.It lasted 5 ms on thread 4"
Result on thread 1:"MyFunction with argument '3' lasted 2999 ms on thread 3"
Final callback on thread 1 after 14022 ms
UI仍然响应,因为处理程序在UI线程上执行得非常快(18毫秒),并且我们从async模式生成的每个单独的Task
收到了完成通知。作为奖励,非UI到UI的编组是完全透明的,这与之前的TPL实现不同。但深入分析执行时间,您会注意到完成所有任务所需的时间比预期要长。很明显,尽管任务与UI线程并行执行,但它们之间是串行执行的。尽管它们明显使用了不同的线程(如其ThreadId
3和4所示)。考虑到线程是昂贵的资源,而且我们没有充分利用它们,这种情况并不好。如果用汽油发动机来类比,听起来就像这样:与其让所有气缸在每个周期都点火,不如我们最终每个周期只有一个随机气缸点火……效率不高,一个气缸就能完成同样的工作!一个更有效的实现应该是只让一个任务异步执行所有调用,如下所示。这意味着只需将UI处理程序中的ExecuteAsyncSerial1
替换为下面的ExecuteAsyncSerial2
。
async void ExecuteAsyncSerial2()
{
DisableUIControls(true);
_textBox.Text = string.Format(
"Using async serial 2: Main thread Id is {0}\r\n",
Thread.CurrentThread.ManagedThreadId);
Stopwatch duration = Stopwatch.StartNew();
Task tsk = Task.Factory.StartNew(() =>
{
foreach (string arg in _myArguments)
{
try
{
string result = MyFunction(arg);
_textBox.Text += string.Format(
"Result on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
result);
}
catch (Exception ex)
{
_textBox.Text += string.Format(
"Exception thrown on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
ex.Message);
}
}
});
await tsk;
duration.Stop();
_textBox.Text += string.Format(
"Final callback on thread {0} after {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
DisableUIControls(false);
}
请注意,我们从MyFunctionAsync
回到了MyFunction
,但是第二次“async”代码转换并不像第一次那样“容易”。然而,结果看起来相似,并表明我们这次不会创建不必要的线程:
The UI thread 1 was blocked for 15 ms
Result on thread 3:"MyFunction with argument '7' lasted 6999 ms on thread 3"
Result on thread 3:"MyFunction with argument '4' lasted 3999 ms on thread 3"
Exception thrown on thread 3:"Exception 'Input string was not in
a correct format.' thrown for argument 'bad'.It lasted 5 ms on thread 3"
Result on thread 3:"MyFunction with argument '3' lasted 2999 ms on thread 3"
Final callback on thread 1 after 14013 ms
虽然即将推出的“async”语法似乎比TPL类更容易使用,但我们在第二个目标上显然有所不足:“代码应并行且独立地执行其操作”。如果您能想到一个“简单”的方法来实现这个目标,请告诉我。那么接下来的问题是,“这种新语法是否足以解决更复杂的异步场景?”
如何启动多个并行任务并等待它们
事实证明,您仍然可以“await”并实现所有原始目标,但代码将不再与原始同步代码有太多相似之处。也许有人会证明我是错的,但在此期间,这是我基于“async”语言特性的练习的可能解决方案:
private void _buttonAsyncParallel1_Click(object sender, EventArgs e)
{
DisableUIControls(true);
_textBox.Text = string.Format(
"Using async parallel 1: Main thread Id is {0}\r\n",
Thread.CurrentThread.ManagedThreadId);
Stopwatch duration = Stopwatch.StartNew();
ExecuteAsyncParallel1();//or ExecuteAsyncParallel2() will last only miliseconds
_textBox.Text += string.Format(
"The UI thread {0} was blocked for {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
}
async void ExecuteAsyncParallel1()
{
Stopwatch duration = Stopwatch.StartNew();
//without .ToArray() or .ToList() will wait after each task
List<Task<string>> tasks = _myArguments.Select(
arg => MyFunctionAsync(arg)).ToList();
// spawn all the threads
tasks.ForEach(async (task) =>
{
try
{
_textBox.Text += string.Format(
"Result on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
await task);
}
catch (Exception ex)
{
_textBox.Text += string.Format(
"Exception thrown on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
ex.Message);
}
});
//wait to finish all tasks
try
{
await TaskEx.WhenAll(tasks);
}
catch //discard the exception(s)
{
}
duration.Stop();
_textBox.Text += string.Format("Final callback on thread {0} after {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
DisableUIControls(false);
}
第一种方法是单独创建和等待每个任务,然后再次等待所有任务完成。这与我之前的任务并行库实现非常相似。现在可能是再次欣赏“async”语法荣耀的绝佳机会,因为这段代码看起来比其TPL对应项更简单。此外,我们可以安全地假设编译器自动生成了后台代码来创建任务续集和非UI到UI的上下文切换。您可能已经注意到上面代码中使用了TaskEx
类。该类可能仅适用于CTP,并且下一版.NET将弃用它,其所有静态方法都将最终进入Task
类。以下是结果:
Using async parallel 1: Main thread Id is 1
The UI thread 1 was blocked for 42 ms
Exception thrown on thread 1:"Exception 'Input string was not in a correct
format.' thrown for argument 'bad'.It lasted 6 ms on thread 5"
Result on thread 1:"MyFunction with argument '4' lasted 3999 ms on thread 4"
Result on thread 1:"MyFunction with argument '3' lasted 3000 ms on thread 5"
Result on thread 1:"MyFunction with argument '7' lasted 6999 ms on thread 3"
Final callback on thread 1 after 7022 ms
正如您所见,我们通过这次真正并行执行所有任务来治愈了我之前异步实现的弊病,因此我最初设定的所有目标都实现了。
但这个解决方案并非独一无二。我找到了另一个,也许有些聪明人会提出其他想法。这次我从列表中移除任务,因为它们已完成并通知UI线程。
async void ExecuteAsyncParallel2()
{
Stopwatch duration = Stopwatch.StartNew();
List<Task<string>> tasks = _myArguments.Select(
arg => MyFunctionAsync(arg)).ToList();
//eliminate threads that have completed
while (tasks.Count > 0)
{
Task<string> tsk = null;
try
{
tsk = await TaskEx.WhenAny(tasks);
_textBox.Text += string.Format(
"Result on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
await tsk);
}
catch (Exception ex)
{
_textBox.Text += string.Format(
"Exception thrown on thread {0}:\"{1}\"\r\n",
Thread.CurrentThread.ManagedThreadId,
ex.Message);
}
finally
{
tasks.Remove(tsk);
}
}
duration.Stop();
_textBox.Text += string.Format(
"Final callback on thread {0} after {1} ms\r\n",
Thread.CurrentThread.ManagedThreadId,
duration.ElapsedMilliseconds);
DisableUIControls(false);
return;
}
只需在UI处理程序的调用中将ExecuteAsyncParallel1()
替换为新的ExecuteAsyncParallel2()
,结果将如下所示:
Using async parallel 2: Main thread Id is 1
The UI thread 1 was blocked for 50 ms
Exception thrown on thread 1:"Exception 'Input string was not in a correct
format.' thrown for argument 'bad'.It lasted 6 ms on thread 5"
Result on thread 1:"MyFunction with argument '4' lasted 3999 ms on thread 3"
Result on thread 1:"MyFunction with argument '3' lasted 2999 ms on thread 5"
Result on thread 1:"MyFunction with argument '7' lasted 6999 ms on thread 4"
Final callback on thread 1 after 7019 ms
正如预期的那样,与最新的实现一样,它实现了我最初设定的所有3个目标,但它与原始同步代码的距离更远了。此外,这个任务移除实现看起来与您在之前的BackgroundWorker实现中看到的递减计数器非常相似……旧想法变成新想法。我不是语言设计者,但我希望微软能为AwaitAny
和AwaitAll
之类的东西提供语法支持,以实现完整的开发人员故事和与现有Task
“WhenXXX”方法WhenAny
[^]和WhenAll
[^]的对称性。
值得关注的点
我希望本文展示了使用各种技术使UI应用程序更具响应性的喜悦和陷阱。似乎.NET提供了许多处理并发的方法,有些人可能会说太多了,谁知道未来的版本会带来什么。我使用了WinForms,但在WPF中重现相同的结果应该足够容易,因为delegate
、BackgroundWorker
、TPL或新的异步语言语法不依赖于特定的UI技术。我包含了当前AsyncCtpLibrary.dll,它允许您在.NET 4.0运行时上运行可执行文件,但对于Visual Studio 2010的构建,您仍然需要完整的CTP下载 [^]。如果您使用VS2010的后继版本之一,您可以从项目引用中删除此库,摆脱TaskEx
等CTP伪影,并改用更新的.NET Framework提供的程序集的引用。
历史
- 版本1.0:这是第一个版本。
- 版本1.1:添加了异步委托主题和支持代码。