【问题标题】:Async I/O intensive code is running slower than non-async, why?异步 I/O 密集型代码的运行速度比非异步慢,为什么?
【发布时间】:2015-04-17 03:34:09
【问题描述】:

我正在重构应用程序并尝试添加现有函数的异步版本以提高 ASP.NET MVC 应用程序的性能时间。我知道异步函数会产生开销,但我希望通过足够的迭代,从数据库加载数据的 I/O 密集型性质将足以弥补开销损失,并且我将获得显着的性能提升。

TermusRepository.LoadByTermusId 函数通过从数据库中检索一堆数据表来加载数据(使用 ADO.NET 和 Oracle 托管客户端),填充模型并返回它。 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 我的期望是单个用户访问该站点。如果他发出一个运行我的非异步代码的请求,我希望它的执行速度比他向我的异步代码发出请求要慢几倍。忽略多个用户同时访问该站点的可能性。所以我相信我对你问题的回答是单线程的。
  • @mason 我建议您使用JMeter 之类的东西来测试性能结果,以对您的站点进行压力测试。使用单线程访问,您会看到更多的开销是合乎逻辑的。
  • @mason 在这种情况下,没有理由相信async 会更快。看看这个问题:How to measure performance of awaiting asynchronous operations?
  • @i3arnon 你在回答这个问题时说过To measure that you need to have a lot of async operations concurrently,但我相信我有。大约有 60 个答案要检索,我将它们全部排入DownloadAnswersTasks 并等待它们返回。我的理解是数据库操作是一起启动的,然后我等待每个操作返回并在它们返回后立即处理它们。

标签: c# oracle asynchronous async-await


【解决方案1】:

在没有并发的情况下,异步版本总是比同步版本慢。它所做的工作与非异步版本相同,但增加了少量开销来管理异步。

异步在性能方面是有利的,因为它可以提高可用性。每个单独的请求都会变慢,但如果您同时发出 1000 个请求,异步实现将能够更快地处理它们(至少在某些情况下)。

发生这种情况是因为异步解决方案允许分配用于处理请求的线程返回到池中并处理其他请求,而同步解决方案迫使线程坐在那里并且在等待异步操作时什么都不做去完成。以允许线程被释放来做其他工作的方式来构建程序是有开销的,但优点是该线程能够去做其他工作。在您的程序中,线程没有其他工作要做,因此最终成为净损失。

【讨论】:

  • 我不同意。对于 I/O 绑定进程,异步实现仍然会更慢。因为它是做边界的 I/O。您会从响应速度更快的 UI 中获得明显的性能提升。
  • @Aron UI 不再响应 ASP 应用程序的异步请求处理。有非常真实的潜在性能提升,但是它们来自线程池线程被重新用于其他请求的能力,这在这个测试中当然不会发生,因为没有其他请求需要处理。
  • @Aron 我的意思是,ASP 应用程序没有 消息泵。这是一个 ASP 应用程序,而不是桌面应用程序。
  • @Servy 所以你是说如果我有 1000 个用户同时运行代码,他们在异步代码上会比非异步代码快得多?在我的脑海中,我很难将这一点与我看到的一堆任务排队的一些演示相协调。我希望LoadByTermusIdAsync 中的DownloadAnswersTasks 都在某种程度上并行运行(大约有60 个答案要检索,我会更新我的问题以指出这一点)。
  • @mason 异步和并行是完全不同的概念。异步的东西并不意味着它是并行的,而同步的东西并不意味着它的序列化。当然,并行化操作也不一定总是更快,具体取决于上下文。
【解决方案2】:

原来 Oracle 托管驱动程序是 "fake async",,这可以部分解释为什么我的异步代码运行速度较慢。

【讨论】:

  • 好吧,如果您查看 OP 的代码,您会发现甚至没有使用伪造的 async — OP 正在调用 dt.Load(command.ExecuteReader())
  • @binki 我编写了原始代码。再看一遍。我提供了异步和非异步版本。
  • 啊,对不起,我去找GetDataTableAsync() 然后滚动过去。在我看来,这应该是公认的答案。
  • 他们在 7 天前发布了新版本 18.3(最后一个是 12.2)。 nuget.org/packages/Oracle.ManagedDataAccess/18.3.0 也许这个新的真的是异步的。谁能确认一下?
猜你喜欢
  • 1970-01-01
  • 2021-03-19
  • 2012-09-14
  • 2019-07-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-23
  • 2018-04-06
相关资源
最近更新 更多