使用事务的批量插入在异步进程中阻塞 UI
Bulk insert using transactions is blocking UI on async process
尝试使用 async/await 和事务将多条记录插入到 MySQL 数据库中,但是它仍然导致 UI 在循环中变为 frozen/unresponsive。
看看下面的代码,我做错了什么或者如何实现这一点,以便 UI 在此过程中仍然响应。
异步方法
public static async Task AddRecords() {
foreach ( string month in Months ) {
await MakeTable( month );
string query="INSERT INTO `"+month+"` ( Caller, Started, Dialed, DurationSec, DurationMin, Cost, Location, Switch ) VALUES (@Caller, @Started, @Dialed, @DurationSec, @DurationMin, @Cost, @Location, @Switch);";
using ( MySqlConnection cn=new MySqlConnection( ConnectionString.ToString() ) ) {
await cn.OpenAsync();
using ( MySqlTransaction trans=cn.BeginTransaction() ) {
using ( MySqlCommand cmd=new MySqlCommand( query, cn, trans ) ) {
cmd.CommandType=CommandType.Text;
foreach ( Record r in CDR.Records ) {
if ( r.Started.ToString( "yyyy-MM" )==month ) {
cmd.Parameters.Clear();
cmd.Parameters.AddWithValue( "@Caller", r.Caller );
cmd.Parameters.AddWithValue( "@Started", r.Started );
cmd.Parameters.AddWithValue( "@Dialed", r.Dialed );
cmd.Parameters.AddWithValue( "@DurationSec", r.Duration );
cmd.Parameters.AddWithValue( "@DurationMin", Math.Ceiling( r.Duration/60 ) );
cmd.Parameters.AddWithValue( "@Cost", r.Cost );
cmd.Parameters.AddWithValue( "@Location", r.Location );
cmd.Parameters.AddWithValue( "@Switch", r.Switch.ToString() );
cmd.ExecuteNonQuery();
}
}
trans.Commit();
}
}
await cn.CloseAsync();
}
}
}
关于如何调用的片段:
private async void button1_Click( object sender, EventArgs e ) {
this.Text = "Adding Records";
await AddRecords();
this.Text = "Completed";
}
顺便说一句,when UI 是阻塞的,它不应该在执行完所有先前的代码之后阻塞。例如,在上面的按钮点击方法中,第一个'this.Text'没有设置,因为await AddRecords();
一执行,它就发生在UI有机会完成更新之前,并且没有'直到阻塞完成后才会完成,这导致只有 this.Text - "Completed"
在 UI 级别被注意到。
更新
UI 在将 cmd.ExecuteNonQuery();
修改为 await cmd.ExecuteNonQueryAsync();
后仍然阻塞(由 推荐),这让我相信它在 trans.Commit();
上阻塞行或与交易的建立方式有关的事情。
更新代码
public static async Task AddRecords() {
foreach ( string month in Months ) {
await MakeTable( month );
string query="INSERT INTO `"+month+"` ( Caller, Started, Dialed, DurationSec, DurationMin, Cost, Location, Switch ) VALUES (@Caller, @Started, @Dialed, @DurationSec, @DurationMin, @Cost, @Location, @Switch);";
using ( MySqlConnection cn=new MySqlConnection( ConnectionString.ToString() ) ) {
await cn.OpenAsync();
using ( MySqlTransaction trans=cn.BeginTransaction() ) {
using ( MySqlCommand cmd=new MySqlCommand( query, cn, trans ) ) {
cmd.CommandType=CommandType.Text;
foreach ( Record r in CDR.Records ) {
if ( r.Started.ToString( "yyyy-MM" )==month ) {
cmd.Parameters.Clear();
cmd.Parameters.AddWithValue( "@Caller", r.Caller );
cmd.Parameters.AddWithValue( "@Started", r.Started );
cmd.Parameters.AddWithValue( "@Dialed", r.Dialed );
cmd.Parameters.AddWithValue( "@DurationSec", r.Duration );
cmd.Parameters.AddWithValue( "@DurationMin", Math.Ceiling( r.Duration/60 ) );
cmd.Parameters.AddWithValue( "@Cost", r.Cost );
cmd.Parameters.AddWithValue( "@Location", r.Location );
cmd.Parameters.AddWithValue( "@Switch", r.Switch.ToString() );
await cmd.ExecuteNonQueryAsync();
}
}
trans.Commit();
}
}
await cn.CloseAsync();
}
}
}
使用ExecuteNonQueryAsync
代替ExecuteNonQuery
:
await cmd.ExecuteNonQueryAsync();
这是您查询中最耗时的调用,因为您使用的是同步版本,所以被同步阻塞。
编辑:
由于您不需要做任何需要同步上下文的 UI 工作,您可以使用 ConfigureAwait(false)
,您可以将其应用到 OpenAsync
,然后继续运行 在线程池 IO 工作者上。
我觉得大部分时间都花在了Commit
操作上。问题是 AddRecords
函数中的每个 await
都会同步回调用该函数的 UI 线程。那是你的主要问题。
与这个给您带来麻烦的 SynchronizationContext
断开连接的最简单方法是在 ThreadPool
-线程中强制 AddRecords
到 运行。
这样做很简单:
private async void button1_Click( object sender, EventArgs e ) {
this.Text = "Adding Records";
await Task.Run(() => AddRecords());
this.Text = "Completed";
}
这将 运行 您在 ThreadPool
中的数据库内容,只有您在此处创建的 Task
同步回 UI 一切完成后。
不利的一面是,您无法再从 AddRecords
方法中访问 UI。但是在你的代码中你没有这样做,所以我想没关系。
尝试使用 async/await 和事务将多条记录插入到 MySQL 数据库中,但是它仍然导致 UI 在循环中变为 frozen/unresponsive。
看看下面的代码,我做错了什么或者如何实现这一点,以便 UI 在此过程中仍然响应。
异步方法
public static async Task AddRecords() {
foreach ( string month in Months ) {
await MakeTable( month );
string query="INSERT INTO `"+month+"` ( Caller, Started, Dialed, DurationSec, DurationMin, Cost, Location, Switch ) VALUES (@Caller, @Started, @Dialed, @DurationSec, @DurationMin, @Cost, @Location, @Switch);";
using ( MySqlConnection cn=new MySqlConnection( ConnectionString.ToString() ) ) {
await cn.OpenAsync();
using ( MySqlTransaction trans=cn.BeginTransaction() ) {
using ( MySqlCommand cmd=new MySqlCommand( query, cn, trans ) ) {
cmd.CommandType=CommandType.Text;
foreach ( Record r in CDR.Records ) {
if ( r.Started.ToString( "yyyy-MM" )==month ) {
cmd.Parameters.Clear();
cmd.Parameters.AddWithValue( "@Caller", r.Caller );
cmd.Parameters.AddWithValue( "@Started", r.Started );
cmd.Parameters.AddWithValue( "@Dialed", r.Dialed );
cmd.Parameters.AddWithValue( "@DurationSec", r.Duration );
cmd.Parameters.AddWithValue( "@DurationMin", Math.Ceiling( r.Duration/60 ) );
cmd.Parameters.AddWithValue( "@Cost", r.Cost );
cmd.Parameters.AddWithValue( "@Location", r.Location );
cmd.Parameters.AddWithValue( "@Switch", r.Switch.ToString() );
cmd.ExecuteNonQuery();
}
}
trans.Commit();
}
}
await cn.CloseAsync();
}
}
}
关于如何调用的片段:
private async void button1_Click( object sender, EventArgs e ) {
this.Text = "Adding Records";
await AddRecords();
this.Text = "Completed";
}
顺便说一句,when UI 是阻塞的,它不应该在执行完所有先前的代码之后阻塞。例如,在上面的按钮点击方法中,第一个'this.Text'没有设置,因为await AddRecords();
一执行,它就发生在UI有机会完成更新之前,并且没有'直到阻塞完成后才会完成,这导致只有 this.Text - "Completed"
在 UI 级别被注意到。
更新
UI 在将 cmd.ExecuteNonQuery();
修改为 await cmd.ExecuteNonQueryAsync();
后仍然阻塞(由 trans.Commit();
上阻塞行或与交易的建立方式有关的事情。
更新代码
public static async Task AddRecords() {
foreach ( string month in Months ) {
await MakeTable( month );
string query="INSERT INTO `"+month+"` ( Caller, Started, Dialed, DurationSec, DurationMin, Cost, Location, Switch ) VALUES (@Caller, @Started, @Dialed, @DurationSec, @DurationMin, @Cost, @Location, @Switch);";
using ( MySqlConnection cn=new MySqlConnection( ConnectionString.ToString() ) ) {
await cn.OpenAsync();
using ( MySqlTransaction trans=cn.BeginTransaction() ) {
using ( MySqlCommand cmd=new MySqlCommand( query, cn, trans ) ) {
cmd.CommandType=CommandType.Text;
foreach ( Record r in CDR.Records ) {
if ( r.Started.ToString( "yyyy-MM" )==month ) {
cmd.Parameters.Clear();
cmd.Parameters.AddWithValue( "@Caller", r.Caller );
cmd.Parameters.AddWithValue( "@Started", r.Started );
cmd.Parameters.AddWithValue( "@Dialed", r.Dialed );
cmd.Parameters.AddWithValue( "@DurationSec", r.Duration );
cmd.Parameters.AddWithValue( "@DurationMin", Math.Ceiling( r.Duration/60 ) );
cmd.Parameters.AddWithValue( "@Cost", r.Cost );
cmd.Parameters.AddWithValue( "@Location", r.Location );
cmd.Parameters.AddWithValue( "@Switch", r.Switch.ToString() );
await cmd.ExecuteNonQueryAsync();
}
}
trans.Commit();
}
}
await cn.CloseAsync();
}
}
}
使用ExecuteNonQueryAsync
代替ExecuteNonQuery
:
await cmd.ExecuteNonQueryAsync();
这是您查询中最耗时的调用,因为您使用的是同步版本,所以被同步阻塞。
编辑:
由于您不需要做任何需要同步上下文的 UI 工作,您可以使用 ConfigureAwait(false)
,您可以将其应用到 OpenAsync
,然后继续运行 在线程池 IO 工作者上。
我觉得大部分时间都花在了Commit
操作上。问题是 AddRecords
函数中的每个 await
都会同步回调用该函数的 UI 线程。那是你的主要问题。
与这个给您带来麻烦的 SynchronizationContext
断开连接的最简单方法是在 ThreadPool
-线程中强制 AddRecords
到 运行。
这样做很简单:
private async void button1_Click( object sender, EventArgs e ) {
this.Text = "Adding Records";
await Task.Run(() => AddRecords());
this.Text = "Completed";
}
这将 运行 您在 ThreadPool
中的数据库内容,只有您在此处创建的 Task
同步回 UI 一切完成后。
不利的一面是,您无法再从 AddRecords
方法中访问 UI。但是在你的代码中你没有这样做,所以我想没关系。