Task.WhenAll()一次只执行2个线程?

时间:2014-07-25 06:01:04

标签: c# caching locking task semaphore

在这个问题中,我试图缓存一个值,让我们称之为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次(通过),而不是&gt; 1次(失败)。

输出:

Getting new foo
Foo retrieved while waiting
Foo already retrieved
Foo already retrieved
Foo already retrieved

但是你可以从测试的输出中看出它并不像我期望的那样。看起来好像只有两个线程同时执行,而其他线程等到前两个线程完成甚至输入 GetFoo()方法。

为什么会这样?是因为我在VS单元测试中运行吗?请注意,我的测试仍然通过,但不是我预期的方式。我怀疑VS单元测试中的线程数有一些限制。

3 个答案:

答案 0 :(得分:5)

Task.WhenAll()没有启动任务 - 它只是等待

同样,调用async方法实际上并不强制并行化 - 它不会引入新线程或类似的东西。如果出现以下情况,您只能获得新的主题:

  • 等待尚未完成的事情,并且您的同步上下文会在新线程上安排延续(例如,它在WinForms上下文中不会执行;它只会重复使用UI线程)
  • 您明确使用Task.Run,任务计划程序创建一个新线程来运行它。 (当然,这可能不需要。)
  • 您明确地开始新线程。

老实说,在异步方法中使用阻止 Semaphore方法对我来说是非常错误的。你似乎并没有真正接受异步的想法......我还没有试图分析完全你的代码将要做什么,但我认为你需要阅读详细了解async如何运作,以及如何最好地使用它。

答案 1 :(得分:2)

您的问题似乎与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;
}

答案 2 :(得分:1)

 await Task.Delay(TimeSpan.FromSeconds(5));

这应该允许其他任务运行,但我怀疑它们被阻止了:

semaphore.WaitOne();

混合并发样式(在这种情况下使用Tasks和手动控制与同步对象)很难做到正确。

(您似乎试图通过将多个并发任务全部合并来获得相同的值:这似乎有点过分。)

默认情况下,.NET会将Task并发限制为可用的(逻辑)CPU核心数,我怀疑你的系统有两个。