【问题标题】:Task fired again after WaitAll在 WaitAll 之后再次触发任务
【发布时间】:2014-04-19 02:03:02
【问题描述】:

在 Linq Select 中使用 HttpClient.GetAsync 或其任何异步方法或任何 BCL 异步方法可能会导致一些奇怪的两次拍摄。

这里是一个单元测试用例:

[TestMethod]
public void TestTwiceShoot()
{
    List<string> items = new List<string>();
    items.Add("1");
    int k = 0;

    var tasks = items.Select(d =>
    {
        k++;
        var client = new System.Net.Http.HttpClient();
        return client.GetAsync(new Uri("http://testdevserver.ibs.local:8020/prestashop/api/products/1"));
    });

    Task.WaitAll(tasks.ToArray());

    foreach (var r in tasks)
    {

    }

    Assert.AreEqual(1, k);           
}

测试将失败,因为 k 为 2。不知何故,程序运行了两次触发 GetAsync 的委托。为什么?

如果我删除foreach (var r in tasks),测试通过。为什么?

[TestMethod]
public void TestTwiceShoot()
{
    List<string> items = new List<string>();
    items.Add("1");
    int k = 0;

    var tasks = items.Select(d =>
    {
        k++;
        var client = new System.Net.Http.HttpClient();
        return client.GetAsync(new Uri("http://testdevserver.ibs.local:8020/prestashop/api/products/1"));
    });

    Task.WaitAll(tasks.ToArray());

    Assert.AreEqual(1, k);

}

如果我使用foreach 而不是items.Select,则测试通过。为什么?

[TestMethod]
public void TestTwiceShoot()
{
    List<string> items = new List<string>();
    items.Add("1");
    int k = 0;

    var tasks = new List<Task<System.Net.Http.HttpResponseMessage>>();
    foreach (var item in items)
    {
        k++;
        var client = new System.Net.Http.HttpClient();
        tasks.Add( client.GetAsync(new Uri("http://testdevserver.ibs.local:8020/prestashop/api/products/1")));
    };

    Task.WaitAll(tasks.ToArray());

    foreach (var r in tasks)
    {

    }

    Assert.AreEqual(1, k);

}

显然items.Select 返回的枚举器与返回的Task 对象并不能很好地相处,只要我走过枚举器,代表就会再次被解雇。

此测试通过。

[TestMethod]
public void TestTwiceShoot()
{
    List<string> items = new List<string>();
    items.Add("1");
    int k = 0;

    var tasks = items.Select(d =>
    {
        k++;
        var client = new System.Net.Http.HttpClient();
        return client.GetAsync(new Uri("http://testdevserver.ibs.local:8020/prestashop/api/products/1"));

    });


    var tasksArray = tasks.ToArray();
    Task.WaitAll(tasksArray);

    foreach (var r in tasksArray)
    {

    }

    Assert.AreEqual(1, k);

}

Scott 提到 Select 在遍历枚举器时可能会再次运行,但是,此测试通过

[TestMethod]
public void TestTwiceShoot()
{
    List<string> items = new List<string>();
    items.Add("1");
    int k = 0;

    var tasks = items.Select(d =>
    {
        k++;
        return int.Parse(d);

    });

    foreach (var r in tasks)
    {

    };

    Assert.AreEqual(1, k);

}

我猜 Linq SelectTask 有一些特殊处理。

毕竟,在 Linq 中触发多个异步方法并检查WaitAll 之后的结果的好方法是什么?

【问题讨论】:

  • 您没有调用items.ToArray() 在您的测试方法中实现集合,因此迭代只发生一次(通过foreach)。斯科特的答案是正确的,你的测试不是。
  • 您的第一个测试(使用任务)也不能代表您的实际代码,因为在测试中您正在迭代 tasks.ToArray() 生成的数组(因此实际的 tasks 可枚举仅迭代一次 - 通过调用.ToArray()),而在原始代码示例中,您首先通过调用.ToArray() 迭代tasks,然后通过foreach 语句进行迭代。

标签: c# linq asynchronous parallel-processing task


【解决方案1】:

这是因为tasksIEnumerable&lt;Task&gt; 并且每次枚举列表时它都会重新运行.Select() 操作。目前,您遍历列表两次,一次是调用.ToArray(),一次是传递给foreach

要解决此问题,只需像您一样使用.ToArray(),但将其向上移动。

    var tasks = items.Select(d =>
    {
        k++;
        var client = new System.Net.Http.HttpClient();
        return client.GetAsync(new Uri("http://testdevserver.ibs.local:8020/prestashop/api/products/1"));

    }).ToArray(); //This makes tasks a "Task[]" instead of a IEnumerable<Task>.

    Task.WaitAll(tasks);

    foreach (var r in tasks)
    {

    };

像发生在你身上的事情就是为什么微软建议在你编写 Linq 语句时它们没有任何副作用(比如递增 k),因为很难判断语句将运行多少次,尤其是如果结果 IEnumerable&lt;T&gt; 通过作为结果返回或传递给新函数而超出您的控制范围。

【讨论】:

    【解决方案2】:

    我认为问题在于我对枚举如何工作的误解。这些测试通过了:

            [TestMethod]
        public void TestTwiceShoot()
        {
            List<string> items = new List<string>();
            items.Add("1");
            int k = 0;
    
            var tasks = items.Select(d =>
            {
                k++;
                return int.Parse(d);
    
            });
    
            foreach (var r in tasks)
            {
    
            };
    
            foreach (var r in tasks)
            {
    
            };
    
            Assert.AreEqual(2, k);
    
        }
    
        [TestMethod]
        public void TestTwiceShoot2()
        {
            List<string> items = new List<string>();
            items.Add("1");
            int k = 0;
    
            var tasks = items.Where(d =>
            {
                k++;
                return true;
    
            });
    
            foreach (var r in tasks)
            {
    
            };
    
            foreach (var r in tasks)
            {
    
            };
    
            Assert.AreEqual(2, k);
    
        }
    

    虽然 Linq 语句返回了一个 IEnumerable 对象,该对象存储了委托的结果。但是,显然它只存储了委托的快捷方式,因此每个枚举器遍历都会触发委托。因此,最好使用 ToArray() 或 ToList() 来获取结果列表,如下所示:

            [TestMethod]
        public void TestTwiceShoot2()
        {
            List<string> items = new List<string>();
            items.Add("1");
            int k = 0;
    
            var tasks = items.Where(d =>
            {
                k++;
                return true;
    
            }).ToList();
    
            foreach (var r in tasks)
            {
    
            };
    
            foreach (var r in tasks)
            {
    
            };
    
            Assert.AreEqual(1, k);
    
        }
    

    【讨论】:

      猜你喜欢
      • 2014-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多