繁体   English   中英

C# 多线程foreach循环

[英]C# multi-threaded foreach loop

我最近开始使用 C# 进行多线程调用,但我不确定它是否正确。

我怎样才能让这个 go 更快? 我猜它与并行性有关,但我还没有成功地将这个概念整合到其中。

编辑

请注意,这是在远程 VM 中运行的,它是一个控制台程序; 意味着用户体验不是问题。 我只是希望它运行得快,因为链接数可能 go 多达 200k 个元素,我们希望尽快得到结果。 我还删除了除一个以外的所有问题,因为这是我需要帮助的问题。

这是我的代码,它似乎有效:

// Use of my results
public void Main() 
{
  var results = ValidateInternalLinks();
  // Writes results to txt file
  WriteResults(results.Result, "Internal Links");
}

// Validation of data
public async Task<List<InternalLinksModel>> ValidateInternalLinks() 
{
  var tasks = new List<Task>();
  var InternalLinks = new List<InternalLinksModel>();
  // Populate InternalLinks with the data

  foreach (var internalLink in InternalLinks)
  {
    tasks.Add(GetResults(internalLink));
  }

  await Task.WhenAll(tasks);

  return InternalLinks;
}

// Get Results for each piece of data
public async Task GetResults(InternalLinksModel internalLink)
{ 
  var response = await SearchValue(internalLink.SearchValue);

// Analyse response and change result (possible values: SUCCESS, FAILED, [])
  internalLink.PossibleResults = ValidateSearchResult(response);
}

// Http Request
public async Task<ResponseModel> SearchValue(string value) 
{
  // RestSharp API creation and headers addition
  var response = await client.ExecuteTaskAsync(request);

  return JsonConvert.DeserializeObject<ResponseModel>(response.Content);
}

似乎您有一系列 I/O 密集型和 CPU 密集型作业,您需要一个接一个地执行,每个步骤都需要不同程度的并发性。 处理此类工作负载的一个好工具是TPL 数据流库 该库的设计方式允许形成从一个块流向下一个块的数据管道(甚至复杂网络)。 我试图提出一个示例来演示如何使用此库,然后意识到您的工作流程包括最后一个步骤,其中必须更新属于进入管道的第一种类型的项目的属性 ( internalLink.PossibleResults )。 这使事情变得相当复杂,因为它意味着第一种类型必须沿着管道的所有步骤进行。 最简单的方法可能是使用ValueTuple作为输入和块的 output。 不过,这会使我的示例过于混乱,因此我更愿意将其保留为最简单的形式,因为它的目的主要是展示 TPL Dataflow 库的功能:

var cts = new CancellationTokenSource();
var restClient = new RestClient();

var block1 = new TransformBlock<InternalLinksModel, RestResponse>(async item =>
{
    return await restClient.ExecuteTaskAsync(item);
}, new ExecutionDataflowBlockOptions()
{
    MaxDegreeOfParallelism = 10, // 10 concurrent REST requests max
    CancellationToken = cts.Token, // Cancel at any time
});

var block2 = new TransformBlock<RestResponse, ResponseModel>(item =>
{
    return JsonConvert.DeserializeObject<ResponseModel>(item.Content);
}, new ExecutionDataflowBlockOptions()
{
    MaxDegreeOfParallelism = 2, // 2 threads max for this CPU bound job
    CancellationToken = cts.Token, // Cancel at any time
});

var block3 = new TransformBlock<ResponseModel, string>(async item =>
{
    return await SearchValue(item);
}, new ExecutionDataflowBlockOptions()
{
    MaxDegreeOfParallelism = 10, // Concurrency 10 for this I/O bound job
    CancellationToken = cts.Token, // Cancel at any time
});

var block4 = new ActionBlock<string>(item =>
{
    ValidateSearchResult(item);
}, new ExecutionDataflowBlockOptions()
{
    MaxDegreeOfParallelism = 1, // 1 thread max for this CPU bound job
    CancellationToken = cts.Token, // Cancel at any time
});

block1.LinkTo(block2, new DataflowLinkOptions() { PropagateCompletion = true });
block2.LinkTo(block3, new DataflowLinkOptions() { PropagateCompletion = true });
block3.LinkTo(block4, new DataflowLinkOptions() { PropagateCompletion = true });

var internalLinks = new List<InternalLinksModel>();
// Populate internalLinks with the data
foreach (var internalLink in internalLinks)
{
    await block1.SendAsync(internalLink);
}
block1.Complete();

await block4.Completion;

此示例中使用了两种类型的块, TransformBlockActionBlock ActionBlock通常是管道的最后一个块,因为它不会产生任何 output。 如果您的工作负载过于细化,并且传递对象的开销与工作负载本身相当,您可以使用BatchBlock启动管道,然后分批处理后续步骤,例如每个 10 个元素。 不过,您的情况似乎不需要这样做,因为发出 web 请求和解析 JSON 响应是相当庞大的工作。

async/await/WhenAll 是 go 的正确方法,您的性能瓶颈可能是 I/O 限制(HTTP 请求)而不是计算限制。 异步是处理这个问题的合适工具。 您发出多少 HTTP 请求,它们都发往同一台服务器吗? 如果是这样,您可能会遇到连接限制。 我对 RestSharp 不是很熟悉,但您可以尝试通过 ServicePointManager 增加连接限制。 您拥有的未完成请求越多,假设服务器可以处理它们,WhenAll 完成的速度就越快。

https://docs.microsoft.com/en-us/dotnet/api/system.net.servicepointmanager?view=netframework-4.8

综上所述,我会重新组织你的代码。 对您的 HTTP 请求使用 Task/WhenAll。 并在WhenAll 完成后处理响应。 如果这样做,您可以确定 HTTP 请求是否是瓶颈所在,方法是在 WhenAll 观察执行时间之后设置断点。 如果无法调试,可以记录执行时间。 这应该让您了解瓶颈是否主要是网络 I/O。 我很有信心是的。

如果事实证明存在计算瓶颈,您可以使用 Parallel.ForEach 循环来反序列化、验证和分配。

            var internalLinks = new List<InternalLinksModel>();
            // Populate InternalLinks with the data
            // I'm assuming this means internalLinks is assumed to contain data. If not I'm not sure I understand your code.
            var dictionary = new Dictionary<Task, InternalLinksModel>(); //You shouldn't need a concurrent dictionary since you'll only be doing reads in parallel.

            //make api calls - I/O bound
            foreach (var l in internalLinks)
            {
                dictionary[client.ExecuteTaskAsync(l.SearchValue)] = l;
            }

            await Task.WhenAll(dictionary.Keys);    
            // I/O is done.

            // Compute bound - deserialize, validate, assign.
            Parallel.ForEach(dictionary.Keys, (task) =>
            {
                var responseModel = JsonConvert.DeserializeObject<ResponseModel>(task.Result.Content);
                dictionary[task].PossibleResults = ValidateSearchResult(responseModel);
            });


            // Writes results to txt file
            WriteResults(dictionary.Values, "Internal Links");

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM