什么是.Net的异步机制 (第二部分)
2009-07-29 00:46 博弈IT 阅读(506) 评论(0) 编辑 收藏 举报什么是.Net的异步机制(异步Stream读/写) - step 4
在前面3篇文章,我已经介绍了异步的工作原理和相关方法和参数的应用.下面我们介绍Stream流的操作, 并使用System.IO.FileStream来揭开如何开发异步的Stream(System.IO.Stream) 读/写操作
异步的Stream读/写操作
下面是继承于System.IO.Stream的类
System.IO.Stream
Microsoft.JScript.COMCharStream
System.IO.BufferedStream
System.IO.FileStream
System.IO.MemoryStream
System.IO.UnmanagedMemoryStream
System.Security.Cryptography.CryptoStream
System.Printing.PrintQueueStream
System.IO.Pipes.PipeStream
System.Data.OracleClient.OracleBFile
System.Data.OracleClient.OracleLob
System.IO.Compression.DeflateStream
System.IO.Compression.GZipStream
System.Net.Sockets.NetworkStream
System.Net.Security.AuthenticatedStream
在System.IO.Stream中提供了异步的读/写(Read/Write)行为,上面继承于System.IO.Stream的类都具有同样的异步操作行为.在.Net Framework框架中,微软设计师使用Begin+同步方法名 / End+同步方法名来设计异步方法的规则,基本上我们在微软MSDN看到的 BeginXXX + EndXXX都是异步的方法,并且当我们在某个类中看到BeginInvoke / EndInvoke,都是微软提供的最原始的异步方法.在System.IO.Stream类中表现为BeginRead+EndRead / BeginWrite/EndWrite.
我们来看一个例子,FileStream(System.IO),Read / BeginRead+EndRead,读取文件内容,开始我们使用同步方法.
同步调用
Code1.1
2 {
3 static string path = @"c:\file.txt";//确保你本地有这个文件
4 const int bufferSize = 5;//演示,一次只读取5 byte
5 static void Main()
6 {
7 FileStream fs = new FileStream(path, FileMode.Open,
8FileAccess.Read, FileShare.Read, 20480, false);//同步调用false
9 using (fs)//使用using来释放FileStream资源
10 {
11 byte[] data = new byte[bufferSize];
12 StringBuilder sb = new StringBuilder(500);
13 int byteReads;
14 do// 不断循环,直到读取完毕
15 {
16 byteReads = fs.Read(data, 0, data.Length);
17 sb.Append(Encoding.ASCII.GetString(data, 0, byteReads));
18 } while (byteReads > 0);
19 Console.WriteLine(sb.ToString());//输出到工作台
20
21 }//自动清除对象资源,隐式调用fs.Close();
22 Console.ReadLine();// 让黑屏等待,不会直接关闭..
23 }
24 }
方法非常简单,它会构造一个 FileStream 对象,调用 Read方法,不断循环读取数据。C# using 语句可确保完成数据处理后会关闭该 FileStream 对象。
下面我们看异步调用(BeginRead/EndRead)
异步调用
Code1.2
2 {
3 static string path = @"c:\file.txt";//确保你本地有这个文件
4 const int bufferSize = 5;//演示,一次只读取5 byte
5 static byte[] data;
6 static void Main()
7 {
8 data = new byte[bufferSize];
9 FileStream fs = new FileStream(path, FileMode.Open,
10FileAccess.Read, FileShare.Read, 20480, true);//设置异步调用true, 注意0
11
12 //异步读取文件,把FileStream对象作为异步的参数// <-
13 AsyncCallback callback = new AsyncCallback(OnReadCompletion);
14 IAsyncResult async = fs.BeginRead(data, 0, bufferSize, callback, fs); // <-
15
16 Console.ReadLine();// 让黑屏等待,不会直接关闭..
17 }
18 static void OnReadCompletion(IAsyncResult asyncResult)
19 {
20 FileStream fs = asyncResult.AsyncState as FileStream;
21 int bytesRead = fs.EndRead(asyncResult);
22 //输出到工作台
23 Console.Write(Encoding.ASCII.GetString(data, 0, bytesRead));
24 //不断循环,直到读取完毕
25 if (bytesRead > 0)
26 fs.BeginRead(data, 0, bufferSize, OnReadCompletion, fs);
27 else
28 fs.Close(); //当全部读取完毕,显式释放资源
29 }
30 }
方法是使用BeginRead和EndRead 完成的, 我们注意到方法不能使用 C# using 语句(释放资源),因为 FileStream 是在一个主线程中打开,然后在另一个线程中关闭的,而是通过把FileStream 作为参数的形式来在另外一个线程中关闭的(fs.Close();),查看红色部分.
注意0:创建FileStram 对象,如果没有FileStream fs = new FileStream(path, FileMode.Open,FileAccess.Read, FileShare.Read, 20480, true); bool useAsync=true 或者构造FileStream(String, FileMode, FileAccess, FileShare, Int32, FileOptions) FileOptions = FileOptions.Asynchronous 时, 即使使用了BeginRead/EndRead, 程序也是在同步执行方法,FileStream 对象会使用其他线程来模仿异步行为,反而降低了应用程序的性能.
下面我将通过使用C# 匿名方法(C# 2.0) 和 lambda 表达式(C# 3.0引入的一个新功能) 来完成上面操作,如果对这个不熟悉的朋友可以查看下面文章.
匿名方法:http://www.microsoft.com/china/msdn/library/langtool/vcsharp/CreElegCodAnymMeth.mspx?mfr=true
lambda 表达式:http://msdn.microsoft.com/zh-cn/magazine/cc163362.aspx
C# 匿名方法 和 lambda 表达式
1,匿名方法:
Code1.3
2 {
3 static string path = @"c:\file.txt";//确保你本地有这个文件
4 const int bufferSize = 5;//演示,一次只读取5 byte
5 static void Main()
6 {
7 byte[] data = new byte[bufferSize];
8 //[1]
9 FileStream fs = new FileStream(path, FileMode.Open,
10FileAccess.Read, FileShare.Read, 20480, true);//设置异步调用true
11 //使用匿名委托方式
12 AsyncCallback callback = null; //注意1
13 callback = delegate(IAsyncResult asyncResult)//匿名方法
14 {
15 int bytesRead = fs.EndRead(asyncResult);//[2]
16 Console.Write(Encoding.ASCII.GetString(data, 0, bytesRead));//输出到工作台
17 //不断循环,直到读取完毕
18 if (bytesRead > 0)
19 fs.BeginRead(data, 0, bufferSize, callback, null);//[3]
20 else
21 fs.Close();//[4]
22 };
23
24 //异步读取文件
25 IAsyncResult async = fs.BeginRead(data, 0, bufferSize, callback, null);
26
27 Console.ReadLine();// 让黑屏等待,不会直接关闭..
28 }
29 }
对比Code1.2代码我们可以看出, 匿名方法非常出色的完成我们功能, 在匿名方面体内 fs ([2][3][4])像普通变量一样执行引用FileStream([1]) 对象,而不需要任何的类型转换. 对象在方法之间轻松实现传递,并且从一个线程轻松迁移到另一个线程, 对APM 编程而言这是十分完美的,但实际上编译器会重新编写您的代码,从堆栈中取出这些变量,并将它们作为字段嵌入对象。由于编译器会自动执行所有的工作,您可以很轻松地将最后一个参数的空值传递到 BeginRead 方法,因为现在没有必要再在方法和线程之间显式传递的数据了。
注意1: 必须先AsyncCallback callback = null; 要不编程器会告诉你错误:” Use of unassigned local variable 'callback '”.
2,Lambda 表达式
我们只需要简单的修改(在执行同一操作,lambda 表达式语法比 C# 匿名方法更简洁),匿名方法,把Code1.3中红色部分的
callback = delegate(IAsyncResult asyncResult)
修改成
callback = asyncResult =>
下面是完整代码.
Code1.4
最后,我们来看看异步的写操作(BeginWrite/EndWrite)
Code2
2 {
3 static void Main()
4 {
5 FileStream fs = new FileStream("text.txt", FileMode.Create,
6FileAccess.ReadWrite, FileShare.None, 20480, true);//设置异步调用true
7 //输入信息
8 Console.Write("Please Enter:");
9 byte[] data = Encoding.ASCII.GetBytes(Console.ReadLine());
10
11 //异步写文件
12 IAsyncResult async = fs.BeginWrite(data, 0, data.Length, asyncResult =>
13 {
14 fs.EndWrite(asyncResult);//写文件介绍,输出到text.txt文件中.
15 fs.Close();
16
17 }, null);
18
19 Console.ReadLine();// 让黑屏等待,不会直接关闭..
20 }
21 }
大家觉得是否很简单呢? 基本上所有具有异步行为的流(继承于System.IO.Stream)操作都可以按照类似于上面的代码编写. 当然其他异步行为也可以使用上面代码中的技巧. 在System.IO.Stream 中,提供了ReadTimeout/WriteTimeout 的超时处理,但是基类中是不支持的.会报 InvalidOperationException 异常,反编译可以看到throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_TimeoutsNotSupported")).
下篇文章我会提供其他的例子来说明异步中的线程间通信.采用Window Forms程序.
以上有word 文档直接粘贴,排版可能不太好看,你可以通过下面来下载相应的代码/文档
1,文档
2,代码(VS2008开发,.Net Framework 3.5(C Sharp)编写)
什么是.Net的异步机制(线程间通信) - step 5
前几篇文章我已经对异步的操作进行的详细的解释.异步操作也是线程的一种,当我们开始一个异步操作(新线程),完成调用后需要和其他线程通信(可能需要告知状态信息),这时候我们就需要线程间的通信编程.
线程间通信
我们看下面的图
图1
我们来看线程间通信的原理:线程(Thread B)和线程(Thread A)通信, 首先线程A 必须实现同步上下文对象(Synchronization Context), 线程B通过调用线程A的同步上下文对象来访问线程A,所有实现都是在同步上下文中完成的.线程B有两种方式来实现线程间的通信.
第一种:调用线程A的同步上下文对象,阻碍当前线程,执行红色箭头调用,直到黄色箭头返回(同步上下文执行完毕)才释放当前线程. (1->2->3->5)
第二种: 调用线程A的同步上下文对象(实际上是在开启一个新线程去执行,1->2->3->5) ,执行红色箭头,但并不阻碍当前线程(原有线程,1->4->5),绿色箭头继续执行.
文章中将会通过下面几个类来进行介绍:
1. ISynchronizeInvoke
接口
2. SynchronizationContext 类
3. AsyncOperation / AsyncOperationManager 类
附: 同步上下文对象(Synchronization Context)是什么呢? 当我们访问上下文对象时候(多个对象可以存在于一个上下文中), 是使用代理的方式引用的,而不是直接引用的.这种方式可能是当多个对象访问上下文对象时候,先到达对象先访问,锁住,执行完毕再解锁,排队式访问.
1. ISynchronizeInvoke
接口
我们先来看下面一段异步的代码(Window Form控件下有1个Button/1个Label),但点击Button的时候,执行异步调用,完成后,告诉Window Form的 Label控件Text属性” Asynchronous End”.
Code1.1
2 private void button1_Click(object sender, EventArgs e)
3 {
4 //辅助方法,查看当前线程
5 Debug.WriteLine(string.Format("Window Form Method.Thread ID:#{0}",
Thread.CurrentThread.ManagedThreadId));
6 //Label lblStatus 属于主线程的控件[1]
7 this.lblStatus.Text = "Asynchronous Start.";
8 //使用委托来调用异步方法
9 DoWork work = DoWorkMethod;
10 work.BeginInvoke(OnWorkCallback, work);
11 }
12 void OnWorkCallback(IAsyncResult asyncResult)
13 {
14 //辅助方法,查看当前线程
15 Debug.WriteLine(string.Format("Asynchronous Callback Method.Thread ID:#{0}",
Thread.CurrentThread.ManagedThreadId));
16 DoWork work = asyncResult.AsyncState as DoWork;
17 if (work != null)
18 {
19 work.EndInvoke(asyncResult);
20 }
21 // 报错:"线程间操作无效: 从不是创建控件“lblStatus”的线程访问它."
22 this.lblStatus.Text = "Asynchronous End"; //上面注释[1]
23 }
24
25 void DoWorkMethod()
26 {
27 Thread.Sleep(3000);//模拟耗时工作
28 }
运行代码,我们在第22行报错(异步方法体内).为什么呢?我们必须清楚的一点,在windows应用窗体应用程序中,对窗体上控件属性的任何修改都必须在主线程中完成。不能从其他线程安全地访问控件的方法和属性。从Debug窗口中我们也可以看出(图1.1).执行Button Click事件的时候,运行在线程ID =#10; 在异步的方法体内,运行在线程ID=#7.不同线程间不能直接通信.
图1.1
为了解决这个问题,实现图1.1 中 #10 和 #7 的通信,下来开始认识ISynchronizeInvoke
接口(此接口来自.Net Framework 1.0),提供3个方法1个属性:
BeginInvoke / EndInvoke 方法 : 异步方法
Invoke 方法 : 同步方法
InvokeRequired 属性 : 判读来源的执行线程
下面我们看Code1.2的具体代码来说明(对Code1.1改写,其中Label 改为ListBox)
Code1.2
2 private void button1_Click(object sender, EventArgs e)
3 {
4 //更新状态,添加到Listbox 中
5 AddValue("Asynchronous Start.");
6 //使用委托来调用异步方法
7 DoWork work = DoWorkMethod;
8 work.BeginInvoke(OnWorkCallback, work);
9 }
10
11 void OnWorkCallback(IAsyncResult asyncResult)
12 {
13 DoWork work = asyncResult.AsyncState as DoWork;
14 if (work != null)
15 {
16 work.EndInvoke(asyncResult);
17 }
18 //(1)方法:调用Control控件的Invoke
19 //Action<string> asyncUpdateState = UpdateStatus; //Action<string> 介绍=> 附1
20 //Invoke(asyncUpdateState, "1:Asynchronous End.");
21
22 //(2)方法:直接在异步调用的线程下
23 UpdateStatus("2:Asynchronous End.");
24 }
25
26 void UpdateStatus(string input)
27 {
28 //把你需要通知的控件Control 赋值给ISynchronizeInvoke
29 //来实现线程间的通信
30 ISynchronizeInvoke async = this.listBoxStatus;
31 //使用(1)方法,InvokeRequired == false ,来源当前(Window Form)主线程
32 if (async.InvokeRequired == false)
33 AddValue(input);
34 else// 使用(2)方法 == true ,来源其他线程(异步)
35 {
36 Action<string> action = new Action<string>(status =>
37 {
38 AddValue(status);
39 });
40 //调用ISynchronizeInvoke 提供的Invoke 同步方法,阻碍线程,直到调用结束
41 //也可以使用ISynchronizeInvoke 提供的异步BeginInvoke/EndInvoke方法来实现调用.
42 async.Invoke(action, new object[] { input });
43 }
44 }
45
46 void AddValue(string input)
47 {
48 this.listBoxStatus.Items.Add(string.Format("[(#{2}){0}]Context is null:{1}", input,Thread.CurrentContext==null, Thread.CurrentThread.ManagedThreadId));
49 }
50 void DoWorkMethod()
51 {
52 Thread.Sleep(3000);//模拟耗时工作
53 }
图1.2
在代码中(UpdateStatus方法体内),我们可以看到主要是在ISynchronizeInvoke async = this.listBoxStatus;实现了线程间的通信,MSDN的解释” 实现此接口的对象可以接收事件已发生的通知,并且可以响应有关该事件的查询”. 并使Window Form(主线程) 下的ListBox 控件和来自异步方法(另外一个线程)的建立了通道. InvokeRequired 判断线程的来源,如果使用(1)方法,来源于Window Form 自身Control 的Invoke方法, InvokeRequired将返回false; 来源另外线程(异步)如果使用(2)返回true.同时ISynchronizeInvoke 提供了异步(BeginInvoke+EndInvok)和同步方法(Invoke)来实现线程间通信.Invoke 就是最上面的图1 所示的第一种 / BeginInvoke+EndInvok 是第二种.
附1:关于Action<T…> / Func (T…, TResult) (简单的说就是”简化后的委托”)的知识可以看MSDN的介绍.
Action<T…>: http://msdn.microsoft.com/zh-cn/library/018hxwa8.aspx
Func (T…, TResult): http://msdn.microsoft.com/zh-cn/library/bb549151.aspx
Code1.2虽然实现了线程间的通信, 回顾图1的解释,” 首先线程A 必须实现同步上下文对象(Synchronization Context)”, 而在Code1.2 中并没有为Window Form(主线程)实现上下文对象,如果没有这个对象一切都是不成立的.那么Window Form 做了些什么呢?
我们来看下面的代码(使用Console程序):
Code1.3
2 {
3 static void Main()
4 {
5 //1,在Main 主线程中运行,查看线程ID和同步上下文
6 Console.WriteLine("0.ThreadID:#{1},Synchronization Context is null?{0}",
7 SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId);
8
9 //2,在Main 主线程中运行,实例化空对象Test,查看线程ID和同步上下文
10 Test a = new Test();
11 Console.WriteLine("1.ThreadID:#{1},Synchronization Context is null?{0}",
12 SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId);
13
14 //3,在Main 主线程中运行,实例化FormTest对象(继承Form),查看线程ID和同步上下文
15 FormTest test = new FormTest();
16 Console.WriteLine("2.ThreadID:#{1},Synchronization Context is null?{0}",
17 SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId);
18
19 //4,在新线程中运行,查看线程ID和同步上下文
20 new Thread(work).Start();
21
22 Console.Read();
23 }
24 static void work()
25 {
26 Console.WriteLine("3.ThreadID:#{1},Synchronization Context is null?{0}",
27 SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId);
28 }
29 }
30 public class FormTest : System.Windows.Forms.Form { }
31 public class Test { }
图1.3
由代码和图可以看出(SynchronizationContext.Current == null 判断同步上下文对象是否存在), 实例化FormTest 对象后(继承System.Windows.Forms.Form),Form默认的帮我们创建了同步上下文对象,使主线程#9 具备了同步上下文对象,这就是为什么Code1.2 不用声明同步上下文对象的原因,同时也告诉我们,开启一个新线程#10,线程本身是没有同步上下文对象的.
2. SynchronizationContext 类
相比ISynchronizeInvoke 接口,SynchronizationContext 类(来自.Net Framework 2.0)提供了更多的方法来操作同步上下文对象,实现线程间通信.在上面的例子中SynchronizationContext类中将由 Post/Send 方法来实现.
反编译后我们看到:
Code2.1
2 {
3 ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
4 }
5
6 public virtual void Send(SendOrPostCallback d, object state)
7 {
8 d(state);
9 }
Send = ISynchronizeInvoke 中的Invoke 同步调用.图1中的第一种
Post = ISynchronizeInvoke 中的BeginInvoke + EndInvoke异步调用. 图1中的第二种
改写Code1.2的代码(还是在WinForm 下编程).
Code2.2
2 private void button1_Click(object sender, EventArgs e)
3 {
4 //System.Windows.Forms.Form 自动的创建默认的同步上下文对象,
5 //直接的获取当前的同步上下文对象
6 SynchronizationContext context = SynchronizationContext.Current;
7 //更新状态,添加到Listbox 中
8 AddValue<string>("Asynchronous Start.");
9 //使用委托来调用异步方法
10 DoWork work = DoWorkMethod;
11 work.BeginInvoke(OnWorkCallback, context);
12
13 }
14 void OnWorkCallback(IAsyncResult asyncResult)
15 {
16 AsyncResult async = (AsyncResult)asyncResult;
17 DoWork work = (DoWork)async.AsyncDelegate;
18 work.EndInvoke(asyncResult);
19
20 //更新状态
21 UpdateStatus("Asynchronous End.", asyncResult.AsyncState);
22 }
23 void UpdateStatus(object input,object syncContext)
24 {
25 //获取主线程(Window Form)中同步上下文对象
26 SynchronizationContext context = syncContext as SynchronizationContext;
27 //使用SynchronizationContext 类中异步Post 方法
28 SendOrPostCallback callback = new SendOrPostCallback(p => {
29 AddValue<object>(p);
30 });
31 context.Post(callback, input);//Post 为异步,Send 为同步
32
33 }
34 void AddValue<T>(T input)
35 {
36 this.listBoxStatus.Items.Add(string.Format("[(#{2}){0}]Context is null:{1}", input, Thread.CurrentContext == null, Thread.CurrentThread.ManagedThreadId));
37 }
38 void DoWorkMethod()
39 {
40 Thread.Sleep(3000);//模拟耗时工作
41 }
上面我们已经说过在主线程中System.Windows.Forms.Form 自动的创建默认的同步上下文对象, 这时候我们把当前的同步上下文对象通过参数的形式赋值到异步线程中,调用Post 方法来实现, Post 方法接收 SendOrPostCallback 委托和额外object state参数,在Post方法体内调用线程池的线程来实现(Code2.1).当然我们也可以直接使用Send方法.
下面我们看看线程中的代码(在Console 下编程).
Code2.3
2 {
3 static void Main()
4 {
5 Output("Main Thread Start.");
6 //为主线程创建Synchronization Context
7 var context = new SynchronizationContext();
8 //开始一个新线程
9 Thread threadB = new Thread(work);
10 threadB.Start(context);
11
12 Console.Read();
13 }
14 static void work(object context)
15 {
16 Output("Thread B");
17
18 //获取主线程中的同步上下文对象
19 SynchronizationContext sc = context as SynchronizationContext;
20
21 //异步的方式和主线程通信,并发送"Hello World".
22 sc.Post(new SendOrPostCallback(p =>
23 {
24 Output(p);
25 }), "Hello World");
26 }
27 static void Output(object value)
28 {
29 Console.WriteLine("[ThreadID:#{0}]{1}", Thread.CurrentThread.ManagedThreadId, value);
30 }
31 }
图2.3
在主线程中因为没有同步上下文对象,所以开始我们new SynchronizationContext(); 对象,其他和Code2.2 基本一样.从图2.3很好的解释图1的第二种调用,也说明了Post 是开启新线程(线程池)运行的.
3. AsyncOperation / AsyncOperationManager 类
AsyncOperation / AsyncOperationManager 类是
SynchronizationContext 类的进一步封装和实现, AsyncOperationManager在创建AsyncOperation对象的时候会取得当前线程的同步上下文对象,并存储在AsyncOperation之中,使我们访问同步上下文对象更加容易.
Code3.1
2 {
3 private AsyncOperation operation;
4 public event EventHandler somethingHappened;
5 public MySynchronizedClass()
6 {
7 //创建AsyncOperation 对象,并把当前线程的同步上下文保持到AsyncOperation中.
8 operation = AsyncOperationManager.CreateOperation(null);
9
10 Thread workerThread = new Thread(new ThreadStart(DoWork));
11 workerThread.Start();
12 }
13
14 private void DoWork()
15 {
16 SendOrPostCallback callback = new SendOrPostCallback(state =>
17 {
18 EventHandler handler = somethingHappened;
19
20 if (handler != null)
21 {
22 handler(this, EventArgs.Empty);
23 }
24 });
25
26 operation.Post(callback, null);
27 //注意1
28 operation.OperationCompleted();
29 }
30 }
代码很简单,我也不在解释,可以参照上面所有代码. 注意1: AsyncOperation类中实现了
OperationCompleted的方法. SynchronizationContext 类中这个方法是没有具体的代码实现的.
总结:
文章中也非常适合线程的编程(除了异步)来实现通信, SynchronizationContext是最重要的一个,其他的扩展类,如SynchronizationContextSwitcher 等更高级的主题,具体可参考下面的链接. 在Winform中有个非常重要的BackgroundWorker 类,关于
BackgroundWorker的文章很多,在此不做解释了.
下一篇文章中,我将会讨论Web 上的异步方法.
技术参考:
http://www.codeproject.com/KB/cpp/SyncContextTutorial.aspx
http://www.codeproject.com/KB/threads/SynchronizationContext.aspx
http://www.codeproject.com/KB/threads/SynchronizationContext2.aspx
http://www.codeproject.com/KB/threads/SynchronizationContext3.aspx
http://www.code-magazine.com/Article.aspx?quickid=0403071
以上有word 文档直接粘贴,排版可能不太好看,你可以通过下面来下载相应的代码/文档
1,文档
2,代码(VS2008开发,.Net Framework 3.5(C Sharp)编写)