【问题标题】:Task.WaitAll, get all exceptions and individual task durationsTask.WaitAll,获取所有异常和单个任务持续时间
【发布时间】:2018-06-04 13:11:33
【问题描述】:

我正在开发一个实用程序,它将使用 ExecuteNonQueryAsync 并行执行多个 SqlCommand。该实用程序应仅在所有请求完成后才返回,因此我使用的是 WaitAll。

事实上,这个实用程序是一个 SQL CLR 存储过程。 这排除了 Task.Run() 的使用

我想捕获所有发生的个别异常,这似乎工作正常。我现在正在尝试添加获取数组中每个单独任务的已用持续时间的功能。

这里是代码的核心没有任何经过的时间信息:

// the commands array has been populated with an array of strings (TSQL statemements to execute)
Task<int>[] tasks = new Task<int>[commands.Length];

try {
    for (int i = 0; i < commands.Length; i++) {
        // executeSQL is an asyc method which makes the appropriate connection and returns await ExecuteNonQueryAsync
        tasks[i] = executeSql(commands[i], connectionString, (int)commandTimeout);
    }
    Task.WaitAll(tasks);
} catch (AggregateException) {
    // exceptions reported in GetResults
} finally { 
    //GetResults builds a datatable of { command, result, exception text }
    var results = GetResults(commands, tasks);
}

现在,我正在努力弄清楚如何获取每个单独的 ExecuteNonQueryAsync 请求的经过时间。

我的第一个想法是创建一个秒表数组,并在executeSQL调用中添加一个秒表参数,然后将手表数组传递给GetResults函数:

Task<int>[] tasks = new Task<int>[commands.Length];
Stopwatch[] watches = new Stopwatch[commands.Length];

try {
    for (int i = 0; i < commands.Length; i++) {
        tasks[i] = executeSql(commands[i], connectionString, (int)commandTimeout, watches[i]);
    }
    Task.WaitAll(tasks);
} catch (AggregateException) {
    // exceptions reported in finally
} finally {
    var results = GetResults(commands, tasks, watches);
}

我可以在 executeSQL 函数中调用 ExecuteNonQueryAsync 之前启动秒表。但是我什么时候可以停止呢?

就我试图解决问题而言。我想我会添加一个延续来停止秒表,然后等待延续。但此代码无效(无法将类型“System.Runtime.CompilerServices.ConfiguredTaskAwaitable>”隐式转换为“System.Threading.Tasks.Task”

var tasks = new Task<int>[commands.Length];
var watches = new System.Diagnostics.Stopwatch[commands.Length];

try {
    for (int i = 0; i < commands.Length; i++) {
        tasks[i] = executeSql(commands[i], connectionString, (int)commandTimeout, watches[i]).ContinueWith(
            t => {
                watches[i].Stop();
                return t;
            },
            TaskContinuationOptions.AttachedToParent
        ).ConfigureAwait(false);
    }
    Task.WaitAll(tasks);
} catch (AggregateException) {
    // ...

【问题讨论】:

  • Task 分配给一个变量,ConfigureAwait 在下一行的该变量上,然后将其添加到您的数组中。因为您已经将所有内容都写在了一行上,所以您正试图将 ConfigureAwait 的返回值添加到您的 tasks 数组中。
  • 干杯科林。我发现试图确定该语言何时“神奇地”为您解决问题,以及何时不使用异步。我的意思是,一方面你所说的似乎非常明显,但另一方面,返回 Task 的异步函数实际上不能返回 Task,而返回 Task 的异步函数可以'实际上并未分配给 Task。例如:watch.Start(); Task&lt;int&gt; t = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); watch.Stop(); return t; 无效!。
  • 这是不可分配的,因为您正在等待Task,然后尝试将其结果(即int)分配给Task&lt;int&gt;,而实际上它只是int,因为@ 987654333@ 已等待作业完成,因此打开了 .Result 对象。你真正想要的是watch.Start(); Task&lt;int&gt; t = cmd.ExecuteNonQueryAsync().ContinueWith(task =&gt; { watch.Stop() return task.Result; }); t.ConfigureAwait(false); return (await t); await 不需要括号,但为了清楚起见添加了。
  • 是的,我开始明白“魔法”语言实际上只存在于结果的任务化中。我仍然不太明白如何分配一个 int 作为结果,返回 int,然后语言会以某种方式计算出如果发生异常,这个 int 会以某种方式“携带”异常,因为它已被任务化。这真是令人困惑。但无论如何,虽然我认为你所拥有的会起作用,但我取得了相同的结果,但略有不同。立即添加答案
  • 是的,我想我理解了它背后的想法:“等待”将任务解包到一个 int,这就是为什么你必须将返回一个 int 任务的函数的返回值分配给一个 int 而不是 int 的任务。但是,如果确实发生了异常,那么它本身异步的等待函数仍将返回一个任务,因此调用者可以使用异常。这对我来说有点道理,但状态机的魔力有点奇怪:) 谢谢你的帮助。

标签: c# asynchronous exception-handling stopwatch


【解决方案1】:

嗯,答案是“简单”的解决方案确实有效,尽管我预计它不会。

下面是我对 executeSQL 函数的新定义,以及它是如何被调用的。

让我头疼的是异常处理。我想如果语言本身将我的结果转化为任务,并在需要时神奇地将异常附加到任务中的这些结果,那么,当然,如果我只是以正常方式自己抛出异常(如根据下面的 finally 块)会发生意想不到的事情。 (事实上​​ ExecuteNonQueryAsync 返回一个 Task,但我必须将结果分配给一个 int - 而不是 Task - 对我来说仍然很奇怪,我喜欢它.)

但是不!它似乎有效。

static async internal Task<int> executeSql(string tsql, string connectionString, int commandTimeout, System.Diagnostics.Stopwatch watch) {
    int i = -1;
    Exception ex = null;
    try {
        using (var con = new SqlConnection(connectionString)) {
            con.Open();
            using (var cmd = new SqlCommand("set xact_abort on;" + tsql, con)) {
                cmd.CommandType = System.Data.CommandType.Text;
                cmd.CommandTimeout = commandTimeout;
                watch.Start();
                i = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
            }
        }
    } catch (Exception e) {
        ex = e;
    } finally {
        watch.Stop();
        if (ex != null) { throw ex; }
    }
    return i;
}

// ....

var tasks = new Task<int>[commands.Length];
var watches = new System.Diagnostics.Stopwatch[commands.Length];

try {
    for (int i = 0; i < commands.Length; i++) {
        watches[i] = new System.Diagnostics.Stopwatch();
        tasks[i] = executeSql(commands[i], connectionString, (int)commandTimeout, watches[i]);
    }
    Task.WaitAll(tasks);
} catch (AggregateException) { 
    // ...

【讨论】:

  • 稍作修改,您不需要捕获异常并重新抛出,因为finally 块将始终执行,因此您可以简单地将代码压缩为try-finally。使用throw ex 也不是一个好主意,因为它不会保留实际错误的堆栈跟踪,而是您的堆栈跟踪将从您的调用方法开始 - 并在executeSql 结束。为了保留堆栈跟踪,正确使用的语法是 throw,但只能在 catch 块内使用。
  • 另一条评论,如果你使用异步编程,那么你应该使用Task.WhenAll(Task[]),而不是你可以使用await,而Task.WaitAll(Task[])是一个线程阻塞操作。
  • 关于 try/catch/finally 与 try/finally 的优点。碰巧我实际上是故意减少堆栈跟踪,应该提到这一点。
  • 关于 waitall 与 whenall - 这实际上是 sql clr 存储过程的代码,它不能在 TSQL 中等待,因此是 WaitAll。但是,是的,我已经在几个地方看到了你的建议(在我为期几周的异步补习班......)并且在更普遍地使用异步时肯定会记住这一点!你是好信息的源泉!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-05-12
  • 1970-01-01
  • 2012-03-14
  • 2019-04-08
  • 2021-12-27
  • 2020-02-10
  • 2016-08-07
相关资源
最近更新 更多