我有一个名为WaitForAction的方法,它接受一个Action委托并在一个新的Task中执行它。该方法将阻塞,直到任务完成或超时到期。它使用ManualResetEvent来等待超时/完成。
以下代码显示了尝试在多线程环境中测试该方法。
class Program
{
public static void Main()
{
List<Foo> list = new List<Foo>();
for (int i = 0; i < 10; i++)
{
Foo foo = new Foo();
list.Add(foo);
foo.Bar();
}
SpinWait.SpinUntil(() => list.Count(f => f.finished || f.failed) == 10, 2000);
Debug.WriteLine(list.Count(f => f.finished));
}
}
public class Foo
{
public volatile bool finished = false;
public volatile bool failed = false;
public void Bar()
{
Task.Factory.StartNew(() =>
{
try
{
WaitForAction(1000, () => { });
finished = true;
}
catch
{
failed = true;
}
});
}
private void WaitForAction(int iMsToWait, Action action)
{
using (ManualResetEvent waitHandle = new ManualResetEvent(false))
{
Task.Factory.StartNew(() =>
{
action();
waitHandle.SafeSet();
});
if (waitHandle.SafeWaitOne(iMsToWait) == false)
{
throw new Exception("Timeout");
}
}
}
}
由于Action无所作为,我希望通过在超时内调用Foo.Bar 10次来完成10个任务。有时会发生这种情况,但通常程序需要2秒才能执行,并且报告只有2个Foo&#39;的实例已经完成。没有错误。换句话说,8次调用WaitForAction已经超时。
我假设WaitForAction是线程安全的,因为Task提供的线程上的每个调用都有自己的堆栈。我或多或少通过记录每个调用的线程ID和等待句柄ID来证明这一点。
我意识到这个代码是一个愚蠢的例子,但我对这个原则很感兴趣。任务调度程序是否可以将运行操作委托的任务调度到已经等待另一个操作完成的同一个线程池线程?或者还有其他事情发生了我错过了吗?
答案 0 :(得分:2)
Task.Factory
默认使用ThreadPool
。每次调用WaitHandle.WaitOne
,都会阻止工作线程。 .Net 4 / 4.5线程池以少量工作线程开始,具体取决于您的硬件平台(例如,我的机器上有4个),并且它会定期重新评估池大小(我相信它是每1秒),创建新的工人如有必要。
由于您的程序阻塞了所有工作线程,并且线程池的增长速度不够快,因此您的等待句会超时。
要确认这一点,您可以1)增加超时或2)通过将以下行添加到程序的开头来增加开始线程池大小:
ThreadPool.SetMinThreads(32, 4);
那么你应该看到没有发生超时。
我相信你的问题比其他任何问题更具学术性,但你可以阅读更好的任务超时机制here的实现,例如
var task = Task.Run(someAction);
if (task == await Task.WhenAny(task, Task.Delay(millisecondsTimeout)))
await task;
else
throw new TimeoutException();