1、写在前面
最近在研究我们产品的代码,在某些功能(比如说入库)上用到了异步,但这么久一直也没有细细地去研究,网上对相关内容的通俗讲解很多都是以泡茶、洗衣服之类的生活例子,个人认为这对于理解异步的作用没有问题,但真正遇到问题还是不能很好的使用。所以我在学习相关内容的时候也写了一些示例代码,主要的功能是模拟一个耗时的操作,并模拟记录日志(因为我们产品也有响应的功能)。用了两个思路实现,分别是针对.net4.5以上版本的await/async语法糖和低版本.net的IAsyncResult。
2、功能的设计
设计了如下的窗体,两个按钮分别通过await/async和IAsyncResult模拟两个耗时操作,并在底部的文本框中模拟记录日志。
3、await/async实现
窗体上方窗体绑定的代码如下:
private async void button1_ClickAsync(object sender, EventArgs e)
{
Class1 class1 = new Class1();
class1.WriteLogHandle += WriteSystemLogAction;
int a = await class1.MethodA();
Notice("最终结果:" + a);
}
绑定的记录日志方法:
private void WriteSystemLogAction(string logStr)
{
//解决“线程间操作无效”错误,第二种方式
Action<string> action = new Action<string>(WriteText);
this.Invoke(action, logStr);
}
private void WriteText(string s)
{
this.richTextBox1.AppendText(String.Format("\n{0}\t{1}", System.DateTime.Now.ToLongTimeString(), s));
this.richTextBox1.SelectionStart = richTextBox1.Text.Length;
richTextBox1.Focus();
}
最开始没有用委托的方式些,遇到了这个错误,原来,windows窗体控件不是线程安全的,如果几个线程操作某一控件的状态,可能会使该控件的状态不一致,出现争用或死锁状态,所以要用委托机制实现线程安全。当然,也可以初始化控件时设置Control.CheckForIllegalCrossThreadCalls = false;,但微软不推荐这种做法。
在主窗体的方法中有一个提示的方法:
private void Notice(string msg)
{
MessageBox.Show(msg);
}
在Class1.cs中,定义了一个写日志的委托,
//日志记录委托
public Action<string> WriteLogHandle;
定义一个实例化委托的方法:
//实例化委托
private void WriteLog(string log)
{
if (WriteLogHandle != null)
{
WriteLogHandle(log);
}
}
,最后模拟一个耗时操作,并记录日志,模拟的操作是计算累加值,每次计算的时候sleep1秒,并记录每次加的数,最后返回计算结果:
//await/async示例方法
public async Task<int> MethodA()
{
int result=await Task.Run(()=>{
int temp = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
WriteLog("正在计算"+temp+"+" + i.ToString() + ";当前线程" + Thread.CurrentThread.ManagedThreadId.ToString());
temp += i;
}
return temp;
});
return result;
}
最后运行的结果如下:
大概解释下吧,可能解释得不够深入,不足之处还望指点。在MethodA()方法中使用Task新开一个线程,这个线程用来计算累加值,await关键字在.net官方文档中的介绍是:
await 运算符应用于一个异步方法的任务挂起方法的执行,直到等待任务完成。 任务表示正在进行的工作。
await 表达式不阻止它在其上执行的线程。 相反,它导致编译器注册异步方法的其余部分为等待的任务继续。 然后控件回异步方法的调用方。 当任务完成时,将会调用其延续任务,并且,异步方法的执行恢复它将会停止的位置。
对async的说明是:
异步方法提供了一种简便方式完成可能需要长时间运行的工作,而不必阻止调用方的线程。 异步方法的调用方可以继续工作,而不必等待异步方法完成。
我的理解是,UI线程调用async修饰的异步方法MethodA(),UI线程不会发生阻塞,可以进行其他操作(比如拖动窗体等),可以试试讲这个方法修改成如下,会发现并没有等待计算结果的返回就直接弹出了窗体。
private async void button1_ClickAsync(object sender, EventArgs e)
{
Class1 class1 = new Class1();
class1.WriteLogHandle += WriteSystemLogAction;
class1.MethodA();
Notice("最终结果:");
}
在MethodA()方法用Task新开一个线程时,result变量等待这个线程的返回结果,结果返回之后,MethodA()方法返回result的值至UI线程,UI线程得到结果弹出提示框。
4、IAsyncResult
IAsyncResult主要是用委托来实现,先写一个方法MethodB()用来模拟耗时操作并记录日志,记录日志的WriteLog方法3中的WriteLog:
//IAsyncResult方法
public int MethodB()
{
int temp = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
WriteLog("正在计算" + temp + "+" + i.ToString() + ";当前线程" + Thread.CurrentThread.ManagedThreadId.ToString());
temp += i;
}
return temp;
}
首先,在Class1中定义委托,并在构造方法中实例化委托,绑定Method方法。
public delegate int AsyncTestDelegate();
public AsyncTestDelegate asyncTestDelegate;
public Class1()
{
if (asyncTestDelegate == null)
{
asyncTestDelegate = MethodB;
}
}
接下来写窗体的控件方法,BeginInvoke()方法:
private void button2_Click(object sender, EventArgs e)
{
Class1 class1 = new Class1();
class1.WriteLogHandle += WriteSystemLogAction;
IAsyncResult asyncResult = class1.asyncTestDelegate.BeginInvoke(null, null);
while (!asyncResult.IsCompleted)
Application.DoEvents();
Notice(class1.asyncTestDelegate.EndInvoke(asyncResult).ToString());
}
其中的while循环主要是避免在等待返回结果时造成卡死状态。
最后的结果也和3中的一样。
5、最后
刚接触异步多线程方面的内容,有些东西确实不够深入,也希望各路高手可以多多指点。相关的代码已上传:
码云下载:https://gitee.com/ranhongwu/dotnet-async-test