gpt4 book ai didi

c# - 为什么这个 TAP 异步/等待代码比 TPL 版本慢?

转载 作者:IT王子 更新时间:2023-10-29 04:50:00 25 4
gpt4 key购买 nike

我必须编写一个调用 Microsoft Dynamics CRM Web 服务的控制台应用程序来对八千多个 CRM 对象执行操作。 Web 服务调用的细节无关紧要,此处未显示,但我需要一个多线程客户端,以便我可以并行调用。我希望能够控制配置设置中使用的线程数量,并且如果服务错误数量达到配置定义的阈值,应用程序也可以取消整个操作。

我使用任务并行库 Task.Run 和 ContinueWith 编写它,跟踪正在进行的调用(线程)数量、我们收到的错误数量以及用户是否已从键盘取消。一切正常,我进行了大量的日志记录,以确保线程干净利落地完成,并且在运行结束时一切都井井有条。我可以看到该程序正在使用最大并行线程数,如果达到最大限制,则等待一个正在运行的任务完成,然后再开始另一个任务。

在我的代码审查期间,我的同事建议最好使用 async/await 而不是 tasks 和 continuations,所以我创建了一个分支并以这种方式重写了它。结果很有趣——async/await 版本的速度几乎是原来的两倍,而且它从未达到允许的最大并行操作/线程数。 TPL 始终达到 10 个并行线程,而异步/等待版本从未超过 5 个。

我的问题是:我编写异步/等待代码(甚至 TPL 代码)的方式是否有误?如果我没有编码错误,您能否解释为什么 async/await 效率较低,这是否意味着继续使用 TPL 进行多线程代码会更好。

请注意,我测试的代码实际上并未调用 CRM - CrmClient 类只是线程休眠了配置中指定的持续时间(五秒),然后抛出异常。这意味着没有可能影响性能的外部变量。

为了这个问题的目的,我创建了一个结合了两个版本的精简程序;调用哪一个由配置设置决定。它们中的每一个都从设置环境的 Bootstrap 运行器开始,创建队列类,然后使用 TaskCompletionSource 等待完成。 CancellationTokenSource 用于向用户发出取消信号。要处理的 ID 列表从嵌入式文件中读取并推送到 ConcurrentQueue。他们都开始调用 StartCrmRequest 的次数与 max-threads 一样;随后,每次处理结果时,ProcessResult 方法都会再次调用 StartCrmRequest,一直持续到处理完我们所有的 ID。

您可以从这里克隆/下载完整的程序:https://bitbucket.org/kentrob/pmgfixso/

相关配置如下:

<appSettings>
<add key="TellUserAfterNCalls" value="5"/>
<add key="CrmErrorsBeforeQuitting" value="20"/>
<add key="MaxThreads" value="10"/>
<add key="CallIntervalMsecs" value="5000"/>
<add key="UseAsyncAwait" value="True" />
</appSettings>

从 TPL 版本开始,这里是启动队列管理器的引导运行程序:

public static class TplRunner
{
private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();

public static void StartQueue(RuntimeParameters parameters, IEnumerable<string> idList)
{
Console.CancelKeyPress += (s, args) =>
{
CancelCrmClient();
args.Cancel = true;
};

var start = DateTime.Now;
Program.TellUser("Start: " + start);

var taskCompletionSource = new TplQueue(parameters)
.Start(CancellationTokenSource.Token, idList);

while (!taskCompletionSource.Task.IsCompleted)
{
if (Console.KeyAvailable)
{
if (Console.ReadKey().Key != ConsoleKey.Q) continue;
Console.WriteLine("When all threads are complete, press any key to continue.");
CancelCrmClient();
}
}

var end = DateTime.Now;
Program.TellUser("End: {0}. Elapsed = {1} secs.", end, (end - start).TotalSeconds);
}

private static void CancelCrmClient()
{
CancellationTokenSource.Cancel();
Console.WriteLine("Cancelling Crm client. Web service calls in operation will have to run to completion.");
}
}

这是 TPL 队列管理器本身:

public class TplQueue
{
private readonly RuntimeParameters parameters;
private readonly object locker = new object();
private ConcurrentQueue<string> idQueue = new ConcurrentQueue<string>();
private readonly CrmClient crmClient;
private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();
private int threadCount;
private int crmErrorCount;
private int processedCount;
private CancellationToken cancelToken;

public TplQueue(RuntimeParameters parameters)
{
this.parameters = parameters;
crmClient = new CrmClient();
}

public TaskCompletionSource<bool> Start(CancellationToken cancellationToken, IEnumerable<string> ids)
{
cancelToken = cancellationToken;

foreach (var id in ids)
{
idQueue.Enqueue(id);
}

threadCount = 0;

// Prime our thread pump with max threads.
for (var i = 0; i < parameters.MaxThreads; i++)
{
Task.Run((Action) StartCrmRequest, cancellationToken);
}

return taskCompletionSource;
}

private void StartCrmRequest()
{
if (taskCompletionSource.Task.IsCompleted)
{
return;
}

if (cancelToken.IsCancellationRequested)
{
Program.TellUser("Crm client cancelling...");
ClearQueue();
return;
}

var count = GetThreadCount();

if (count >= parameters.MaxThreads)
{
return;
}

string id;
if (!idQueue.TryDequeue(out id)) return;

IncrementThreadCount();
crmClient.CompleteActivityAsync(new Guid(id), parameters.CallIntervalMsecs).ContinueWith(ProcessResult);

processedCount += 1;
if (parameters.TellUserAfterNCalls > 0 && processedCount%parameters.TellUserAfterNCalls == 0)
{
ShowProgress(processedCount);
}
}

private void ProcessResult(Task<CrmResultMessage> response)
{
if (response.Result.CrmResult == CrmResult.Failed && ++crmErrorCount == parameters.CrmErrorsBeforeQuitting)
{
Program.TellUser(
"Quitting because CRM error count is equal to {0}. Already queued web service calls will have to run to completion.",
crmErrorCount);
ClearQueue();
}

var count = DecrementThreadCount();

if (idQueue.Count == 0 && count == 0)
{
taskCompletionSource.SetResult(true);
}
else
{
StartCrmRequest();
}
}

private int GetThreadCount()
{
lock (locker)
{
return threadCount;
}
}

private void IncrementThreadCount()
{
lock (locker)
{
threadCount = threadCount + 1;
}
}

private int DecrementThreadCount()
{
lock (locker)
{
threadCount = threadCount - 1;
return threadCount;
}
}

private void ClearQueue()
{
idQueue = new ConcurrentQueue<string>();
}

private static void ShowProgress(int processedCount)
{
Program.TellUser("{0} activities processed.", processedCount);
}
}

请注意,我知道有几个计数器不是线程安全的,但它们并不重要; threadCount 变量是唯一的关键变量。

这是虚拟 CRM 客户端方法:

public Task<CrmResultMessage> CompleteActivityAsync(Guid activityId, int callIntervalMsecs)
{
// Here we would normally call a CRM web service.
return Task.Run(() =>
{
try
{
if (callIntervalMsecs > 0)
{
Thread.Sleep(callIntervalMsecs);
}
throw new ApplicationException("Crm web service not available at the moment.");
}
catch
{
return new CrmResultMessage(activityId, CrmResult.Failed);
}
});
}

下面是相同的 async/await 类(为了简洁起见删除了常用方法):

public static class AsyncRunner
{
private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();

public static void StartQueue(RuntimeParameters parameters, IEnumerable<string> idList)
{
var start = DateTime.Now;
Program.TellUser("Start: " + start);

var taskCompletionSource = new AsyncQueue(parameters)
.StartAsync(CancellationTokenSource.Token, idList).Result;

while (!taskCompletionSource.Task.IsCompleted)
{
...
}

var end = DateTime.Now;
Program.TellUser("End: {0}. Elapsed = {1} secs.", end, (end - start).TotalSeconds);
}
}

异步/等待队列管理器:

public class AsyncQueue
{
private readonly RuntimeParameters parameters;
private readonly object locker = new object();
private readonly CrmClient crmClient;
private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();
private CancellationToken cancelToken;
private ConcurrentQueue<string> idQueue = new ConcurrentQueue<string>();
private int threadCount;
private int crmErrorCount;
private int processedCount;

public AsyncQueue(RuntimeParameters parameters)
{
this.parameters = parameters;
crmClient = new CrmClient();
}

public async Task<TaskCompletionSource<bool>> StartAsync(CancellationToken cancellationToken,
IEnumerable<string> ids)
{
cancelToken = cancellationToken;

foreach (var id in ids)
{
idQueue.Enqueue(id);
}
threadCount = 0;

// Prime our thread pump with max threads.
for (var i = 0; i < parameters.MaxThreads; i++)
{
await StartCrmRequest();
}

return taskCompletionSource;
}

private async Task StartCrmRequest()
{
if (taskCompletionSource.Task.IsCompleted)
{
return;
}

if (cancelToken.IsCancellationRequested)
{
...
return;
}

var count = GetThreadCount();

if (count >= parameters.MaxThreads)
{
return;
}

string id;
if (!idQueue.TryDequeue(out id)) return;

IncrementThreadCount();
var crmMessage = await crmClient.CompleteActivityAsync(new Guid(id), parameters.CallIntervalMsecs);
ProcessResult(crmMessage);

processedCount += 1;
if (parameters.TellUserAfterNCalls > 0 && processedCount%parameters.TellUserAfterNCalls == 0)
{
ShowProgress(processedCount);
}
}

private async void ProcessResult(CrmResultMessage response)
{
if (response.CrmResult == CrmResult.Failed && ++crmErrorCount == parameters.CrmErrorsBeforeQuitting)
{
Program.TellUser(
"Quitting because CRM error count is equal to {0}. Already queued web service calls will have to run to completion.",
crmErrorCount);
ClearQueue();
}

var count = DecrementThreadCount();

if (idQueue.Count == 0 && count == 0)
{
taskCompletionSource.SetResult(true);
}
else
{
await StartCrmRequest();
}
}
}

因此,将 MaxThreads 设置为 10 并将 CrmErrorsBeforeQuitting 设置为 20,我机器上的 TPL 版本在 19 秒内完成,而异步/等待版本需要 35 秒。考虑到我有超过 8000 个电话,所以这是一个显着的差异。有什么想法吗?

最佳答案

我想我在这里看到了问题,或者至少是其中的一部分。仔细看下面的两段代码;它们不等价。

// Prime our thread pump with max threads.
for (var i = 0; i < parameters.MaxThreads; i++)
{
Task.Run((Action) StartCrmRequest, cancellationToken);
}

和:

// Prime our thread pump with max threads.
for (var i = 0; i < parameters.MaxThreads; i++)
{
await StartCrmRequest();
}

在原始代码中(我认为它在功能上是合理的)有一个对 ContinueWith 的调用。如果要保留原始行为,那正是我希望在微不足道的重写中看到的 await 语句的数量。

这不是硬性规定,只适用于简单的情况,但仍然是一件值得留意的好事。

关于c# - 为什么这个 TAP 异步/等待代码比 TPL 版本慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24389549/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com