从BindingSource.PositionChanged运行时的并行任务块

时间:2012-06-26 14:44:52

标签: .net winforms c#-4.0 task-parallel-library bindingsource

一项非常简单的任务:

Task.Factory.StartNew(() =>
{
    Thread.Sleep(5000);
}).ContinueWith(_ =>
{
    lblStatus.Text = "Done";
}, TaskScheduler.FromCurrentSynchronizationContext());

Form_Load()运行时按预期执行,但从与BindingSource相关的任何事件运行时,它会阻塞5秒。

我是否遗漏了BindingSources的一些内容?我使用的是.NET 4。

2 个答案:

答案 0 :(得分:1)

您的代码会阻塞ThreadPool线程5秒钟。如果为大量事件执行此代码,则可能会耗尽所有线程池线程并有效阻止应用程序,直到所有Sleep语句完成。

两个代码示例都使用默认线程调度程序执行。不同之处在于第二个示例使用TaskCreationOptions.LongRunning来指示调度程序创建新线程而不是等待池线程。这可能会克服最初的问题,它仍然不是正确的解决方案,因为您仍在浪费线程并且可能没有任何线程可用于其他任务。

正确的实现是使用在定时器到期时将发出信号的TaskSource。这样你就不会阻塞任何线程。

C#5已经使用Task.Delay方法支持此功能。如果对Visual Studio 2012使用async targeting pack或在2010年使用Async v3 CTP

,则可以在.NET 4.0中使用此功能

您还可以在ParallelExtensionExtras库中找到类似的方法。 TaskFactory.StartNewDelayed扩展方法的工作方式几乎相同。示例代码from Stephen Toub's article提供了简化版本:

public static Task StartNewDelayed(int millisecondsDelay, Action action) 
{ 
// Validate arguments 
if (millisecondsDelay < 0) 
    throw new ArgumentOutOfRangeException("millisecondsDelay"); 
if (action == null) throw new ArgumentNullException("action"); 

// Create a trigger used to start the task 
var tcs = new TaskCompletionSource<object>(); 

// Start a timer that will trigger it 
var timer = new Timer( 
    _ => tcs.SetResult(null), null, millisecondsDelay, Timeout.Infinite); 

// Create and return a task that will be scheduled when the trigger fires. 
return tcs.Task.ContinueWith(_ => 
{
    timer.Dispose();
    action();
}); 
}

使用ParallelExtensionsExtras中的版本,您可以按如下方式重写代码:

Task.Factory.StartNewDelayed(5000).ContinueWith(_ =>
{
    lblStatus.Text = "Done";
}, TaskScheduler.FromCurrentSynchronizationContext());

修改

看起来实际的代码毕竟没有Thread.Sleep。它执行一些繁重的数据库相关操作。但效果是一样的。在每个BindingSource事件之后启动一个新任务可能会导致许多正在运行的任务导致线程池耗尽。

一个解决方案是再次使用带有TaskFactory.StartNew(Action,TaskCreationOptions)覆盖的TaskCreationOptions.LongRunning标志来指示调度程序创建更多线程。

更好的解决方案是异步执行数据库操作,使用BeginExecuteXXX,EndExecuteXXX方法结合TaskFactory.FromAsync将异步调用转换为任务。这样,数据库操作就不会阻塞任何线程。

你可以这样写:

Task<SqlDataReader> task = Task<SqlDataReader>.Factory.FromAsync(
    cmd.BeginExecuteReader(CommandBehavior.CloseConnection),
    cmd.EndExecuteReader)
.ContinueWith(reader=>
{
    //do some processing
    reader.Close();
});
.ContinueWith(_ => 
{
    lblStatus.Text="Done";
},TaskScheduler.FromCurrentSynchronizationContext());

以异步方式读取和处理数据,并在处理完成后更新UI。

答案 1 :(得分:0)

我通过为任务分配默认任务调度程序来解决这个问题。最终形式成为:

Task.Factory.StartNew(() =>
{
    Thread.Sleep(5000);
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default).ContinueWith(_ =>
{
    lblStatus.Text = "Done";
}, TaskScheduler.FromCurrentSynchronizationContext());

我不是C#的专家,所以我真的不知道为什么会这样。