异步I/O密集型代码运行比非异步的慢,为什么?

6
我正在重构一个应用程序,并尝试为现有功能添加异步版本,以提高ASP.NET MVC应用程序的性能。我知道异步函数会带来一定的开销,但我期望随着足够的迭代次数,从数据库加载数据的I/O密集型特性将超过开销惩罚,从而获得显著的性能增益。 TermusRepository.LoadByTermusId 函数通过从数据库中检索一堆数据表(使用ADO.NET和Oracle Managed Client),填充一个模型并返回它来加载数据。 TermusRepository.LoadByTermusIdAsync 类似,只是采用异步方式执行,当有多个数据表需要检索时,使用稍微不同的方法加载数据表下载任务。
public async Task<ActionResult> AsyncPerformanceTest()
{
    var vm = new AsyncPerformanceTestViewModel();
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < 60; i++)
    {
        TermusRepository.LoadByTermusId<Termus2011_2012EndYear>("1");
        TermusRepository.LoadByTermusId<Termus2011_2012EndYear>("5");
        TermusRepository.LoadByTermusId<Termus2011_2012EndYear>("6");
        TermusRepository.LoadByTermusId<Termus2011_2012EndYear>("7");
    }
    watch.Stop();
    vm.NonAsyncElapsedTime = watch.Elapsed;
    watch.Reset();
    watch.Start();
    var tasks = new List<Task<Termus2011_2012EndYear>>();
    for (int i = 0; i < 60; i++)
    {
        tasks.Add(TermusRepository.LoadByTermusIdAsync<Termus2011_2012EndYear>("1"));
        tasks.Add(TermusRepository.LoadByTermusIdAsync<Termus2011_2012EndYear>("5"));
        tasks.Add(TermusRepository.LoadByTermusIdAsync<Termus2011_2012EndYear>("6"));
        tasks.Add(TermusRepository.LoadByTermusIdAsync<Termus2011_2012EndYear>("7"));               
    }
    await Task.WhenAll(tasks.ToArray());
    watch.Stop();
    vm.AsyncElapsedTime = watch.Elapsed;            
    return View(vm);
}

public static async Task<T> LoadByTermusIdAsync<T>(string termusId) where T : Appraisal
{
    var AppraisalHeader = new OracleCommand("select tu.termus_id, tu.manager_username, tu.evaluee_name, tu.evaluee_username, tu.termus_complete_date, termus_start_date, tu.termus_status, tu.termus_version, tn.managername from tercons.termus_users tu left outer join tercons.termus_names tn on tu.termus_id=tn.termus_id where tu.termus_id=:termusid");
    AppraisalHeader.BindByName = true;
    AppraisalHeader.Parameters.Add("termusid", termusId);
    var dt = await Database.GetDataTableAsync(AppraisalHeader);
    T Termus = Activator.CreateInstance<T>();
    var row = dt.AsEnumerable().Single();
    Termus.TermusId = row.Field<decimal>("termus_id").ToString();
    Termus.ManagerUsername = row.Field<string>("manager_username");
    Termus.EvalueeUsername = row.Field<string>("evaluee_username");
    Termus.EvalueeName = row.Field<string>("evaluee_name");
    Termus.ManagerName = row.Field<string>("managername");
    Termus.TERMUSCompleteDate = row.Field<DateTime?>("termus_complete_date");
    Termus.TERMUSStartDate = row.Field<DateTime>("termus_start_date");
    Termus.Status = row.Field<string>("termus_status");
    Termus.TERMUSVersion = row.Field<string>("termus_version");
    Termus.QuestionsAndAnswers = new Dictionary<string, string>();

    var RetrieveQuestionIdsCommand = new OracleCommand("select termus_question_id from tercons.termus_questions where termus_version=:termus_version");
    RetrieveQuestionIdsCommand.BindByName = true;
    RetrieveQuestionIdsCommand.Parameters.Add("termus_version", Termus.TERMUSVersion);
    var QuestionIdsDt = await Database.GetDataTableAsync(RetrieveQuestionIdsCommand);
    var QuestionIds = QuestionIdsDt.AsEnumerable().Select(r => r.Field<string>("termus_question_id"));

    //There's about 60 questions/answers, so this should result in 60 calls to the database. It'd be a good spot to combine to a single DB call, but left it this way so I could see if async would speed it up for learning purposes.
    var DownloadAnswersTasks = new List<Task<DataTable>>();
    foreach (var QuestionId in QuestionIds)
    {
        var RetrieveAnswerCommand = new OracleCommand("select termus_response, termus_question_id from tercons.termus_responses where termus_id=:termus_id and termus_question_id=:questionid");
        RetrieveAnswerCommand.BindByName = true;
        RetrieveAnswerCommand.Parameters.Add("termus_id", termusId);
        RetrieveAnswerCommand.Parameters.Add("questionid", QuestionId);
        DownloadAnswersTasks.Add(Database.GetDataTableAsync(RetrieveAnswerCommand));
    }
    while (DownloadAnswersTasks.Count > 0)
    {
        var FinishedDownloadAnswerTask = await Task.WhenAny(DownloadAnswersTasks);
        DownloadAnswersTasks.Remove(FinishedDownloadAnswerTask);
        var AnswerDt = await FinishedDownloadAnswerTask;
        var Answer = AnswerDt.AsEnumerable().Select(r => r.Field<string>("termus_response")).SingleOrDefault();
        var QuestionId = AnswerDt.AsEnumerable().Select(r => r.Field<string>("termus_question_id")).SingleOrDefault();
        if (!String.IsNullOrEmpty(Answer))
        {
            Termus.QuestionsAndAnswers.Add(QuestionId, System.Net.WebUtility.HtmlDecode(Answer));
        }
    }
    return Termus;
}

public static async Task<DataTable> GetDataTableAsync(OracleCommand command)
{
    DataTable dt = new DataTable();
    using (var connection = GetDefaultOracleConnection())
    {
        command.Connection = connection;
        await connection.OpenAsync();
        dt.Load(await command.ExecuteReaderAsync());
    }
    return dt;
}

public static T LoadByTermusId<T>(string TermusId) where T : Appraisal
{
    var RetrieveAppraisalHeaderCommand = new OracleCommand("select tu.termus_id, tu.manager_username, tu.evaluee_name, tu.evaluee_username, tu.termus_complete_date, termus_start_date, tu.termus_status, tu.termus_version, tn.managername from tercons.termus_users tu left outer join tercons.termus_names tn on tu.termus_id=tn.termus_id where tu.termus_id=:termusid");
    RetrieveAppraisalHeaderCommand.BindByName = true;
    RetrieveAppraisalHeaderCommand.Parameters.Add("termusid", TermusId);
    var AppraisalHeaderDt = Database.GetDataTable(RetrieveAppraisalHeaderCommand);
    T Termus = Activator.CreateInstance<T>();
    var AppraisalHeaderRow = AppraisalHeaderDt.AsEnumerable().Single();
    Termus.TermusId = AppraisalHeaderRow.Field<decimal>("termus_id").ToString();
    Termus.ManagerUsername = AppraisalHeaderRow.Field<string>("manager_username");
    Termus.EvalueeUsername = AppraisalHeaderRow.Field<string>("evaluee_username");
    Termus.EvalueeName = AppraisalHeaderRow.Field<string>("evaluee_name");
    Termus.ManagerName = AppraisalHeaderRow.Field<string>("managername");
    Termus.TERMUSCompleteDate = AppraisalHeaderRow.Field<DateTime?>("termus_complete_date");
    Termus.TERMUSStartDate = AppraisalHeaderRow.Field<DateTime>("termus_start_date");
    Termus.Status = AppraisalHeaderRow.Field<string>("termus_status");
    Termus.TERMUSVersion = AppraisalHeaderRow.Field<string>("termus_version");
    Termus.QuestionsAndAnswers = new Dictionary<string, string>();

    var RetrieveQuestionIdsCommand = new OracleCommand("select termus_question_id from tercons.termus_questions where termus_version=:termus_version");
    RetrieveQuestionIdsCommand.BindByName = true;
    RetrieveQuestionIdsCommand.Parameters.Add("termus_version", Termus.TERMUSVersion);
    var QuestionIdsDt = Database.GetDataTable(RetrieveQuestionIdsCommand);
    var QuestionIds = QuestionIdsDt.AsEnumerable().Select(r => r.Field<string>("termus_question_id"));
    //There's about 60 questions/answers, so this should result in 60 calls to the database. It'd be a good spot to combine to a single DB call, but left it this way so I could see if async would speed it up for learning purposes.
    foreach (var QuestionId in QuestionIds)
    {
        var RetrieveAnswersCommand = new OracleCommand("select termus_response from tercons.termus_responses where termus_id=:termus_id and termus_question_id=:questionid");
        RetrieveAnswersCommand.BindByName = true;
        RetrieveAnswersCommand.Parameters.Add("termus_id", TermusId);
        RetrieveAnswersCommand.Parameters.Add("questionid", QuestionId);
        var AnswersDt = Database.GetDataTable(RetrieveAnswersCommand);
        var Answer = AnswersDt.AsEnumerable().Select(r => r.Field<string>("termus_response")).SingleOrDefault();
        if (!String.IsNullOrEmpty(Answer))
        {
            Termus.QuestionsAndAnswers.Add(QuestionId, System.Net.WebUtility.HtmlDecode(Answer));
        }
    }
    return Termus;
}

public static DataTable GetDataTable(OracleCommand command)
{
    DataTable dt = new DataTable();
    using (var connection = GetDefaultOracleConnection())
    {
        command.Connection = connection;
        connection.Open();
        dt.Load(command.ExecuteReader());
    }
    return dt;
}

public static OracleConnection GetDefaultOracleConnection()
{
    return new OracleConnection(ConfigurationManager.ConnectionStrings[connectionstringname].ConnectionString);
}

进行60次迭代的结果如下:

Non Async 18.4375460 seconds

Async     19.8092854 seconds

这个测试结果是一致的。无论我在AsyncPerformanceTest()操作方法的for循环中进行多少次迭代,异步部分的运行时间比同步部分慢约1秒钟。(我运行测试多次以考虑JITter热身.) 我做错了什么导致异步比同步更慢?我是否对编写异步代码的基本知识有误解?

你是将 async 与单线程或多线程进行比较,以得出“我将获得显着的性能提升”的结论吗? - Sinatr
@Sinatr 我的期望是一个单一用户访问该网站。如果他运行我的非异步代码并发出一个请求,我预计它会比他发出一个请求到我的异步代码慢几倍。忽略多个用户同时访问该网站的可能性。因此,我认为我的回答是单线程的。 - mason
@mason 我建议你使用类似JMeter这样的工具来对你的网站进行性能测试。通过单线程访问,很可能会出现更多的开销而不是收益。 - Yuval Itzchakov
@mason 没有理由相信在这种情况下 async 会更快。看一下这个问题:如何测量等待异步操作的性能? - i3arnon
@i3arnon,你在那个问题的回答中说:“要测量这一点,你需要同时进行许多异步操作”,但我认为我已经做到了。大约有60个答案需要检索,我将它们全部排队到“DownloadAnswersTasks”中,并等待它们返回。我的理解是数据库操作会一起启动,然后我等待每个操作返回并尽快处理它们。 - mason
显示剩余15条评论
2个回答

11

如果没有并发,异步版本始终比同步版本慢。它需要执行与非异步版本相同的所有工作,但是增加了一小部分开销来管理异步性。

通过允许改进可用性,异步化对于性能是有优势的。每个单独的请求会变得较慢,但是如果您同时进行1000个请求,异步实现将能够更快地处理它们(至少在某些情况下)。

这是因为异步解决方案允许分配给处理请求的线程返回到池中并处理其他请求,而同步解决方案则强制该线程坐在那里等待异步操作完成时无法执行其他任务。以一种允许线程被释放以执行其他工作的方式构建程序存在开销,但优点是该线程能够去做其他工作。在程序中没有其他工作可以让线程去处理,因此最终导致净损失。


我不同意。对于I/O绑定的进程来说,异步实现仍然会更慢。因为是I/O在限制速度。你所得到的是从一个更加响应的UI中获得的感知性能提升。 - Aron
1
@Aron 当ASP应用程序使用异步请求处理时,UI不再响应。虽然有非常实际的潜在性能提升,但这些提升来自线程池线程能够被重复使用于其他请求的能力,当然,在这个测试中无法发生,因为没有其他请求需要处理。 - Servy
1
@Aron 我的观点是ASP应用程序并没有一个消息泵。它是一个ASP应用程序,而不是桌面应用程序。 - Servy
3
异步和并行是完全不同的概念。某些操作异步并不意味着它被并行化了,而同步操作也不一定是串行化的。当然,在某些情况下,并行化操作也不一定更快。 - Servy
@mason 在这种情况下,异步工作速度更快,因为它允许您重复使用线程,而不是每个线程都“阻塞”(即坐在那里等待SQL服务器)。这样做的另一个好处是现代操作系统抢占式多任务处理使得线程之间的上下文切换非常昂贵(特别是当您有数千个线程时)。 - Aron
显示剩余13条评论

6
原来 Oracle 托管驱动程序是 "伪异步", 的,这在一定程度上解释了为什么我的异步代码运行较慢。

嗯,如果你看一下原帖的代码,你会发现假异步甚至没有被使用——原帖作者正在调用 dt.Load(command.ExecuteReader()) - binki
@binki 我 编写 了原始代码。请再看一遍。我提供了异步和非异步版本。 - mason
啊,抱歉,我一直在寻找 GetDataTableAsync() 然后就滑过去了。在我看来,这应该是被接受的答案。 - binki
他们在7天前发布了新版本18.3(上一个版本是12.2)。https://www.nuget.org/packages/Oracle.ManagedDataAccess/18.3.0也许这个新版本真的是异步的。有人可以确认一下吗? - lmcarreiro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接