【问题标题】:How do to asynchronous I/O bound work correctly异步 I/O 绑定如何正确工作
【发布时间】:2020-11-04 10:30:46
【问题描述】:

我想完全了解如何在 TAP 中实现 async 和 await 关键字。我遇到了这个 MSDN 条目:https://docs.microsoft.com/en-us/dotnet/csharp/async

在这篇文章中,您可以阅读以下两个陈述:

对于 I/O 绑定代码,您等待在异步方法中返回任务或任务的操作。

在编写任何代码之前,您应该问两个问题:

您的代码会“等待”某事,例如来自数据库的数据吗?

如果您的回答是“是”,那么您的工作是 I/O 绑定的。

您的代码会执行昂贵的计算吗?

如果您回答“是”,那么您的工作受 CPU 限制。

如果您的工作受 I/O 限制,请使用 async 和 await 而不使用 Task.Run。您不应使用任务并行库。其原因在 Async in Depth 中有概述。

链接的文章是https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth

但我不明白,为什么我不应该使用 Task.Run 从数据库中获取数据。

我的意思是对于大多数方法,我可以简单地使用异步计数器部分,例如 ExecuteNonQueryAsyncExecuteScalarAsync

但是我应该如何使用数据适配器来获取 DataTable。考虑以下方法

public DataTable SelectData(string selectCommand)
{
    OpenConnection();
    DataTable data = new DataTable();
    Command.CommandType = CommandType.Text;
    Command.CommandText = selectCommand;

    using (MySqlDataAdapter adapter = new MySqlDataAdapter(Command))
    {
        adapter.Fill(data);
    }
    CloseConnection();
    return data;
}

我建议使用以下方法来异步获取我的数据,但根据 MSDN 上的帖子,我不知道这是否是正确的方法。

public async Task<DataTable> SelectDataAsync(string selectCommand)
{
    return await Task.Run(() => SelectData(selectCommand));
}

此外,考虑以下方法:

public void Query(string sqlCommand)
{
    OpenConnection();
    Command.CommandText = sqlCommand;
    Command.CommandType = CommandType.Text;
    Command.ExecuteNonQuery();
    CloseConnection();
}

如果我想同时支持异步和同步,我该如何编写这个方法?我的意思是我可以写两次相同的代码,但我没有调用同步方法,而是使用异步方法。

public async QueryAsync(string sqlCommand)
{
    OpenConnection();
    Command.CommandText = sqlCommand;
    Command.CommandType = CommandType.Text;
    await Command.ExecuteNonQueryAsync();
    CloseConnection();
}

同样的场景。我不应该将 Task.Run 用于 I/O-Bound 工作,但我不想两次编写几乎相同的代码。

谁能澄清如何正确地做到这一点?

【问题讨论】:

  • 但我不明白,为什么我应该使用 Task.Run 从数据库中获取数据。 你应该使用 awaitasync 进行交互一个数据库,而不是 Task.Run,你可能弄错了。
  • 调用者中的代码无法将使用非异步版本的 Query 转换为您的 QueryAsync 函数。减少重复的一种选择是使 QueryAsync 成为主要功能,并在使用 Query 的地方用 Task.Result 包装它。
  • @imsmn 哦,对不起,我应该放在那里而不是不应该放在那里。我编辑了我的主要帖子。
  • “我建议使用以下方法来获取我的数据异步” - 这种方法不会真正让您的数据“异步”。您当前的线程在等待Task.Run 的结果时被释放,但另一个线程池线程(Task.Run)被阻塞,等待SelectData 完成,所以最后 - 什么都没有实现。如果你想要两个版本 - 最好只留下Async vesrion,因为调用者可以在适当的情况下同步等待它的结果,但是你不能从同步版本中获取异步版本(Task.Run 没有按照描述的那样做)。

标签: c# asynchronous async-await


【解决方案1】:

MySqlDataAdapter 有一个FillAsync 方法。您可以将代码替换为:

public async Task<DataTable> SelectData(string selectCommand)
{
    using(var conn=new MySqlConnection(...))
    using( var cmd=new MySqlCommand(selectCommand,conn))
    using (MySqlDataAdapter adapter = new MySqlDataAdapter(cmd))
    {

        DataTable data = new DataTable()
        await adapter.FillAsync(data);
        return data;
    }
}

我删除了 OpenConnection()CloseConnection() 方法,因为它们保证连接泄漏。即使出现异常,连接也应该是短暂的和关闭的。在 using 块内创建它们,所有示例都显示保证它们将被关闭和处置,即使在 finally 不会被调用的情况下。

使用 Dapper 的强类型结果

虽然不是使用弱类型的 DataTable,但也许更好的解决方案是使用例如 Dapper 并返回一个强类型的集合,例如:

public async Task<List<Customers>> SelectData(string selectCommand)
{
    using(var conn=new MySqlConnection(...))
    {
        var results=await conn.QueryAsync<Customers>(selectCommand);
        return results.ToList();
    }
}

使用 Dapper 可以轻松使用参数化查询,通过匿名类型指定参数,例如:

var publishers = await connection.QueryAsync<Publisher>(
    "select * from Publishers where Name = @name", 
    new { Name = "O'Reilly" });

尝试将该名称与字符串连接一起使用 ....

如果你真的想要动态结果,你可以使用dynamic而不是特定类型。

【讨论】:

  • 我不担心打开和关闭连接。这是由我持有该方法的班级管理的。我应该在我的帖子中说明这一点。
  • @MarvinKlein 你应该担心,因为你的代码已经泄漏了连接。人们使用这些方法和全局连接/命令对象是有原因的。您可以创建一个返回新连接对象的方法,但当Dispose() 已经关闭连接时,不需要CloseConnection()
  • 我的班级实现 IDisposable 并调用 Command?.Dispose();和 Connection?.Dispose();关于破坏。打开和关闭的工作方式相同。 if (Command?.Connection?.State != ConnectionState.Open) { Command?.Connection?.Open(); }
  • @MarvinKlein 除了加宽连接、命令数据适配器使用的读取器的范围之外,它还能提供什么?这不是隐藏复杂性或封装数据访问。
【解决方案2】:

但我不明白,为什么我不应该使用 Task.Run 从数据库中获取数据。

因为这只是转移问题而不是解决问题;它会在等待 I/O 时阻塞一个 ThreadPool 线程而不是当前线程。

如果您的目标是桌面应用程序,这可能不是一个主要问题,但它会扼杀网络应用程序的可扩展性。

如果我想同时支持异步和同步,我该如何编写这个方法?

通常,如果您需要同时支持这两种范式,则需要进行一些重复,但是,您仍然可以重用一些通用部分:

private void CreateCommand(string sqlCommand)
{
    OpenConnection();
    Command.CommandText = sqlCommand;
    Command.CommandType = CommandType.Text;
}

public void Query(string sqlCommand)
{
    CreateCommand(sqlCommand)
    Command.ExecuteNonQuery();
    CloseConnection();
}

public async Task QueryAsync(string sqlCommand)
{
    CreateCommand(sqlCommand)
    await Command.ExecuteNonQueryAsync();
    CloseConnection();
}

但是我应该如何使用数据适配器来获取 DataTable。

使用非阻塞FillAsync:

public async Task<DataTable> SelectData(string selectCommand)
{
    //...

    using (MySqlDataAdapter adapter = new MySqlDataAdapter(Command))
    {
        await adapter.FillAsync(data);
    }
    CloseConnection();
    return data;
}

或者您可以将ExecuteReaderAsyncDataTable.Load 结合使用:

public async Task<DataTable> SelectData(string selectCommand)
{
    //...

    data.Load(await Command.ExecuteReaderAsync());
    
    CloseConnection();
    return data;
}

【讨论】:

  • 您好乔纳森,感谢您的回复。并非每个 DataAdapter 都支持 FillAsync。它适用于 MysqlDataAdapter,但不适用于 FbDataAdapter 处理 Firebird 数据库。我只使用了 MySqlDataApdater,因为大多数人都知道它。但是我也不知道MySql也支持这种方法。如果 FillAsync 不可用,您知道其他类型的解决方案吗?
  • 您可以将ExecuteReaderAsyncDataTable.Load 结合使用。
猜你喜欢
  • 2018-12-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-19
  • 1970-01-01
相关资源
最近更新 更多