问题描述
这个小型控制台应用程序是进行数千次数据库调用的概念证明.这个想法是,我们希望许多呼叫被同时挂断.无需等待一个呼叫结束就可以开始下一个呼叫.
This little console app is a proof of concept for making thousands of database calls. The idea is that we want many calls to be happing simultaneously. No need to wait for one call to finish before starting the next.
起初,这种方法(参见下文)似乎是一种不错的方法,但是当我们将其应用于实际的数据库调用时,我们看到的似乎是将进程堆叠在一起.意思是,它启动了所有程序,但是直到所有程序都启动后,它们都没有完成.
At first, this (see below) seemed like a good approach, but when we applied this to the actual database calls, what we're seeing is that it seems to stack the processes. Meaning, it Starts all of them, but none of them Finish until all of them have been started.
我希望(并希望)一些电话能在其他电话开始之前完成.但这似乎并非如此.
I would expect (and want) some of the calls to be finishing before others have started. But that doesn't seem to be the case.
class Program
{
static void Main(string[] args)
{
Console.WriteLine("starting");
DatabaseCallsAsync().Wait();
Console.WriteLine("ending"); // Must not fire until all database calls are complete.
Console.Read();
}
static async Task DatabaseCallsAsync()
{
List<int> inputParameters = new List<int>();
for (int i = 0; i < 100; i++)
{
inputParameters.Add(i);
}
await Task.WhenAll(inputParameters.Select(x => DatabaseCallAsync($"Task {x}")));
}
static async Task DatabaseCallAsync(string taskName)
{
Console.WriteLine($"{taskName}: start");
await Task.Delay(1000);
Console.WriteLine($"{taskName}: finish");
}
}
如何对此进行调整,以使某些呼叫无需等待所有呼叫开始就可以结束?
How can this be adjusted so that some calls finish without waiting for all of them to start?
推荐答案
请注意以下几点很重要:
It's important to note a couple things:
-
async
方法开始同步运行.如果给await
提供了不完整的Task
,魔术就发生在await
上. - 异步!=并行.异步运行某些东西只会让线程在等待某处的回复时继续执行其他操作.这并不意味着同时发生多件事.
async
methods start running synchronously. The magic happens atawait
, ifawait
is given an incompleteTask
.- Asynchronous != parallel. Running something asynchronously just lets the thread go and do something else while it is waiting for a reply from somewhere. It doesn't mean that multiple things are happening at the same time.
记住这些事情,这就是您遍历您创建的所有任务时所发生的情况:
With those things in mind, this is what's happening in your case when it loops through all the tasks you've created:
- 所有任务都放在待办事项"列表上.
- 任务1已启动.
- 在
await
处,返回不完整的Task
,并将该方法的其余部分放在待办事项"列表中. - 该线程意识到无事可做,然后继续执行待办事项"列表中的下一件事,即启动下一个
Task
.
- All of the tasks are put on the "to do" list.
- Task 1 is started.
- At
await
, an incompleteTask
is returned, and the rest of the method is put on the "to do" list. - The thread realizes there is nothing to do and moves on to the next thing on the "to do" list, which is to start the next
Task
.
在第4步中,待办事项"列表中的下一件事将始终是列表中的下一个 Task
,直到列表中没有任何剩余内容为止.只有 then ,要做的事情"列表上的下一件事情是按照已完成的任务的顺序继续执行已完成的任务.
At step 4, the next thing on the "to do" list will always be the next Task
in the list until there is nothing left in the list. Only then, the next thing on the "to do" list is the continuation of the tasks that have completed, in the order they completed.
所有这些都在同一线程上发生:它是异步的,而不是并行的.
All of this happens on the same thread: it is asynchronous, not parallel.
但是!如果您实际上使用了SQL调用(并且您为每个任务建立了新的连接,因为单个连接一次只能运行一个查询-除非启用多个活动结果集)并监控SQL,您将看到这些调用进入并因为SQL并行运行查询,所以可能在所有这些查询开始之前就已经完成了.只是直到所有任务都开始,C#方法的延续才会开始.
But! If you actually use SQL calls (and you make a new connection for each task, since a single connection can only run one query at a time - unless you enable Multiple Active Result Sets) and monitor SQL, you will see those calls coming in and likely finishing before all of them have started, because SQL runs queries in parallel. It's only that the continuation of the C# methods won't start until all the tasks have started.
如果您真正希望并行运行它们,则需要多线程.您可以查看 Parallel.ForEach
(示例此处),但这不是异步的.它将为每个实例创建一个线程,该线程将阻塞直到完成.在桌面应用程序中这没什么大不了的,但是在ASP.NET中,线程是有限的,因此您需要小心.
If you are truly looking to run these in parallel, then you need multi-threading. You can look at Parallel.ForEach
(examples here), but that is not asynchronous. It will create a thread for each instance and the thread will block until it's complete. That's not a big deal in a desktop app, but in ASP.NET, threads are finite, so you need to be careful.
对此此处进行了大讨论,但我特别喜欢此答案,这不是 多线程技术,但可以节制您的任务.因此,您可以告诉它开始 x
个任务,并在每个任务完成时开始下一个任务,直到所有任务都运行为止.对于您的代码,它看起来像这样(一次运行10个任务):
There is a big discussion of this here, but I particularly like this answer, which is not multi-threading, but gives a way to throttle your tasks. So you can tell it to start x
number of tasks, and as each task finishes, start the next until all of them have run. For your code, that would look something like this (running 10 tasks at a time):
static async Task DatabaseCallsAsync()
{
List<int> inputParameters = new List<int>();
for (int i = 0; i < 100; i++)
{
inputParameters.Add(i);
}
await RunWithMaxDegreeOfConcurrency(10, inputParameters, x => DatabaseCallAsync($"Task {x}"));
}
static async Task DatabaseCallAsync(string taskName)
{
Console.WriteLine($"{taskName}: start");
await Task.Delay(1000);
Console.WriteLine($"{taskName}: finish");
}
public static async Task RunWithMaxDegreeOfConcurrency<T>(
int maxDegreeOfConcurrency, IEnumerable<T> collection, Func<T, Task> taskFactory)
{
var activeTasks = new List<Task>(maxDegreeOfConcurrency);
foreach (var task in collection.Select(taskFactory))
{
activeTasks.Add(task);
if (activeTasks.Count == maxDegreeOfConcurrency)
{
await Task.WhenAny(activeTasks.ToArray());
//observe exceptions here
activeTasks.RemoveAll(t => t.IsCompleted);
}
}
await Task.WhenAll(activeTasks.ToArray()).ContinueWith(t =>
{
//observe exceptions in a manner consistent with the above
});
}
这篇关于同时处理数千个数据库调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!