C# 为什么多个短任务最终会得到相同的id?
我有一个要处理的项目列表,并为每个项目创建一个任务,然后使用task.WhenAny()等待。我遵循此处描述的模式: 我改变了一件事:我正在使用C# 为什么多个短任务最终会得到相同的id?,c#,async-await,C#,Async Await,我有一个要处理的项目列表,并为每个项目创建一个任务,然后使用task.WhenAny()等待。我遵循此处描述的模式: 我改变了一件事:我正在使用HashSet而不是List。但我注意到所有任务最终都会得到相同的id,因此HashSet只添加了其中一个,因此我最终只等待一个任务 我在dotnetfiddle中有一个工作示例: 同时粘贴以下代码: using System; using System.Collections.Generic; using System.Threading.Tasks;
HashSet
而不是List
。但我注意到所有任务最终都会得到相同的id,因此HashSet
只添加了其中一个,因此我最终只等待一个任务
我在dotnetfiddle中有一个工作示例:
同时粘贴以下代码:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ReproTasksWithSameId
{
public class Program
{
public static async Task Main(string[] args)
{
List<int> itemIds = new List<int>() { 1, 2, 3, 4 };
await ProcessManyItems(itemIds);
}
private static async Task ProcessManyItems(List<int> itemIds)
{
//
// Create tasks for each item and then wait for them using Task.WhenAny
// Following Task.WhenAny() pattern described here: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/start-multiple-async-tasks-and-process-them-as-they-complete
// But replaced List<Task> with HashSet<Task>.
//
HashSet<Task> tasks = new HashSet<Task>();
// We map the task ids to item ids so that we have enough info to log if a task throws an exception.
Dictionary<int, int> taskIdToItemId = new Dictionary<int, int>();
foreach (int itemId in itemIds)
{
Task task = ProcessOneItem(itemId);
Console.WriteLine("Created task with id: {0}", task.Id);
tasks.Add(task);
taskIdToItemId[task.Id] = itemId;
}
// Add a loop to process the tasks one at a time until none remain.
while (tasks.Count > 0)
{
// Identify the first task that completes.
Task task = await Task.WhenAny(tasks);
// Remove the selected task from the list so that we don't
// process it more than once.
tasks.Remove(task);
// Get the item id from our map, so that we can log rich information.
int itemId = taskIdToItemId[task.Id];
try
{
// Await the completed task.
await task; // unwrap exceptions.
Console.WriteLine("Successfully processed task with id: {0}, itemId: {1}", task.Id, itemId);
}
catch (Exception ex)
{
Console.WriteLine("Failed to process task with id: {0}, itemId: {1}. Just logging & eating the exception {1}", task.Id, itemId, ex);
}
}
}
private static async Task ProcessOneItem(int itemId)
{
// Assume this method awaits on some asynchronous IO.
Console.WriteLine("item: {0}", itemId);
}
}
}
因此,基本上程序在等待第一个任务后退出
Task
而不是Task
的方法进行了测试,在这种情况下效果很好这是因为
ProcessOneItem
不是异步的
您应该看到以下警告:
此异步方法缺少“await”运算符,将同步运行。考虑使用“Acess”操作符等待非阻塞API调用,或“等待任务.Run(…)”在后台线程上执行CPU绑定的工作。
将
wait(…)
添加到ProcessOneItem
后,返回任务将有一个id。问题的代码是同步的,因此只有一个已完成的任务async
不会使某些东西异步运行,它是一种语法糖,允许使用await
等待已经执行的异步操作完成,而不会阻塞调用线程
至于文档示例,就是这样。一个文档示例,不是一个模式,当然也不是可以在生产中使用的东西,除了简单的案例
如果一次只能发出5个请求以避免网络或CPU被淹没,会发生什么情况?你只需要下载固定数量的记录就可以了。如果您需要处理下载的数据,该怎么办?如果URL列表来自另一个线程怎么办
这些问题由并发容器、发布/订阅模式以及专门构建的数据流和通道类来处理
数据流
较旧的数据流类负责缓冲输入和输出,并自动处理辅助任务。整个下载代码可以替换为:
我们可以使用其他块(如TransformBlock)生成输出,将其传递给另一个块,从而创建并发处理管道。假设我们有两种方法,DownloadURL
和ParseResponse
,而不仅仅是ProcessUrl
:
Task<string> DownloadUrlAsync(string url,HttpClient client)
{
return client.GetStringAsync(url);
}
void ParseResponse(string content)
{
var object=JObject.Parse();
DoSomethingWith(object);
}
频道
推出围棋式频道。这些实际上是数据流块的较低级别的概念。如果2012年频道可用,它们将使用频道编写
等效的下载方法如下所示:
ChannelReader<string> Downloader(ChannelReader<string> ulrs,HttpClient client,
int capacity,CancellationToken token=default)
{
var channel=Channel.CreateBounded(capacity);
var writer=channel.Writer;
_ = Task.Run(async ()=>{
await foreach(var url in urls.ReadAsStreamAsync(token))
{
var response=await client.GetStringAsync(url);
await writer.WriteAsync(response);
}
}).ContinueWith(t=>writer.Complete(t.Exception));
return channel.Reader;
}
出版商可以创建自己的频道,并将阅读器传递给下载程序方法。他们也不需要提前发布任何内容:
var channel=Channel.CreateUnbounded<string>();
var dlReader=Downloader(channel.Reader,client,5,5);
foreach(var url in someUrlList)
{
await channel.Writer.WriteAsync(url);
}
channel.Writer.Complete();
从财产文件中:
任务ID是按需分配的,不一定表示创建任务实例的顺序。请注意,尽管冲突非常罕见,但任务标识符不能保证唯一
据我所知,该属性主要用于调试目的。您可能应该避免在生产代码中依赖它。这不是一种模式,它只是一个演示任务的文档示例。除了非常简单的情况外,它并不适用于生产场景。有更好的类来处理发布/订阅、多个worker和任务,比如Dataflow的ActionBlock和System.Threading通道?此代码中没有活动任务。编译器应该已经发出警告,ProcessOneItem
不包含wait
,因此将同步运行。这里没有任务,所有任务都在主线程上运行。如果同步运行代码,每次都返回相同的已完成任务。添加一个等待任务。延迟(100)
在该方法中,它将返回新任务。有更好的方法吗?
做什么?具体细节很重要。处理1000个URL需要不同于处理100K内存元素的体系结构。如果您对此有答案,最好将其标记为这样,并询问一个新问题。谢谢您提供的详细信息。正在阅读。@Turbo添加了另一个关于ChannelsDataflow的部分,这是IMHO执行此任务的正确工具。它是一个涵盖缓冲和处理的完整解决方案。通道非常适合缓冲(实际上可能是完美的异步队列),但在处理部分没有提供任何帮助。因此,您最终会手动生成任务,并编写具有问题异常处理特征的次优处理循环。例如,对于本例中的第二个下载程序
,单个异常将杀死一个辅助任务,从而降低并行度。在处理完所有URL或所有工作线程都已死亡之前,不会传播错误。
Task<string> DownloadUrlAsync(string url,HttpClient client)
{
return client.GetStringAsync(url);
}
void ParseResponse(string content)
{
var object=JObject.Parse();
DoSomethingWith(object);
}
var dlOptions=new ExecutionDataflowBlockOptions(){
MaxDegreeOfParallelism=5,
BoundedCapacity=5,
CancellationToken=cts.Token
};
var downloader=new TransformBlock<string,string>(
url=>DownloadUrlAsync(url,client),
dlOptions);
var parseOptions = new ExecutionDataflowBlockOptions(){
MaxDegreeOfParallelism=10,
BoundedCapacity=2,
CancellationToken=cts.Token
};
var parser=new ActionBlock<string>(ParseResponse);
downloader.LinkTo(parser, new DataflowLinkOptions{PropageateCompletion=true});
foreach(var url in urls)
{
await downloader.SendAsync(url);
}
//Tell the block we're done
downloader.Complete();
//Wait until all urls are parsed
await parser.Completion;
ChannelReader<string> Downloader(ChannelReader<string> ulrs,HttpClient client,
int capacity,CancellationToken token=default)
{
var channel=Channel.CreateBounded(capacity);
var writer=channel.Writer;
_ = Task.Run(async ()=>{
await foreach(var url in urls.ReadAsStreamAsync(token))
{
var response=await client.GetStringAsync(url);
await writer.WriteAsync(response);
}
}).ContinueWith(t=>writer.Complete(t.Exception));
return channel.Reader;
}
ChannelReader<string> Downloader(ChannelReader<string> ulrs,HttpClient client,
int capacity,int dop,CancellationToken token=default)
{
var channel=Channel.CreateBounded(capacity);
var writer=channel.Writer;
var tasks = Enumerable
.Range(0,dop)
.Select(_=> Task.Run(async ()=>{
await foreach(var url in urls.ReadAllAsync(token))
{
var response=await client.GetStringAsync(url);
await writer.WriteAsync(response);
}
});
_=Task.WhenAll(tasks)
.ContinueWith(t=>writer.Complete(t.Exception));
return channel.Reader;
}
var channel=Channel.CreateUnbounded<string>();
var dlReader=Downloader(channel.Reader,client,5,5);
foreach(var url in someUrlList)
{
await channel.Writer.WriteAsync(url);
}
channel.Writer.Complete();
ChannelReader<T> Generate<T>(this IEnumerable<T> source)
{
var channel=Channel.CreateUnbounded<T>();
foreach(var item in source)
{
channel.Writer.TryWrite(T);
}
channel.Writer.Complete();
return channel.Reader;
}
var pipeline= someUrls.Generate()
.Downloader(client,5,5);