任务似乎相互阻塞
本文关键字:任务 | 更新日期: 2023-09-27 17:57:13
>我有一个名为WaitForAction的方法,它接受一个Action委托并在新任务中执行它。该方法将一直阻塞,直到任务完成或超时到期。它使用手动重置事件来等待超时/完成。
下面的代码演示了在多线程环境中测试该方法的尝试。
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");
}
}
}
}
由于操作不执行任何操作,我希望通过调用 10 次 Foo.Bar 启动的 10 个任务在超时内完成。有时会发生这种情况,但通常程序需要 2 秒才能执行,并报告只有 2 个 Foo 实例"完成"而没有错误。换句话说,对 WaitForAction 的 8 次调用已超时。
我假设 WaitForAction 是线程安全的,因为任务提供的线程上的每个调用都有自己的堆栈。我通过记录每个调用的线程 ID 和等待句柄 ID 或多或少地证明了这一点。
我意识到这个代码是一个愚蠢的例子,但我对原理感兴趣。任务计划程序是否可以将运行操作委托的任务调度到已在等待另一个操作完成的同一线程池线程?还是我错过了其他事情?
Task.Factory
默认使用该ThreadPool
。每次调用 WaitHandle.WaitOne
时,都会阻塞一个工作线程。.Net 4/4.5 线程池从少量工作线程开始,具体取决于您的硬件平台(例如,我的机器上有 4 个),它会定期重新评估池大小(我相信是每 1 秒一次),必要时创建新的工作线程。
由于程序会阻塞所有工作线程,并且线程池的增长速度不够快,因此您的 waithandle 会超时。
要确认这一点,您可以 1) 增加超时或 2) 通过将以下行添加到程序开头来增加起始线程池大小:
ThreadPool.SetMinThreads(32, 4);
然后,您应该会看到超时不会发生。
我相信你的问题比其他任何事情都更具学术性,但你可以在这里阅读有关任务超时机制的更好实现的信息,例如
var task = Task.Run(someAction);
if (task == await Task.WhenAny(task, Task.Delay(millisecondsTimeout)))
await task;
else
throw new TimeoutException();