异步函数阻塞了我的WPF应用程序。

3

我正在尝试了解async/await。所以我尝试编写一个C#/WPF程序,异步查询数据库而不会阻塞我的GUI。

我创建了一个实现INotifyPropertyChanged接口的对象。该对象提供了一个DataTable属性,这应该由我的异步函数更改。我的GUI组件绑定到DataTable属性。

我的对象看起来像这样:

public class AsyncDataDemo : INotifyPropertyChanged
{
    protected DataTable data = new DataTable();

    public DataTable Data
    {
        get { return data; }
        protected set
        {
            data = value;
            doPropertyChanged("Data");
        }
    }

    protected virtual void doPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChangedEventArgs Arguments = new PropertyChangedEventArgs(propertyName);
            PropertyChanged(this, Arguments);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected async Task<DataTable> OpenQueryAsync(string ConnectionString, string Query)
    {
        OdbcConnection connection = new OdbcConnection(ConnectionString);
        await connection.OpenAsync().ConfigureAwait(false);

        OdbcCommand command = new OdbcCommand(Query, connection);
        DbDataReader dataReader = await command.ExecuteReaderAsync().ConfigureAwait(false);

        DataTable resultData = new DataTable();
        resultData.Load(dataReader);
        connection.Close();
        return resultData;
    }

    public async void RunQueryAsync(string Query)
    {           
            Data = await OpenQueryAsync("<ConectionString>", (Query as string)).ConfigureAwait(false);          
    }
}

在按钮点击事件中,我调用:

private void Button_Click(object sender, RoutedEventArgs e)
{
    data.RunQueryAsync("SELECT * FROM BigTable");
}

这个功能基本正常,但有一个例外:按钮点击会阻塞我的GUI,直到数据加载完成,我不明白为什么。
请问有人能给我解释一下吗?我不明白为什么异步函数不能异步运行?
谢谢!
2个回答

4
您所遇到的行为问题的原因是DbConnectionDbCommand类中存在一个缺陷,所有ADO提供程序都将其用作其特定类的基础。而这个缺陷是默认情况下所有 Async 方法都是同步的!甚至已经有文档说明了!
例如,DbConnnection.OpenAsync文档中写道:

默认实现调用同步的Open方法并返回一个已完成的任务。

以及DbCommand.ExecuteDbDataReaderAsync 文档中写道:

默认实现调用同步的ExecuteReader方法并返回一个已完成的任务,阻塞调用线程

据我所见,只有SqlServer提供程序使用真正的异步实现覆盖了异步方法。但由于您使用的是OleDb提供程序,您就没那么幸运了。

非常感谢您提供的非常有用的解释,我会牢记在心。 - user5914638
@Mike 如果不清楚的话(结合两个答案中的信息),当异步函数同步完成时,使用.ConfigureAwait(false)对代码流程没有影响,即使你设置了.ConfigureAwait(false),在await调用之后仍然会停留在UI线程上。 - Scott Chamberlain

4

您真的需要通过调试器逐步了解哪个步骤花费了很长时间。最有可能的候选者是resultData.Load(dataReader);,如果您调用的每个函数都返回一个已经具有.IsCompleted == true的任务,则代码仍然可能在UI线程上运行。

如果任务已经完成并且您使用await,即使您使用了ConfigureAwait(false),您仍然停留在UI线程上。需要发生的全部是OpenAsync()ExecuteReaderAsync()需要非常快速地完成或同步完成(两者都非常可能)。

一种解决方法是将查询放在后台线程上启动,而不是等待ConfigureAwait(false)为您执行它。

public async void RunQueryAsync(string Query)
{           
        Data = await Task.Run(() => OpenQueryAsync("<ConectionString>", (Query as string)));          
}

我还删除了 ConfigureAwait(false),因为您希望您的 INotifyPropertyChanged 在 UI 线程上发生。

此外,您真的需要处理您的可处理对象。

protected async Task<DataTable> OpenQueryAsync(string ConnectionString, string Query)
{
    using(OdbcConnection connection = new OdbcConnection(ConnectionString))
    {    
        await connection.OpenAsync().ConfigureAwait(false);

        using(OdbcCommand command = new OdbcCommand(Query, connection))
        using(DbDataReader dataReader = await command.ExecuteReaderAsync().ConfigureAwait(false))
        {
            DataTable resultData = new DataTable();
            resultData.Load(dataReader);
            return resultData;
        }
    }
}

谢谢,那个解决了问题。还要感谢您提醒我关于可处理对象的提示。我会做得更好的。 - user5914638

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