简体   繁体   English

修复顺序执行的C#异步方法

[英]Fix for C# Async Method Executing Sequentially

The problem I'm running into is a call to an async method I'm performing is happening sequentially. 我遇到的问题是对正在执行的异步方法的调用是顺序发生的。 I'm adding the call's tasks to a ConcurrentBag and awaiting the tasks in the bag. 我正在将呼叫的任务添加到ConcurrentBag中,并等待包中的任务。 I don't care about the results of these calls, I just need confirmation that they completed. 我不在乎这些电话的结果,我只需要确认它们已完成即可。 However, these calls are happening fully sequentially which is very confusing. 但是,这些调用是完全按顺序进行的,这非常令人困惑。 The method in question performs a few PostgreSQL queries via Npgsql with parameterized queries. 有问题的方法通过Npgsql使用参数化查询执行一些PostgreSQL查询。 The caller gets a tree of our own data and pulls out all the nodes in the tree and iterates over the nodes and performs this task on them. 调用者获得一棵我们自己的数据树,并拔出树中的所有节点并遍历这些节点并在它们上执行此任务。 I am also using a custom AsyncHelper class which will iterate over the tasks in an IEnumerable implementer and wait the tasks inside it. 我还使用了一个自定义AsyncHelper类,该类将遍历IEnumerable实现程序中的任务并等待其中的任务。 Both my Tree implementation and AsyncHelper have been tested in another piece of code that does the same basic principles of this code, which executes the tasks asynchronously as expected. 我的Tree实现和AsyncHelper均已在另一段代码中进行过测试,这些代码与该代码的基本原理相同,它们可以按预期异步执行任务。

I've added logging on the function calls to confirm these are happening sequentially. 我在函数调用上添加了日志记录,以确认这些事件是顺序发生的。 I event also taking the method out of the bag and just running the method, which still does the same thing, it happens sequentially and won't continue my loop until it's done. 我还从包装中取出该方法并运行该方法,它仍然执行相同的操作,它顺序发生,并且直到完成后才继续我的循环。 All my methods are labeled async and I'm not awaiting them until after the loop. 我所有的方法都标记为异步,直到循环之后我才等待它们。

//method executing sequentially
public static async Task<List<ContactStatistic>> getContactStats(Guid tenantId, DateTime start, DateTime end, Breakdown breakdown) {
    if (!await Postgres.warmConnection(5)) { return null; }
    var hierarchy = await getTreeForTenant<TenantContactStatsNode>(tenantId);

    //perform calculations to determine stats for each element
    var calculationTasks = new ConcurrentBag<Task>();
    var allData = await hierarchy.getAllData();
    var timestampGotAllData = DateTime.Now;

    foreach (var d in allData) {
        calculationTasks.Add(d.getContactStats(start, end, breakdown));
    }

    Console.WriteLine("about to await all the tasks");
    //await the tasks to complete for calculations
    await AsyncHelper.waitAll(calculationTasks);
}


//method it's calling
public async Task getContactStats(DateTime start, DateTime end, Breakdown breakdown) {
    //perform two async postgres calls
    //await postgres calls
    //validate PG response
    //perform manipluation on this object with data from the queries
}

I would expect the first call to call the second function, add the task to the bag, and wait on them after it's done. 我希望第一个调用调用第二个函数,将任务添加到包中,并在完成后等待它们。 What's actually occurring is that the method is running, finishing, then adding to the bag. 实际上发生的是该方法正在运行,完成,然后添加到袋子中。

* EDIT * *编辑*

Below is the full code for that second call as requested. 以下是所请求的第二个呼叫的完整代码。 It takes in some data from the database based on time, fills the gaps between the times pulled back so we have a fully sequential return list including all times not having data in the database, and puts that in a object level variable 它根据时间从数据库中获取一些数据,填补了拉回时间之间的间隔,因此我们有了一个完全顺序的返回列表,包括数据库中所有没有数据的时间,并将其放入对象级变量中

public async Task getContactStats(DateTime start, DateTime end, Breakdown breakdown) {
    if (breakdown == Breakdown.Month) {
        //max out month start day to include all for the initial month in the initial count
        start = new DateTime(start.Year, start.Month, DateTime.DaysInMonth(start.Year, start.Month));
    } else {
        //day breakdown previous stats should start the day before given start day
        start = start.AddDays(-1);
    }

    var tran = new PgTran();
    var breakdownQuery = breakdown == Breakdown.Day ? Queries.GET_CONTACT_DAY_BREAKDOWN : Queries.GET_CONTACT_MONTH_BREAKDOWN;
    tran.setQueries(Queries.GET_CONTACT_COUNT_BEFORE_DATE, breakdownQuery);
    tran.setParams(new NpgsqlParameter("@tid", tenantId), new NpgsqlParameter("@start", start), new NpgsqlParameter("@end", end));
    var tranResults = await Postgres.getAll<ContactDayStatistic>(tran);
    //ensure transaction returns two query results
    if (tranResults == null || tranResults.Count != 2) { return; }


    //ensure valid past count was retrieved
    var prevCountResult = tranResults[0];
    if (prevCountResult == null || prevCountResult.Count != 1) { return; }
    var prevStat = new ContactDayStatistic(start.Day, start.Month, start.Year, prevCountResult[0].count);
    //ensure valid contact stat breakdown was retrieved
    var statBreakdown = tranResults[1];
    if (statBreakdown == null) { return;}

    var datesInBreakdown = new List<DateTime?>();
    //get all dates in the returned stats
    foreach (var e in statBreakdown) {
        var eventDate = new DateTime(e.year, e.month, e.day);
        if (datesInBreakdown.Find(item => item == eventDate) == null)
            datesInBreakdown.Add(eventDate);
    }
    //sort so they are sequential
    datesInBreakdown.Sort();

    //initialize timeline starting with initial breakdown
    var fullTimeline = new List<ContactStatistic>();
    //convert initial stat to the right type for final display
    fullTimeline.Add(breakdown == Breakdown.Month ? new ContactStatistic(prevStat) : prevStat);
    foreach (var d in datesInBreakdown) {
        //null date is useless, won't occur, nullable date just for default value of null
        if (d == null) { continue; }
        var newDate = d.Value;
        //fill gaps between last date given and this date
        ContactStatistic.fillGaps(breakdown, newDate, prevStat.getDate(), prevStat.count, ref fullTimeline, false);
        //get stat for this day
        var stat = statBreakdown.Find(item => d == new DateTime(item.year, item.month, item.day));
        if (stat == null) { continue; }
        //add last total for a rolling total of count
        stat.count += prevStat.count;
        fullTimeline.Add(breakdown == Breakdown.Month ? new ContactStatistic(stat) : stat);
        prevStat = stat;
    }
    //fill gaps between last date and end
    ContactStatistic.fillGaps(breakdown, end, prevStat.getDate(), prevStat.count, ref fullTimeline, true);
    //cast list to appropriate return type
    contactStats.Clear();
    contactStats = fullTimeline;
}

* EDIT 2 * Here is the code the AsyncHelper is using to await these tasks. *编辑2 *这是AsyncHelper用于等待这些任务的代码。 This function works perfectly for other code using this same framework and it's basically just to clean up code that has to wait on enumerated tasks. 此功能非常适合使用同一框架的其他代码,并且基本上只是清理必须等待枚举任务的代码。

public static async Task waitAll(IEnumerable<Task> coll) {
    foreach (var taskToWait in coll) {
        await taskToWait;
    }  
}

* EDIT 3 * As per recommendation, I changed waitAll() to use Task.WhenAll() instead of a foreach loop, the issue is still occurring however. *编辑3 *根据建议,我将waitAll()更改为使用Task.WhenAll()而不是foreach循环,但是问题仍然存在。

public static async Task waitAll(IEnumerable<Task> coll) {
    await Task.WhenAll(coll);
}

* EDIT 4 * To ensure it's not the Postgres calls making this happen, I changed the second method to only do a print line then sleep for 200 milliseconds to keep the execution path clear. *编辑4 *为了确保不是发生这种情况的Postgres调用,我将第二种方法更改为仅执行打印行,然后休眠200毫秒以保持执行路径清晰。 I still notice that is happening fully sequentially (even causing my POST to this function time out because the actual real call takes almost 20ms). 我仍然注意到这是完全按顺序发生的(甚至导致我的POST到此函数超时,因为实际的实际调用花费了将近20ms)。 Below is the code for that change to demonstrate 以下是该更改要演示的代码

public async Task getContactStats(DateTime start, DateTime end, Breakdown breakdown) {
    Console.WriteLine("CALLED!");
    Thread.Sleep(200);
}

* EDIT 5 * Per recommendation, I tried a parallel foreach to try to populate the ConcurrentBag of tasks rather than a normal foreach. *编辑5 *根据建议,我尝试使用并行的foreach尝试填充任务的ConcurrentBag,而不是普通的foreach。 I run into an issue here where the parallel foreach finishes once the first add is done and does not add all of the tasks at once. 我在这里遇到一个问题,即并行的foreach在第一次添加完成后就完成了,并且不会一次添加所有任务。

var calculationTasks = new ConcurrentBag<Task>();
var allData = await hierarchy.getAllData();
var timestampGotAllData = DateTime.Now;
Parallel.ForEach(allData, item => {
    Console.WriteLine("trying parallel foreach");
    calculationTasks.Add(item.getContactStats(start, end, breakdown));
});

Console.WriteLine("about to await all the tasks");
//await the tasks to complete for calculations
await AsyncHelper.waitAll(calculationTasks);

* EDIT 6 * For visual, I ran the code and did some output to show the weirdness going on. *编辑6 *对于视觉,我运行了代码并进行了一些输出以显示异常情况。 The code executing is as follows: 执行的代码如下:

foreach (var d in allData) {
    Console.WriteLine("Adding call to bag");
    calculationTasks.Add(d.getContactStats(start, end, breakdown));
    Console.WriteLine("Done adding call to bag");
}

The output is: https://i.imgur.com/3y5S4eS.png 输出为: https : //i.imgur.com/3y5S4eS.png

Since it's printing "CALLED" every time, then "Done!" 由于它每次都打印“ CALLED”,因此“完成!” before "Done adding call to bag", these executions are happening sequentially, not async as expected. 在“将呼叫添加到袋中”之前,这些执行顺序发生,而不是按预期异步。

My gut instinct on this is that it will be something to do with the transaction that you are opening within your method. 我的直觉是,这与您在方法中打开的事务有关。 It's a little hard to tell exactly what is going on within your code as there seem to be a few custom classes here - but is there potentially some locking going on as you open your transaction? 由于这里似乎有一些自定义类,因此很难确切地说明代码中正在发生什么—但是在打开事务时是否可能会发生一些锁定? As this happens prior to your first await, it will have to run 'sequentially' prior to the code being awaited. 由于这是在您第一次等待之前发生的,因此必须在等待代码之前“顺序”运行。

Your custom 'waitall' method doesn't seem to be the problem, but you should consider removing this and using the built in Task.WhenAll to await these asynchronously. 您的自定义“ waitall”方法似乎不是问题,但您应考虑删除此方法,并使用内置的Task.WhenAll异步等待它们。

Try this: 尝试这个:

foreach (var d in allData) 
{
    calculationTasks.Add(Task.Run(() => d.getContactStats(start, end, breakdown)));
}

//Other code here
//...

Task.WaitAll(calculationTasks.ToArray());

We are essentially creating a task that will "run" your method. 实际上,我们正在创建一个任务,它将“运行”您的方法。 We then await for those tasks to complete. 然后,我们等待这些任务完成。

Admittedly, I am not entirely sure why your version blocks, but this seems to do the trick. 诚然,我不能完全确定为什么您的版本会被阻止,但这似乎可以解决问题。

UPDATE: 更新:

I tested by outputting the thread id and the OP's version executes the tasks on the same thread. 我通过输出线程ID进行了测试,OP的版本在同一线程上执行任务。 Perhaps the thread is being locked by the bag, which forces the new tasks to wait? 也许线程被袋子锁定了,这迫使新任务等待吗? My proposed solution results on different thread ids which I think explains why it doesn't block. 我提出的解决方案基于不同的线程ID,我认为这解释了为什么它不会阻塞。

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

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