在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 Select
对Task
有一些特殊的对待。
毕竟,在Linq中触发多个异步方法的好方法是什么,并在WaitAll
之后检查结果?
答案 0 :(得分:5)
这是因为tasks
是IEnumerable<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);
}