异步代码块






4.93/5 (85投票s)
2006年10月3日
16分钟阅读

257701

1130
一个C# 2.0库,用于通过匿名方法和ManagedIOCP异步执行方法的一部分代码。
目录
1. 介绍
异步代码块(在本文中简称ACB)是一种技术和一个配套库,用于C# 2.0应用程序,可以在不显式使用线程、委托类型声明、线程处理方法、同步对象以及与异步多线程编程相关的所有复杂内容的情况下,异步运行方法代码的某些部分。
以下是ACB库提供功能的摘要
- 异步运行使用匿名方法定义的方法代码片段。
- ACB的异步执行可以安排在整个AppDomain的ManagedIOCP线程池上,或者安排在一个用户创建的ManagedIOCP线程池实例上。
- 等待ACB完成,可以无限期等待,或等待指定时间(毫秒)。
- 等待ACB列表中的所有ACB执行完成。功能3和4允许应用程序在其关闭期间等待所有待处理的ACB。
- 等待ACB列表中的任意一个ACB执行完成。
- 通过委托接收关于异步代码块执行完成的通知。
- 使一个ACB的执行依赖于另一个ACB的执行完成。例如,如果ACB Y依赖于ACB X,那么ACB Y将在ACB X完成执行后才开始执行。
- 使一个ACB的执行依赖于ACB列表中所有ACB的执行完成。
正如您已经观察到的,ACB依赖于C# 2.0中的“匿名方法”以及Sonic.Net库中的基于ManagedIOCP的任务框架和线程池类。Sonic.Net库是我自己创建的库,是我在CodeProject上撰写的ManagedIOCP文章的一部分。以下是我关于“匿名方法”和“ManagedIOCP”文章的链接。
- 深入了解C# 2.0匿名方法:本文解释了C# 2.0匿名方法的内部工作原理。
- 托管I/O完成端口(IOCP):本文详细解释了ManagedIOCP。
- 托管I/O完成端口(IOCP)-第二部分:本文详细解释了建立在ManagedIOCP之上的线程池和任务框架。
阅读以上三篇文章,将有助于ACB的读者更深入地理解ACB的工作原理,并能更有效、更高效地使用ACB。Sonic.Net库不是使用ACB所必需的。我将ManagedIOCP转换到.NET 2.0,并自定义了源代码以直接用于ACB。本ACB库附带的ManagedIOCP版本使用了.NET 2.0泛型类型来处理被释放的对象。开发者可以在其.NET 2.0应用程序中独立于ACB使用这个新的ManagedIOCP基础设施,包括其ThreadPool
类。对于那些没有阅读过我上述三篇文章的读者,或者已经阅读过但迫不及待地想使用ACB的读者,请看这里。
2. 使用异步代码块
本文附带的ZIP文件包含ACB的完整C# 2.0源代码,包括更新的ManagedIOCP类。ZIP文件包含以下文件夹。
AsynchronousCodeBlocks
|
-- AsynchronousCodeBlocks
-- WinTestAsyncCodeBlocks
如上所示的文件夹结构,根文件夹“AsynchronousCodeBlocks”包含解决方案文件,其中有两个项目:一个是AsynchronousCodeBlocks的C# 2.0库,第二个是基于WinForms的示例应用程序,演示了AsynchronousCodeBlocks库的各种功能。根文件夹还包含一个名为AsyncTestDB.sql的SQL文件,其中包含创建SQL Server 2000/2005数据库的SQL脚本,该数据库用于示例应用程序。这就是关于附件ZIP文件的全部内容。现在让我们深入了解并使用AsynchronousCodeBlocks库。
当我们想异步执行方法代码的一部分时,只需将其包装在异步类中,如下面的代码所示
private void button5_Click_1(object sender, EventArgs e)
{
SqlConnection con = new SqlConnection("server=.;" +
"database=AsyncTestDB;uid=sa;pwd=AdiTanuL1$");
con.Open();
new async(delegate
{
try
{
int age = 28;
string cmdStr = string.Format("insert into Address values" +
" ('{0}',{1},'{2}')", "Name_" + age.ToString(),
age, "Address_" + age.ToString());
SqlCommand cmd = new SqlCommand(cmdStr, con);
cmd.ExecuteNonQuery();
}
finally
{
con.Close();
}
});
}
如果您观察到上面的代码片段,除了高亮显示的代码之外,其余代码都是正常的,用于在连接对象上同步执行一个INSERT
命令。现在,高亮显示的代码允许我们将代码包装成一个异步对象实例,该实例将被立即安排在整个AppDomain的ManagedIOCP线程池上执行。包装在异步对象内的代码本身是一个匿名方法,它定义了一个名为AsyncDelegate
的委托。async
类是核心类,它在构造函数中安排被包装的匿名方法执行,并提供我们将很快讨论的各种功能。由于这些匿名方法是方法的一部分,并且是异步执行的,所以我将这种技术以及包含异步和配套类的类库命名为异步代码块。
上面显示的代码是不完整的,因为我们不知道这个异步代码块何时会完成执行。如果我们已经安排了多个这样的异步代码块执行,我们可能希望在一个或多个异步代码块完成后执行一个方法或另一段代码,或者在所有待处理的异步代码块都执行完毕之前不关闭应用程序。
ACB提供了两种处理这种情况的方式。首先,ACB允许我们同步等待异步代码块的完成。其次,ACB允许我们提供一个委托,该委托将在异步代码块执行完成后被调用。
我们可以等待ACB库中一个名为waitableasync
的类包装的异步代码块。waitableasync
类派生自async
类,并允许对其对象进行同步等待。对waitableasync
对象的等待将在其包装的异步代码块完成执行或指定等待方法的超时时间完成后结束。超时值为-1表示无限超时。下面的代码片段显示了waitableasync
类的用法。
private void button5_Click_1(object sender, EventArgs e)
{
SqlConnection con = new SqlConnection("server=.;" +
"database=AsyncTestDB;uid=sa;pwd=AdiTanuL1$");
con.Open();
waitableasync obj = new waitableasync(delegate
{
try
{
int age = 28;
string cmdStr = string.Format("insert into Address values" +
" ('{0}',{1},'{2}')", "Name_" +
age.ToString(), age, "Address_" + age.ToString());
SqlCommand cmd = new SqlCommand(cmdStr, con);
cmd.ExecuteNonQuery();
}
finally
{
con.Close();
}
});
if (obj.Wait(-1) == true)
{
MessageBox.Show("Successfully completed the insert");
}
else
{
MessageBox.Show("Failed to wait");
}
}
async
和waitableasync
类默认使用整个AppDomain的ManagedIOCP线程池来执行包装在它们对象内的匿名方法。这个ManagedIOCP线程池每个AppDomain实例化一次,并存在于AppDomain的整个生命周期内。它使用一个线程来执行由async
和waitableasync
对象发布到它的所有异步代码块(匿名方法)。之所以选择一个线程,是因为如果一个方法包含一系列异步代码块,然后我们在多个线程上执行它们,可能会导致不可预测的结果。这也与在这些异步代码块所在的方法中自然顺序执行代码不符。但是,用于执行所有异步代码块的单个线程也是有限制的,因为一个方法中的异步代码执行必须等待其他不相关方法中任何待处理的异步代码块执行完成。但是,默认的整个AppDomain的ManagedIOCP线程池方法使得ACB代码简洁,并且看起来自然地集成到包含的方法代码中。为了处理默认的整个AppDomain的ManagedIOCP线程池中的线程限制,我们可以在构造/实例化async
和waitableasync
对象时传递我们自己的ManagedIOCP线程池实例。与用户提供的ManagedIOCP线程池实例关联的async
和waitableasync
对象的异步代码块将在与用户定义的线程池相关的线程上执行。这为开发者提供了对异步代码块何时以及如何执行的精细控制。下面的代码片段显示了自定义ManagedIOCP线程池与waitableasync
类的用法。
private void button6_Click(object sender, EventArgs e)
{
int maxPrimeNum = Convert.ToInt32(textBox2.Text);
int iFirstHalfStart = 1;
int iFirstHalfEnd = maxPrimeNum / 2;
int iSecondHalfStart = iFirstHalfEnd + 1;
int iSecondHalfEnd = maxPrimeNum;
int countPrime = 0;
long st = DateTime.Now.Ticks;
waitableasync firstHalf = new waitableasync(delegate
{
for (int i = iFirstHalfStart; i <= iFirstHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
},_myThreadPool);
waitableasync secondHalf = new waitableasync(delegate
{
for (int i = iSecondHalfStart; i <= iSecondHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
},_myThreadPool);
firstHalf.Wait(-1);
secondHalf.Wait(-1);
long et = DateTime.Now.Ticks;
MessageBox.Show(string.Format("Done: {0}, Time: {1}",
countPrime,(et-st)/10000));
}
private Sonic.Net.ThreadPool _myThreadPool =
new Sonic.Net.ThreadPool(5, 2);
如果您看到上面的代码,我们正在使用一个自定义的ManagedIOCP ThreadPool
对象,该对象最多有五个线程和两个并发线程。这允许上述代码在多个线程上运行。使用多线程最适合上面所示的示例代码,其中识别素数的顺序并不重要。通过使用多线程ManagedIOCP ThreadPool
,应用程序的性能将显著提高。例如,在我的笔记本电脑上,其配置为Intel Centrino Duo 1.66 GHz和500 MB RAM,上述代码平均需要2100毫秒才能完成。如果我注释掉waitableasync
对象的创建,并将上述代码作为常规同步操作运行,它平均需要3900毫秒。异步方法实现了100%的性能提升。
如上面的代码示例所示,我们可以独立地等待waitableasync
对象的完成,或者创建一个数组并等待waitableasync
对象的数组。waitableasync
类使用一个Event
对象来实现对其实例包装的异步代码块的执行完成的等待。因此,如果我们想使用WaitHandle.WaitAll
等待多个waitableasync
对象,最多只能等待64个句柄。出于这个原因,waitableasync
提供了两种不同的方法来等待waitableasync
对象的数组。一种方法WaitAll
使用WaitHandle.WaitAll
方法等待waitableasync
对象的数组。此方法最多只能等待64个waitableasync
对象,这是每个waitableasync
对象内部使用的事件对象上的WaitHandle
的基本限制。此外,WaitHandle.WaitAll
不能在STA线程中使用,而STA线程是WinForms运行的典型线程模型。第二种方法WaitAllEx
使用简单的算法来等待waitableasync
对象的数组。此方法对它可以等待的waitableasync
对象的数量或调用线程正在运行的线程模型类型没有限制。下面的代码显示了等待waitableasync
对象数组的用法。
private void button6_Click(object sender, EventArgs e)
{
int maxPrimeNum = Convert.ToInt32(textBox2.Text);
int iFirstHalfStart = 1;
int iFirstHalfEnd = maxPrimeNum / 2;
int iSecondHalfStart = iFirstHalfEnd + 1;
int iSecondHalfEnd = maxPrimeNum;
int countPrime = 0;
long st = DateTime.Now.Ticks;
waitableasync firstHalf = new waitableasync(delegate
{
for (int i = iFirstHalfStart; i <= iFirstHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
},_myThreadPool);
waitableasync secondHalf = new waitableasync(delegate
{
for (int i = iSecondHalfStart; i <= iSecondHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
},_myThreadPool);
// Wait on individual waitableasync objects
//
//firstHalf.Wait(-1);
//secondHalf.Wait(-1);
// Wait on array of waitablesasync objects
//
List<waitableasync> waitableasyncList =
new List<waitableasync>();
waitableasyncList.Add(firstHalf);
waitableasyncList.Add(secondHalf);
// WaitAllEx method
//
waitableasync.WaitAllEx(waitableasyncList.ToArray(), -1);
// WaitAll method
//
//waitableasync.WaitAll(waitableasyncList.ToArray(), -1);
long et = DateTime.Now.Ticks;
MessageBox.Show(string.Format("Done: {0}, Time: {1}",
countPrime,(et-st)/10000));
}
我们也可以等待waitableasync
对象数组中任意一个waitableasync
对象的完成。这可以通过waitableasync
类的静态方法WaitAny
来实现。下面的代码片段显示了WaitAny
方法的用法
// WaitAny method
//
waitableasync.WaitAny(waitableasyncList.ToArray(), -1);
等待方法是同步的,并且会阻塞调用等待方法的线程。当应用程序完成其他任务并且只想等待异步代码块的完成时,这些方法非常有用。可能存在一些情况,我们希望在异步代码块完成后执行一段代码或继续应用程序流程,但我们不想显式地同步等待异步代码块的完成。在这些情况下,我们可以向async
或waitableasync
对象的构造函数提供一个委托对象,该委托将在相应async
或waitableasync
对象执行完成后被调用。这个执行完成委托的签名如下所示
public delegate void AsyncCodeBlockExecutionCompleteCallback(async objAsync);
如上面的签名所示,此委托将收到传递给它的async
或waitableasync
(派生自async
类)对象。这样,当执行附加到此委托的方法时,它将能够访问刚刚完成执行的拥有async
或waitableasync
对象。下面的代码显示了AsyncCodeBlockExecutionCompleteCallback
委托的用法
private void button6_Click(object sender, EventArgs e)
{
int maxPrimeNum = Convert.ToInt32(textBox2.Text);
int iFirstHalfStart = 1;
int iFirstHalfEnd = maxPrimeNum / 2;
int iSecondHalfStart = iFirstHalfEnd + 1;
int iSecondHalfEnd = maxPrimeNum;
int countPrime = 0;
long st = DateTime.Now.Ticks;
waitableasync firstHalf = new waitableasync(delegate
{
for (int i = iFirstHalfStart; i <= iFirstHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
}, _myThreadPool, delegate(async objAsync)
{
for (int i = iSecondHalfStart; i <= iSecondHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
long et = DateTime.Now.Ticks;
MessageBox.Show(string.Format("Done: {0}, Time: {1}",
countPrime, (et - st) / 10000));
});
}
如上代码所示,我在创建waitableasync
类的对象时提供了一个委托。该委托将在包装在waitableasync
对象中的异步代码块执行完成后被执行。如果您查看上面的代码,我使用了一个匿名方法来定义委托。这使我能够轻松无缝地使用外部作用域的局部变量。如果没有匿名方法,如果我定义了我自己的带有委托签名的函数,那么将局部变量(如iSecondHalfStart
、iSecondHalfEnd
和st
)传递给委托函数将非常困难。这就是匿名方法的强大功能和实用性。
传递给async
或waitableasync
对象的委托是在执行了包装在相应async
或waitableasync
对象中的代码的ManagedIOCP线程上同步执行的。我们可以通过将委托内的代码包装在async
或waitableasync
对象中来实现进一步的异步。这种简单的技术可以帮助我们执行链接的代码块或一系列依赖的代码块的异步执行。下面的代码展示了这种技术
private void button6_Click(object sender, EventArgs e)
{
int maxPrimeNum = Convert.ToInt32(textBox2.Text);
int iFirstHalfStart = 1;
int iFirstHalfEnd = maxPrimeNum / 2;
int iSecondHalfStart = iFirstHalfEnd + 1;
int iSecondHalfEnd = maxPrimeNum;
int countPrime = 0;
long st = DateTime.Now.Ticks;
waitableasync firstHalf = null;
waitableasync secondHalf = null;
firstHalf = new waitableasync(delegate
{
for (int i = iFirstHalfStart; i <= iFirstHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
}, _myThreadPool, delegate(async objAsync)
{
secondHalf = new waitableasync(delegate
{
for (int i = iSecondHalfStart; i <= iSecondHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
long et = DateTime.Now.Ticks;
MessageBox.Show(string.Format("Done: {0}, Time: {1}",
countPrime, (et - st) / 10000));
}, _myThreadPool);
});
}
在上面的代码中,我们使用了一个名为secondHalf
的waitableasync
类型的变量,它包装了执行完成委托的代码。但是,将赋值给此变量的waitablesasync
对象要到由firstHalf
变量表示的waitableasync
对象完成执行后才会被创建。所以,在上面的按钮点击处理函数中,在我们能够等待secondHalf
对象完成后,我们无法使用secondHalf
变量。在这种情况下,在某些情况下,在我们可以等待secondhalf
对象完成之前,等待firstHalf
对象完成是必要的。这种依赖关系可能会在使用ACB设计应用程序时导致不必要的/不期望限制。为了克服这些限制,ACB提供了另一种链接和连接依赖的异步代码块的方法。
在创建async
或waitableasync
类对象时,我们可以提供另一个async
或waitableasync
对象,新创建的对象应该依赖于它。这种依赖关系意味着新创建的async
或waitableasync
对象只有在其依赖的async
或waitableasync
对象完成执行后才会被安排执行。下面的代码展示了如何在两个async
或waitableasync
对象之间编程这种依赖关系
private void button6_Click(object sender, EventArgs e)
{
int maxPrimeNum = Convert.ToInt32(textBox2.Text);
int iFirstHalfStart = 1;
int iFirstHalfEnd = maxPrimeNum / 2;
int iSecondHalfStart = iFirstHalfEnd + 1;
int iSecondHalfEnd = maxPrimeNum;
int countPrime = 0;
long st = DateTime.Now.Ticks;
waitableasync firstHalf = new waitableasync(delegate
{
for (int i = iFirstHalfStart; i <= iFirstHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
}, _myThreadPool);
waitableasync secondHalf = new waitableasync(delegate
{
for (int i = iSecondHalfStart; i <= iSecondHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
long et = DateTime.Now.Ticks;
MessageBox.Show(string.Format("Done: {0}, Time: {1}",
countPrime, (et - st) / 10000));
},_myThreadPool,firstHalf);
}
如上代码所示,由secondHalf
变量表示的waitableasync
对象依赖于由firstHalf
变量表示的waitableasync
对象。这样,我们就能够将secondHalf
waitableasync
对象的执行与其firstHalf
waitableasync
对象的执行完成链接起来,并且secondHalf
对象可立即用于执行等待操作,而在前一种情况(如前所述,在firstHalf
waitableasync
对象的执行完成委托中创建secondHalf
对象)中则无法做到这一点。
我们还可以让一个async
或waitableasync
对象依赖于一个async
或waitableasync
对象数组。当它依赖的所有async
或waitableasync
对象都完成执行后,依赖的async
或waitableasync
对象将被安排执行。这种功能在使用我们之前讨论的执行完成委托时是不可能的。下面的代码展示了依赖于多个async
或waitableasync
对象的用法
private void button6_Click(object sender, EventArgs e)
{
int maxPrimeNum = Convert.ToInt32(textBox2.Text);
int iFirstHalfStart = 1;
int iFirstHalfEnd = maxPrimeNum / 2;
int iSecondHalfStart = iFirstHalfEnd + 1;
int iSecondHalfEnd = maxPrimeNum;
int countPrime = 0;
long st = DateTime.Now.Ticks;
waitableasync firstHalf = new waitableasync(delegate
{
for (int i = iFirstHalfStart; i <= iFirstHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
}, _myThreadPool);
waitableasync secondHalf = new waitableasync(delegate
{
for (int i = iSecondHalfStart; i <= iSecondHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
},_myThreadPool);
waitableasync secondHalf2 = new waitableasync(delegate
{
for (int i = iSecondHalfStart; i <= iSecondHalfEnd; i++)
{
bool isPrime = true;
for (int j = 2; j <= (i / 2); j++)
{
if ((i % j) == 0)
{
isPrime = false;
break;
}
}
if (isPrime == true)
Interlocked.Increment(ref countPrime);
}
long et = DateTime.Now.Ticks;
MessageBox.Show(string.Format("Done: {0}, Time: {1}",
countPrime, (et - st) / 10000));
}, _myThreadPool, new waitableasync[] { firstHalf, secondHalf });
}
3. 在异步代码块中处理异常
出于所有实际目的,包装在async
或waitableasync
对象中的代码可能会引发异常。由于异步代码块在单独的ManagedIOCP线程上执行,因此调度异步代码块的线程无法得知此异常。因此,ACB会捕获在执行异步代码块时发生的任何异常,并将其通过包装该异步代码块的async
类对象的CodeExcpetion
属性提供。下面的代码展示了async
类对象上CodeExcpetion
的用法
private void button5_Click_1(object sender, EventArgs e)
{
SqlConnection con = new SqlConnection("server=.;" +
"database=AsyncTestDB;uid=sa;pwd=AdiTanuL1$");
con.Open();
waitableasync obj = new waitableasync(delegate
{
int age = 28;
string cmdStr = string.Format("insert into Address" +
" values ('{0}',{1},'{2}')", "Name_" +
age.ToString(), age, "Address_" +
age.ToString());
SqlCommand cmd = new SqlCommand(cmdStr, con);
cmd.ExecuteNonQuery();
}, _myThreadPool, delegate(async objAsync)
{
if (con.State == ConnectionState.Open) con.Close();
if (objAsync.CodeException != null)
MessageBox.Show(string.Format("Failed. Error: {0}",
objAsync.CodeException.Message));
else
MessageBox.Show("Successfully completed the insert");
});
}
在上面的代码示例中,我们只是尝试将数据插入到名为'AsyncTestDB'的数据库的'Address'表中。我们为异步代码块提供了一个执行完成委托,在委托中我们检查任何异常并显示适当的消息。
如果执行完成委托中的代码本身抛出异常怎么办?正如我们之前讨论的,执行完成委托将在完成关联异步代码块执行的同一ManagedIOCP线程上执行。因此,如果执行完成委托抛出任何异常,ManagedIOCP将忽略它,除非您为ManagedIOCP实例提供了一个委托来处理异常。但是,在ManagedIOCP中处理异步代码块异常并不实用,并且有关发生异常的async
或waitableasync
对象的信息会丢失。因此,ACB也会捕获在执行与async
或waitableasync
对象关联的执行完成委托期间发生的任何异常。此异常可以通过async
类对象上的'ExecutionCompletionDelegateException
'属性获得。
4. WinForms 和异步代码块
WinForms中的控件对象,如Form、TextBox
、ListBox
、Button
、ProgressBar
等,具有线程亲和性。这意味着如果在线程1中创建了一个WinForms控件对象,那么其窗口相关的属性或方法只能从线程1访问,不能从任何其他线程访问。说到这里,请看下面的代码
private void button1_Click(object sender, EventArgs e)
{
progressBar1.Minimum = 1;
progressBar1.Maximum = 10000;
progressBar1.Value = 1;
new waitableasync(delegate
{
for(int i = 1; i <= 10000; i++)
{
textBox1.Text = i.ToString();
progressBar1.Value = i;
}
});
}
上面代码的问题在于,异步代码块在ManagedIOCP线程上执行,而这个线程不是创建TextBox
(textBox1
)和ProgressBar
(progressBar1
)控件的线程。这段代码可能可以正常执行,但不能保证可靠工作。这表明直接从异步代码块访问WinForms控件是不正确的。因此,我们需要某种方式来执行设置WinForms控件属性的代码,该代码应该在创建这些控件的线程中执行。这可以通过一个名为SynchronizationContext
的.NET 2.0 FCL类来实现。下面的代码显示了该类在异步代码块中正确访问WinForms控件的用法。
private void button1_Click(object sender, EventArgs e)
{
SynchronizationContext sc = SynchronizationContext.Current;
progressBar1.Minimum = 1;
progressBar1.Maximum = 10000;
progressBar1.Value = 1;
new waitableasync(delegate
{
for(int i = 1; i <= 10000; i++)
{
sc.Send(delegate(object state)
{
textBox1.Text = i.ToString();
progressBar1.Value = i;
},null);
}
});
}
在上面的代码中,我们通过SynchronizationContext
类上的静态属性Current
来检索与当前线程关联的SynchronizationContext
对象。这将是WindowsFormsSynchronizationContext
类的一个对象,当我们的测试应用程序的主窗体被创建时,它被设置为当前线程的SynchronizationContext
。这个当前SynchronizationContext
对象包含当前线程的信息。我们从异步代码块内部使用相同的Synchronization
对象,通过捕获它(再次利用匿名方法的魔力),来执行访问主窗体控件的代码。SynchronizationContext
对象的Send
方法接收提供的委托,并在SynchronizationContext
对象所代表的线程上执行它。在我们的例子中,传递给sc
对象的Send
方法的委托将在创建我们测试应用程序主窗体及其控件(如textBox1
)的线程上执行。进一步解释SynchronizationContext
超出了本文的范围。我建议读者在CodeProject上查找关于这个新的.NET 2.0类的专门文章。
5. 关注点
到我完成异步代码块库的时候,它已经成为我最喜欢的技术和.NET库。它为构建异步应用程序开辟了新的编程技术途径。在创建ACB时,一个一直让我着迷的点是async
对象的序列化。我尝试序列化async
对象,但在现阶段似乎存在一些未知和技术障碍。本文附带的ACB库包含注释掉的代码,展示了我尝试序列化async
对象的尝试。我面临的一个问题是,C# 2.0编译器为匿名方法生成的包装类未标记为可序列化。我将继续在这方面努力,看看是否有可能序列化async
对象。如果我们能够实现这一点,它将释放ACB的巨大潜力。我们将能够安排async
对象执行,序列化它们,并在以后执行它们,以实现持久化的长期业务活动或在不同系统上实现可伸缩性。
6. 软件用途
本软件按“原样”提供,不作任何明示或暗示的保证。我对本软件可能造成的任何类型的损害或损失不承担任何责任。
7. 历史
- 2006年10月2日 - 异步代码块 v1.0.0.0。