我在C#.NET应用程序中有关于async \ await的问题。我实际上是在基于Kinect的应用程序中尝试解决这个问题但是为了帮助我说明,我已经制作了这个类似的例子:
想象一下,我们有一个名为timer1的Timer,它设置了Timer1_Tick事件。现在,我对该事件采取的唯一操作是使用当前日期时间更新UI。
private void Timer1_Tick(object sender, EventArgs e)
{
txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
}
这很简单,我的UI每隔几百分钟更新一次,我很高兴看到时间流逝。
现在想象一下,我也想用同样的方法计算前500个素数:
private void Timer1_Tick(object sender, EventArgs e)
{
txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
}
private List<int> WorkOutFirstNPrimeNumbers(int n)
{
List<int> primeNumbersList = new List<int>();
txtPrimeAnswers.Clear();
int counter = 1;
while (primeNumbersList.Count < n)
{
if (DetermineIfPrime(counter))
{
primeNumbersList.Add(counter);
}
counter++;
}
return primeNumbersList;
}
private bool DetermineIfPrime(int n)
{
for (int i = 2; i < n; i++)
{
if (n % i == 0)
{
return false;
}
}
return true;
}
private void PrintPrimeNumbersToScreen(List<int> primeNumbersList)
{
foreach (int primeNumber in primeNumbersList)
{
txtPrimeAnswers.Text += String.Format("The value {0} is prime \r\n", primeNumber);
}
}
这是我遇到问题的时候。计算素数的密集方法会阻止事件处理程序运行 - 因此我的计时器文本框现在每30秒左右更新一次。
我的问题是,如何在遵守以下规则的同时解决这个问题:
我试图用async / await做一些事情并让我的素数计算函数返回一个Task&gt;但还没有设法解决我的问题。 Timer1_Tick事件中的await调用似乎仍然阻塞,阻止了进一步执行处理程序。
任何帮助都会很高兴 - 我非常擅长接受正确答案:)
更新:我非常感谢@sstan能够为这个问题提供一个简洁的解决方案。但是,我在将这个应用到基于Kinect的真实情况时遇到了麻烦。由于我有点担心这个问题太具体,我在这里将跟进作为一个新问题发布:Kinect Frame Arrived Asynchronous
答案 0 :(得分:2)
可能不是最好的解决方案,但它会起作用。您可以创建2个单独的计时器。您的第一个计时器的Tick
事件处理程序只需要处理您的txtTimerValue
文本框。它可以保持原来的方式:
private void Timer1_Tick(object sender, EventArgs e)
{
txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
}
对于您的第二个计时器Tick
事件处理程序,请像这样定义Tick
事件处理程序:
private async void Timer2_Tick(object sender, EventArgs e)
{
timer2.Stop(); // this is needed so the timer stops raising Tick events while this one is being awaited.
txtPrimeAnswers.Text = await Task.Run(() => {
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
return ConvertPrimeNumbersToString(primeNumbersList);
});
timer2.Start(); // ok, now we can keep ticking.
}
private string ConvertPrimeNumbersToString(List<int> primeNumbersList)
{
var primeNumberString = new StringBuilder();
foreach (int primeNumber in primeNumbersList)
{
primeNumberString.AppendFormat("The value {0} is prime \r\n", primeNumber);
}
return primeNumberString.ToString();
}
// the rest of your methods stay the same...
您会注意到我已将PrintPrimeNumbersToScreen()
方法更改为ConvertPrimeNumbersToString()
(其余方法保持不变)。更改的原因是您确实希望最小化UI线程上的工作量。所以最好从后台线程准备字符串,然后只需对UI线程上的txtPrimeAnswers
文本框进行简单的分配。
编辑:可以与单个计时器一起使用的另一种选择
这是另一个想法,但只有一个计时器。这里的想法是你的Tick
偶数处理程序将继续定期执行并每次更新你的计时器值文本框。但是,如果素数计算已经在后台进行,则事件处理程序将跳过该部分。否则,它将开始素数计算,并在完成后更新文本框。
// global variable that is only read/written from UI thread, so no locking is necessary.
private bool isCalculatingPrimeNumbers = false;
private async void Timer1_Tick(object sender, EventArgs e)
{
txtTimerValue.Text = DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture);
if (!this.isCalculatingPrimeNumbers)
{
this.isCalculatingPrimeNumbers = true;
try
{
txtPrimeAnswers.Text = await Task.Run(() => {
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
return ConvertPrimeNumbersToString(primeNumbersList);
});
}
finally
{
this.isCalculatingPrimeNumbers = false;
}
}
}
private string ConvertPrimeNumbersToString(List<int> primeNumbersList)
{
var primeNumberString = new StringBuilder();
foreach (int primeNumber in primeNumbersList)
{
primeNumberString.AppendFormat("The value {0} is prime \r\n", primeNumber);
}
return primeNumberString.ToString();
}
// the rest of your methods stay the same...
答案 1 :(得分:2)
你应该避免使用async / await(尽管它们有多好)因为微软的Reactive Framework(Rx) - NuGet要么是“Rx-WinForms”还是“Rx-WPF” - 是一种更好的方法。
这是Windows窗体解决方案所需的代码:
private void Form1_Load(object sender, EventArgs e)
{
Observable
.Interval(TimeSpan.FromSeconds(0.2))
.Select(x => DateTime.Now.ToString("hh:mm:ss.FFF", CultureInfo.InvariantCulture))
.ObserveOn(this)
.Subscribe(x => txtTimerValue.Text = x);
txtPrimeAnswers.Text = "";
Observable
.Interval(TimeSpan.FromSeconds(0.2))
.Select(n => (int)n + 1)
.Where(n => DetermineIfPrime(n))
.Select(n => String.Format("The value {0} is prime\r\n", n))
.Take(500)
.ObserveOn(this)
.Subscribe(x => txtPrimeAnswers.Text += x);
}
就是这样。非常简单。这一切都发生在后台线程上,然后被编组回UI。
以上内容应该是相当自我解释的,但如果您需要进一步解释,请大声说出来。
答案 2 :(得分:1)
因此,您希望在不等待结果的情况下启动任务。当任务完成计算时,它应该更新UI。
您的UI在长时间操作期间没有响应的原因是因为您没有声明事件处理程序是异步的。查看结果的最简单方法是为按钮创建事件处理程序:
同步 - 在执行期间阻止了用户界面:
private void Button1_clicked(object sender, EventArgs e)
{
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
}
异步 - UI在执行期间响应:
private async void Button1_clicked(object sender, EventArgs e)
{
List<int> primeNumbersList = await Task.Run( () => WorkOutFirstNPrimeNumbers(500));
PrintPrimeNumbersToScreen(primeNumbersList);
}
请注意差异:
注意:
<TResult
&gt;而不是TResult。<TResult
&gt;是一个TResult。<TResult
&gt;。问题是您的计时器比计算速度快。如果在先前的计算未完成时报告新的滴答,您想要什么?
(1)启动任务,但不要等待它。
private void Button1_clicked(object sender, EventArgs e)
{
Task.Run ( () =>
{ List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
});
}
(2)如果任务仍然忙,则忽略勾号:
Task primeCalculationTask = null;
private void Button1_clicked(object sender, EventArgs e)
{
if (primeCalculationTask == null || primeCalculationTask.IsCompleted)
{ // previous task finished. Stat a new one
Task.Run ( () =>
{ List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
});
}
}
(3)启动连续计算的任务
private void StartTask(CancellationToken token)
{
Task.Run( () =>
{
while (!token.IsCancelRequested)
{
List<int> primeNumbersList = WorkOutFirstNPrimeNumbers(500);
PrintPrimeNumbersToScreen(primeNumbersList);
}
})
}