WaitAll之后再次触发任务

时间:2014-03-14 00:27:59

标签: c# linq asynchronous parallel-processing task

在Linq HttpClient.GetAsync中使用Select或其任何异步方法或任何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之后检查结果?

2 个答案:

答案 0 :(得分:5)

这是因为tasksIEnumerable<Task>,每次通过列表枚举它都会重新运行.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<T>因结果返回或传入新函数而超出了您的控制范围。

答案 1 :(得分:0)

我认为问题是我对枚举如何运作的误解。这些测试通过:

        [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对象,它存储了委托的结果。但是,显然它只存储代表的快捷方式,因此每个枚举器walk将触发委托。因此,最好使用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);

    }