Task.WhenAll()一次只执行2个线程
本文关键字:执行 2个 线程 WhenAll Task 一次 | 更新日期: 2023-09-27 18:14:48
在这个问题中,我试图缓存一个值,让我们称之为foo。如果值没有缓存,那么检索需要一段时间。
我的问题不是实现,而是测试。
为了测试它,我使用Task.WhenAll()同时启动5个任务来获取缓存的值。第一个线程进入锁并异步检索值,而其他4个线程应该等待锁。等待之后,它们应该一个接一个地重新检查缓存的值,发现它已经被缓存它的第一个线程检索到,然后返回它,而不进行第二次检索。
[TestClass]
public class Class2
{
private readonly Semaphore semaphore = new Semaphore(1, 1);
private bool? foo;
private async Task<bool> GetFoo()
{
bool fooValue;
// Atomic operation to get current foo
bool? currentFoo = this.foo;
if (currentFoo.HasValue)
{
Console.WriteLine("Foo already retrieved");
fooValue = currentFoo.Value;
}
else
{
semaphore.WaitOne();
{
// Atomic operation to get current foo
currentFoo = this.foo;
if (currentFoo.HasValue)
{
// Foo was retrieved while waiting
Console.WriteLine("Foo retrieved while waiting");
fooValue = currentFoo.Value;
}
else
{
// Simulate waiting to get foo value
Console.WriteLine("Getting new foo");
await Task.Delay(TimeSpan.FromSeconds(5));
this.foo = true;
fooValue = true;
}
}
semaphore.Release();
}
return fooValue;
}
[TestMethod]
public async Task Test()
{
Task[] getFooTasks = new[] {
this.GetFoo(),
this.GetFoo(),
this.GetFoo(),
this.GetFoo(),
this.GetFoo(),
};
await Task.WhenAll(getFooTasks);
}
}
在我的实际测试和生产代码中,我通过一个接口检索值,并使用Moq模拟该接口。在测试结束时,我验证接口只被调用了1次(通过),而不是> 1次(失败)。
输出:Getting new foo
Foo retrieved while waiting
Foo already retrieved
Foo already retrieved
Foo already retrieved
然而,您可以从测试的输出中看到,它不像我期望的那样。看起来好像只有两个线程同时执行,而其他线程等待直到前两个线程完成,甚至进入 GetFoo()方法。
为什么会发生这种情况?是因为我在VS单元测试中运行它吗?请注意,我的测试仍然通过了,但不是以我期望的方式。我怀疑在VS单元测试中有一些线程数量的限制。
Task.WhenAll()
不启动任务-它只是等待。
async
方法实际上并不强制并行化——它不会引入一个新的线程,或者类似的东西。只有当:
- 你等待一些还没有完成的事情,你的同步上下文计划在一个新线程上继续(这在WinForms上下文中不会这样做,例如;它只会重用UI线程)
- 您显式地使用
Task.Run
,任务调度器创建一个新线程来运行它。(当然,它可能不需要。) - 你显式地启动一个新线程。
老实说,在异步方法中使用阻塞 Semaphore
方法对我来说是非常错误的。你似乎并没有真正接受异步的概念……我还没有试着分析到底你的代码要做什么,但我认为你需要更多地了解async
是如何工作的,以及如何最好地使用它。
你的问题似乎在于semaphore.WaitOne()
一个async
方法将同步运行,直到它到达第一个await
。在您的代码中,第一个await
只在WaitOne
发出信号之后。事实上,一个方法是async
并不意味着它在多个线程上运行,它通常意味着相反的情况。
要绕过这个问题,使用SemaphoreSlim.WaitAsync
,这样调用线程将放弃控制,直到信号量发出完成信号
public class Class2
{
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
private bool? foo;
private async Task<bool> GetFoo()
{
bool fooValue;
// Atomic operation to get current foo
bool? currentFoo = this.foo;
if (currentFoo.HasValue)
{
Console.WriteLine("Foo already retrieved");
fooValue = currentFoo.Value;
}
else
{
await semaphore.WaitAsync();
{
// Atomic operation to get current foo
currentFoo = this.foo;
if (currentFoo.HasValue)
{
// Foo was retrieved while waiting
Console.WriteLine("Foo retrieved while waiting");
fooValue = currentFoo.Value;
}
else
{
// Simulate waiting to get foo value
Console.WriteLine("Getting new foo");
await Task.Delay(TimeSpan.FromSeconds(5));
this.foo = true;
fooValue = true;
}
}
semaphore.Release();
}
return fooValue;
}
await Task.Delay(TimeSpan.FromSeconds(5));
这应该允许其他任务运行,但我怀疑它们被阻塞了:
semaphore.WaitOne();
混合并发风格(在这种情况下使用任务和手动控制与同步对象)是很难得到正确的。
(您似乎试图通过将多个并发任务全部池化来获得相同的值:这似乎有些过度)
默认情况下,. net将Task
并发限制为可用的(逻辑)CPU内核数,我怀疑您的系统有两个。