官术网_书友最值得收藏!

15.3 進(jìn)程通信

所謂“進(jìn)程通信”,是指正在運(yùn)行的進(jìn)程之間相互交換信息。

每個(gè)進(jìn)程都擁有自己的地址空間,其他進(jìn)程不能直接訪問,因此,通常需要通過一個(gè)第三方媒介間接地在進(jìn)程之間交換信息。

剪貼板是最常用的在進(jìn)程間交換信息的媒介之一。

多個(gè)進(jìn)程間通過共享一個(gè)數(shù)據(jù)文件,也可以實(shí)現(xiàn)進(jìn)程間通信。

除此之外,Windows還提供了另一種強(qiáng)大而靈活的進(jìn)程間通信的方式,這就是COM。基于COM技術(shù)開發(fā)出來的程序(稱為“COM服務(wù)器”)可以被另一程序(稱為“COM客戶端”)在后臺(tái)激活,客戶端可以向服務(wù)器傳送各種信息,要求服務(wù)器完成某些工作,并取回其工作結(jié)果。最典型的COM服務(wù)器就是微軟公司的Office軟件包,前端的程序可以通過COM接口啟動(dòng)這些程序完成許多工作。在.NET出現(xiàn)之前,COM是實(shí)現(xiàn)進(jìn)程間通信的主要方式之一。

交叉鏈接

7.6節(jié)《互操作程序集與本地類型》介紹了一個(gè)在C#程序中通過COM接口“遠(yuǎn)程”操控Word進(jìn)程完成打印預(yù)覽工作的實(shí)例。

.NET出現(xiàn)之后,實(shí)現(xiàn)進(jìn)程通信更為方便,手段更多。

比如在.NET 4.0中,可以通過內(nèi)存映射文件實(shí)現(xiàn)進(jìn)程通信。

真正強(qiáng)大的進(jìn)程通信手段是WCF。WCF可用于開發(fā)復(fù)雜的分布式軟件系統(tǒng),實(shí)現(xiàn)進(jìn)程通信對(duì)于它來說,實(shí)在是“小菜一碟”。

本節(jié)將通過幾個(gè)實(shí)例展示如何讓同一臺(tái)計(jì)算機(jī)上的進(jìn)程可以相互通信。

15.3.1 使用剪貼板在進(jìn)程間傳送對(duì)象

剪貼板簡(jiǎn)介

剪貼板是一個(gè)供應(yīng)用程序使用的公有區(qū)域。在Windows上運(yùn)行的所有程序,在需要時(shí)都可以使用剪貼板存放信息(見圖15-13)。

圖15-13 各種應(yīng)用程序都可存取剪貼板

剪貼板的一個(gè)重要特點(diǎn)可以簡(jiǎn)述如下:

剪貼板相當(dāng)于一個(gè)“物品臨時(shí)寄存處”,一次只能保存一個(gè)“物品”,而且這個(gè)“物品”是大家共享的。

例如原來使用Word復(fù)制了一段文本放在剪貼板上,現(xiàn)在又使用“畫圖”程序?qū)⒁环鶊D放在剪貼板上,則圖片數(shù)據(jù)將替換掉文本數(shù)據(jù)。

將數(shù)據(jù)放到剪貼板之后,其他程序也可以存取它。比如使用“畫圖”程序?qū)⒁环鶊D放在剪貼板上,則Word、寫字板、Photoshop等其他應(yīng)用程序也可以從剪貼板中獲取這些數(shù)據(jù)。

由于剪貼板可以保存多種類型的數(shù)據(jù),因此.NET定義了一個(gè)DataFormats類,此類包容了一些靜態(tài)字段,定義了剪貼板中可以存放的數(shù)據(jù)類型,如表15-2所示。

表15-2 剪貼板支持的數(shù)據(jù)類型(DataFormats類所包容的靜態(tài)字段)

通過Clipboard類使用剪貼板

剪貼板上可以暫存各種類型的數(shù)據(jù),那么如何編程將特定類型的數(shù)據(jù)放入剪貼板?又如何編程向剪貼板中查詢當(dāng)前數(shù)據(jù)的類型并且將數(shù)據(jù)從剪貼板中取出?

答案是使用.NET Framework提供的Clipboard類。

例如,將一段文本放入剪貼板只需一條語(yǔ)句:

Clipboard.SetDataObject("這是一段文字");

Clipboard.GetDataObject方法用于從剪貼板取出數(shù)據(jù),其定義如下:

public static IDataObject GetDataObject();

此方法返回一個(gè)實(shí)現(xiàn)了IDataObject接口的對(duì)象(稱為“數(shù)據(jù)對(duì)象”)。IDataObject接口定義了四個(gè)方法群,每個(gè)都擁有多個(gè)重載的形式,如表15-3所示:

表15-3 IDataObject所定義的方法群

以下示例代碼從剪貼板上提取文本型數(shù)據(jù),然后顯示在一個(gè)文本框控件中:

IDataObject data=Clipboard.GetDataObject();//獲取剪貼板上的數(shù)據(jù)
if(data.GetDataPresent(DataFormats.Text))//是文本型數(shù)據(jù)嗎?
   TextBox1.Text=data.GetData(DataFormats.Text).ToString();

剪貼板示例1:現(xiàn)在剪貼板上有什么?

示例程序ClipboardInfo可以獲取剪貼板上存放的數(shù)據(jù)信息類型(見圖15-14)。

圖15-14 示例程序ClipboardInfo

圖15-14所示為從“記事本”中復(fù)制文本到剪貼板上后得到的數(shù)據(jù)類型信息。

提示

運(yùn)行ClipboardInfo程序,然后在資源管理器中復(fù)制幾個(gè)文件,使用ClipboardInfo程序查看一下剪貼板中的信息,讀者會(huì)發(fā)現(xiàn)原來在Windows中復(fù)制文件也會(huì)用到剪貼板。

示例程序ClipboardInfo的實(shí)現(xiàn)方法非常簡(jiǎn)單。

先獲取剪貼板上的數(shù)據(jù),再調(diào)用IDataObject.GetFormats方法即可知道剪貼板中的數(shù)據(jù)類型:

IDataObject data=Clipboard.GetDataObject();
string[]dataFormats=data.GetFormats();//獲取當(dāng)前剪貼板數(shù)據(jù)所支持的格式清單

剪貼板實(shí)例2:使用剪貼板保存自定義對(duì)象

由于剪貼板是所有進(jìn)程共享的,所以可以很方便地使用它在不同進(jìn)程間共享信息。

請(qǐng)看實(shí)例UseClipboard,程序運(yùn)行時(shí)的界面如圖15-15所示。

圖15-15 使用剪貼板傳送信息

如圖15-15所示,讀者可以在示例程序中裝入一張圖片,然后在“圖片說明”文本框中輸入文字。

點(diǎn)擊“復(fù)制到剪貼板”按鈕后,再次運(yùn)行另一個(gè)UseClipboard程序,現(xiàn)在就有了兩個(gè)同時(shí)運(yùn)行的UseClipboard進(jìn)程,在新的UseClipboard進(jìn)程窗體中點(diǎn)擊“從剪貼板粘貼”按鈕,可以發(fā)現(xiàn)新的程序主窗體中出現(xiàn)了與第一個(gè)進(jìn)程一模一樣的圖片和文字信息。

這個(gè)示例程序展示的技術(shù)關(guān)鍵點(diǎn)是:

可以在不同進(jìn)程間通過剪貼板傳送可序列化的對(duì)象。

示例程序定義了一個(gè)類MyPic,它所創(chuàng)建的對(duì)象可被放置在剪貼板上共享:

[Serializable]
class MyPic
{
   public Image pic;//圖片
   public string picInfo;//圖片信息說明
}

這里特別要注意的是必須給MyPic類加上“[Serializable]”標(biāo)記,表明此類是可以序列化的。

交叉鏈接

對(duì)象的序列化是指將對(duì)象的當(dāng)前屬性和字段值保存到流中,以便在合適的時(shí)候從流中重新創(chuàng)建對(duì)象。

第13章《對(duì)象的復(fù)制與序列化》詳細(xì)介紹了序列化技術(shù)及其應(yīng)用。

將自定義類型對(duì)象放到剪貼板的關(guān)鍵是DataObject類,它實(shí)現(xiàn)了IDataObject接口。可以將它看成是一個(gè)數(shù)據(jù)容器,存放那些將被放置在剪貼板上的數(shù)據(jù)。

當(dāng)示例程序運(yùn)行時(shí),根據(jù)用戶裝入的圖片和輸入的圖片說明創(chuàng)建MyPic對(duì)象,緊接著將此對(duì)象裝入DataObject對(duì)象中,再調(diào)用Clipboard的SetDataObject方法將DataObject對(duì)象保存到剪貼板中,以下是摘錄的示例代碼:

MyPic obj=……;//創(chuàng)建MyPic對(duì)象
//創(chuàng)建一個(gè)數(shù)據(jù)對(duì)象,將MyPic類型的對(duì)象裝入
IDataObject dataobj=new DataObject(obj);
//其他類型的數(shù)據(jù)也可以裝入同一個(gè)數(shù)據(jù)對(duì)象中,可以取消以下兩句代碼的注釋進(jìn)行試驗(yàn)
//dataobj.SetData(DataFormats.UnicodeText,info);
//dataobj.SetData(DataFormats.Bitmap,bmp);
//復(fù)制到剪貼板上,第二個(gè)參數(shù)表明程序退出時(shí)不清空剪貼板Clipboard.SetDataObject(dataobj,true);

特別需要注意的是,當(dāng)使用Clipboard.SetDataObject方法將一個(gè)DataObject對(duì)象放到剪貼板以后,外界要想訪問此DataObject對(duì)象所包容的“真正”對(duì)象時(shí),必須指明這一“真正”對(duì)象的完整類型名稱:

//剪貼板上有我需要的數(shù)據(jù)嗎?
if(Clipboard.ContainsData("UseClipboard.MyPic"))
{
   IDataObject clipobj=Clipboard.GetDataObject();//讀取數(shù)據(jù)
   //將數(shù)據(jù)轉(zhuǎn)換為需要的類型
   MyPic mypicobj=clipobj.GetData("UseClipboard.MyPic")as MyPic;
   //……
}

如果只允許剪貼板上的數(shù)據(jù)被特定類型的進(jìn)程所使用,則僅需創(chuàng)建一個(gè)DataObject對(duì)象,并在其構(gòu)造函數(shù)中傳入可序列化的對(duì)象就夠了,這時(shí),其他類型的進(jìn)程不能讀取剪貼板的數(shù)據(jù)(因?yàn)樗鼈儾恢谰唧w的數(shù)據(jù)類型),比如UseClipboard示例程序使用DataObject對(duì)象將MyPic對(duì)象復(fù)制到剪貼板后,這時(shí)“記事本”的“編輯”菜單中“粘貼”命令是灰掉的,它認(rèn)為剪貼板上是“空的”。

如果要讓剪貼板上的數(shù)據(jù)被多種類型的程序所使用,可以多次調(diào)用DataObject對(duì)象的SetData方法裝入多種類型的數(shù)據(jù),在裝入時(shí)指明其數(shù)據(jù)類型。請(qǐng)看前一段代碼中的以下兩句代碼:

dataobj.SetData(DataFormats.UnicodeText,info);
dataobj.SetData(DataFormats.Bitmap,bmp);

第1句代碼指明將info字段所引用的字符串對(duì)象按照UnicodeText的格式放到剪貼板上,第2句則將bmp字段所引用的圖片以Bitmap的格式放到剪貼板上。這樣一來,任何一個(gè)可以粘貼UnicodeText和Bitmap類型數(shù)據(jù)的進(jìn)程都可以獲取UseClipboard進(jìn)程保存在剪貼板上的數(shù)據(jù)。

提示

讀者可按以下步驟對(duì)上述代碼進(jìn)行測(cè)試:

1)運(yùn)行UseClipboard程序,選擇一個(gè)圖片并輸入一些文字說明,點(diǎn)擊相應(yīng)按鈕復(fù)制到剪貼板上。

2)打開“附件”中的記事本程序,按Ctrl+V(或從“編輯”菜單中選擇“粘貼”命令),可以看到用戶輸入的圖片說明被復(fù)制到了記事本中。

3)打開“附件”中的“畫圖”程序,按Ctrl+V(或從“編輯”菜單中選擇“粘貼”命令),可以看到圖片被插入到了繪圖面板中。

小結(jié)

剪貼板是由操作系統(tǒng)所提供的供各進(jìn)程共享的數(shù)據(jù)存儲(chǔ)區(qū),使用起來非常方便,但其弱點(diǎn)在于一個(gè)進(jìn)程將數(shù)據(jù)放到剪貼板之后,它沒法通知其他進(jìn)程數(shù)據(jù)已放到剪貼板上了。除非在等待接收數(shù)據(jù)的進(jìn)程中設(shè)計(jì)一個(gè)輔助線程定時(shí)監(jiān)控剪貼板,在數(shù)據(jù)來到時(shí)主動(dòng)從剪貼板中獲取數(shù)據(jù),但這并非最佳方式,后面將介紹更合適的進(jìn)程通信方式。

15.3.2 使用FileSystemWatcher實(shí)現(xiàn)進(jìn)程同步

FileSystemWatcher是.NET Framework所提供的一個(gè)組件,它可以監(jiān)控特定的文件夾或文件,比如在此文件夾中某文件被刪除或內(nèi)容被改變時(shí)引發(fā)對(duì)應(yīng)的事件。

通過使用FileSystemWatcher組件,讓多個(gè)進(jìn)程同時(shí)監(jiān)控一個(gè)文件,就可以讓此文件充當(dāng)“臨時(shí)的”進(jìn)程間通信渠道。

請(qǐng)看示例解決方案FileReaderWriter,它包含兩個(gè)項(xiàng)目:FileReader和FileWriter。

FileWriter項(xiàng)目運(yùn)行時(shí),它使用RichTextBox控件打開并編輯一個(gè)TXT或RTF文件,F(xiàn)ileReader也使用RichTextBox控件顯示一個(gè)TXT或RTF文件的內(nèi)容。

同時(shí)啟動(dòng)FileWriter和FileReade可以同時(shí)有多個(gè)FileReader運(yùn)行,但只能有一個(gè)FileWriter進(jìn)程,若有多個(gè)FileWriter同時(shí)修改文件,程序?qū)伋霎惓!? class=,如果FileWriter和FileReader都選擇了同一個(gè)文件,則在Filewriter程序中單擊“保存”按鈕時(shí),可以看到在FileReader會(huì)同步顯示出文件更新后的內(nèi)容(見圖15-16)。

簡(jiǎn)要介紹一下本示例的關(guān)鍵點(diǎn)。

正確設(shè)置文件的共享與讀寫權(quán)限

由于文件會(huì)被多個(gè)進(jìn)程所訪問,因此FileWriter在保存文件時(shí),必須正確設(shè)定文件流的共享權(quán)限為FileShare.Read:

using(StreamWriter writer=new StreamWriter(new FileStream(
   FileName,FileMode.Create,FileAccess.Write,FileShare.Read),
   Encoding.Default))
{
   writer.Write(txtEditor.Text);
}

圖15-16 使用文件在進(jìn)程間傳送信息

而FileReader在打開文件時(shí),必須將文件流的共享權(quán)限設(shè)為FileShare.ReadWrite:

using(StreamReader reader=new StreamReader(new FileStream(
   FileName,FileMode.Open,FileAccess.Read,FileShare.ReadWrite),
   Encoding.Default))
{
   txtReader.Text=reader.ReadToEnd();
}

監(jiān)控文件內(nèi)容的改變

實(shí)現(xiàn)進(jìn)程通信的關(guān)鍵在于FileSystemWatcher組件(它放在Visual Studio工具箱的“組件”面板中)。它可以監(jiān)控特定文件或文件夾一個(gè)或多個(gè)屬性的改變(見表15-4)。

表15-4 FileSystemWatcher組件可以監(jiān)控的文件(夾)屬性

例如以下代碼將監(jiān)控“C:\test”文件夾及其子文件夾下的所有TXT文件的“大小”或“文件名改變”情景的出現(xiàn):

fileSystemWatcher1.Filter="*.txt";
fileSystemWatcher1.Path="C:\\test";
fileSystemWatcher1.IncludeSubdirectories=true;
fileSystemWatcher1.NotifyFilter=
         NotifyFilters.Size|NotifyFilters.FileName;

FileSystemWatcher組件根據(jù)程序員設(shè)定的NotifyFilter屬性,當(dāng)監(jiān)控對(duì)象的特定屬性發(fā)生改變時(shí),激發(fā)以下事件(見表15-5)。

表15-5 FileSystemWatcher組件的常用事件

FileReader示例程序監(jiān)控指定文件內(nèi)容的改變,然后編碼響應(yīng)FileSystemWatcher組件的Changed事件。

請(qǐng)讀者自行閱讀源代碼,其中的代碼并不復(fù)雜。

FileSystemWatcher小結(jié)

FileSystemWatcher在實(shí)際開發(fā)中是比較實(shí)用的,舉個(gè)例子,在網(wǎng)絡(luò)應(yīng)用程序中可以使用此組件監(jiān)控特定的專用于上傳文件的文件夾,當(dāng)發(fā)現(xiàn)用戶上傳了文件之后,系統(tǒng)可以自動(dòng)地啟動(dòng)一系列的處理流程。

請(qǐng)讀者多動(dòng)動(dòng)腦筋,看看FileSystemWatcher還能用在什么地方。

15.3.3 使用內(nèi)存映射文件實(shí)現(xiàn)進(jìn)程通信

操作系統(tǒng)很早就開始使用“內(nèi)存映射文件(Memory Mapped File)”來作為進(jìn)程間的共享存儲(chǔ)區(qū),這是一種非常高效的進(jìn)程通信手段。Win32 API中也包含有創(chuàng)建內(nèi)存映射文件的函數(shù),然而這些函數(shù)都運(yùn)行于非托管環(huán)境下,在.NET中只能通過平臺(tái)調(diào)用機(jī)制來使用它們,用起來很不方便。幸運(yùn)的是,.NET 4.0新增加了一個(gè)System.IO.MemoryMappedFiles命名空間,其中添加了幾個(gè)類和相應(yīng)的枚舉類型,從而使我們可以很方便地創(chuàng)建內(nèi)存映射文件。

內(nèi)存映射文件原理

所謂“內(nèi)存映射文件”,其實(shí)就是在內(nèi)存中開辟出一塊存放數(shù)據(jù)的專用區(qū)域,這區(qū)域往往與硬盤上特定的文件相對(duì)應(yīng)。進(jìn)程將這塊內(nèi)存區(qū)域映射到自己的地址空間中,訪問它就像是訪問普通的內(nèi)存一樣(見圖15-17)。

在.NET中,使用MemoryMappedFile對(duì)象表示一個(gè)內(nèi)存映射文件,通過它的CreateFromFile方法根據(jù)磁盤現(xiàn)有文件創(chuàng)建內(nèi)存映射文件,此方法有多個(gè)重載形式。

以下示例代碼動(dòng)態(tài)地在當(dāng)前文件夾中創(chuàng)建或打開一個(gè)名為MyFile.dat文件,然后將其映射到系統(tǒng)內(nèi)存中創(chuàng)建一個(gè)內(nèi)存映射文件,分配給此映射文件一個(gè)“MyFile”的名字,并設(shè)定其容量為1MB:

MemoryMappedFile memoryFile=MemoryMappedFile.CreateFromFile(
   "MyFile.dat",FileMode.OpenOrCreate,"MyFile",1024*1024);

圖15-17 內(nèi)存映射文件原理圖

提示

用于創(chuàng)建內(nèi)存映射文件的文件流必須是可讀寫的。

擴(kuò)充閱讀

關(guān)于內(nèi)存映射文件的容量

默認(rèn)情況下,在調(diào)用MemoryMappedFile.CreateFromFile方法基于現(xiàn)有磁盤文件創(chuàng)建內(nèi)存映射文件時(shí),如果不指定內(nèi)存映射文件的容量,那么創(chuàng)建的內(nèi)存映射文件的容量等同于磁盤文件的現(xiàn)有大小。

在設(shè)定內(nèi)存映射文件的容量時(shí),其值不能小于磁盤文件的現(xiàn)有長(zhǎng)度,可以比它大。但要注意這將導(dǎo)致一個(gè)戲劇化的結(jié)果:磁盤文件自動(dòng)增長(zhǎng)到內(nèi)存映射文件聲明的容量大小!

可以多次調(diào)用MemoryMappedFile.CreateFromFile方法,每次傳給它一個(gè)更大的容量數(shù)值以不斷擴(kuò)充磁盤文件的大小。

當(dāng)不再使用一個(gè)MemoryMappedFile對(duì)象時(shí),注意應(yīng)該及時(shí)地調(diào)用其Dispose方法釋放它所占有的系統(tǒng)資源。因?yàn)镸emoryMappedFile實(shí)際上對(duì)應(yīng)著運(yùn)行于操作系統(tǒng)核心的核心對(duì)象(Kernel Object),如果不及時(shí)關(guān)閉,會(huì)造成操作系統(tǒng)核心資源(比如句柄)的浪費(fèi),要等到MemoryMappedFile對(duì)象被CLR垃圾回收,或者整個(gè)進(jìn)程中止時(shí),這些資源才會(huì)被操作系統(tǒng)回收再利用。

另外,內(nèi)存映射文件的容量其實(shí)是指最大允許分配給內(nèi)存映射文件的虛擬內(nèi)存存儲(chǔ)區(qū)的字節(jié)數(shù),并不意味著系統(tǒng)會(huì)馬上分配指定容量的內(nèi)存。進(jìn)程訪問內(nèi)存映射文件時(shí),操作系統(tǒng)如果發(fā)現(xiàn)需要的內(nèi)容還未裝入,就會(huì)從磁盤文件裝入相應(yīng)內(nèi)容到內(nèi)存中。因此,不用擔(dān)心聲明一個(gè)大的內(nèi)存映射文件會(huì)導(dǎo)致內(nèi)存的浪費(fèi)。

當(dāng)MemoryMappedFile對(duì)象創(chuàng)建之后,并不能直接對(duì)其進(jìn)行讀寫,必須通過一個(gè)MemoryMappedViewAccessor對(duì)象(可稱之為“內(nèi)存映射視圖訪問對(duì)象”)來訪問這個(gè)內(nèi)存映射文件。

MemoryMappedFile.CreateViewAccessor方法可以創(chuàng)建MemoryMappedViewAccessor對(duì)象,而此對(duì)象提供了一系列讀寫的方法,用于向內(nèi)存映射文件中讀取和寫入數(shù)據(jù)。

以下示例代碼創(chuàng)建了一個(gè)內(nèi)存映射視圖訪問對(duì)象并使用它寫入數(shù)據(jù):

MemoryMappedFile memoryFile=…;//創(chuàng)建內(nèi)存映射文件
//創(chuàng)建內(nèi)存映射視圖訪問對(duì)象
MemoryMappedViewAccessor accessor=
   memoryFile.CreateViewAccessor(0,1024);
for(int i=0;i<1024;i+=2)
   accessor.Write(i,'c');

注意在上述代碼中要?jiǎng)?chuàng)建內(nèi)存映射視圖訪問對(duì)象時(shí),需要指定它所能訪問的內(nèi)存映射文件的內(nèi)容范圍,這個(gè)“范圍”稱為“內(nèi)存映射視圖(Memory Mapped View)”。可以將它與放大鏡類比,當(dāng)使用一個(gè)放大鏡閱讀書籍時(shí),一次只能放大指定部分的文字。類似地,只能在內(nèi)存映射視圖所規(guī)定的范圍內(nèi)存取內(nèi)存映射文件。

在上述代碼中,我們看到內(nèi)存映射視圖訪問對(duì)象accessor只提取了內(nèi)存映射文件開頭1024個(gè)字節(jié)的內(nèi)容,然后,向其中寫入了512個(gè)“c”字符。

當(dāng)調(diào)用內(nèi)存映射視圖訪問對(duì)象的Write方法時(shí),要指明從哪個(gè)位置(即方法的第一個(gè)參數(shù))開始寫入數(shù)據(jù),并且需要計(jì)算清楚要寫入的數(shù)據(jù)占幾個(gè)字節(jié),這樣,當(dāng)寫入下一個(gè)數(shù)據(jù)時(shí),就知道應(yīng)該從哪個(gè)位置開始。

提示

Write方法中的存取位置是相對(duì)“內(nèi)存映射視圖”而非內(nèi)存映射文件本身的,因此,此位置數(shù)值再加上“內(nèi)存映射視圖”距內(nèi)存映射文件開頭的偏移量才是寫入的數(shù)據(jù)在文件中的真實(shí)位置。

Write方法有多個(gè)重載形式,可以向內(nèi)存映射文件中寫入多種類型的數(shù)據(jù),但要注意計(jì)算清楚其寫入的位置,避免造成數(shù)據(jù)覆蓋問題。

類似地,內(nèi)存映射視圖對(duì)象提供了多個(gè)重載的Read方法,可以從內(nèi)存映射文件中讀取數(shù)據(jù)。

比較有趣的是,在同一個(gè)進(jìn)程中可以針對(duì)同一個(gè)內(nèi)存映射文件創(chuàng)建多個(gè)“內(nèi)存映射視圖訪問對(duì)象”,從而允許我們同時(shí)修改同一個(gè)文件的不同部分,在關(guān)閉這些對(duì)象時(shí)由操作系統(tǒng)保證將所有修改都寫回到原始文件中。

下面來看一個(gè)示例。

在同一進(jìn)程內(nèi)同時(shí)讀寫同一內(nèi)存映射文件

示例項(xiàng)目UseMMFInProcess運(yùn)行時(shí)會(huì)在程序的當(dāng)前目錄下創(chuàng)建一個(gè)“MyFile.dat”文件,然后,創(chuàng)建了兩個(gè)內(nèi)存映射視圖訪問對(duì)象,分別向文件的前半部分和后半部分寫入不同的數(shù)據(jù),然后再?gòu)闹凶x出來(見圖15-18)。

這個(gè)示例展示的技術(shù)很基礎(chǔ),沒有什么特別需要注意的地方,請(qǐng)讀者自行查看源碼。

圖15-18 在同一進(jìn)程內(nèi)同時(shí)讀寫同一內(nèi)存映射文件

使用內(nèi)存映射文件在進(jìn)程間傳送值類型數(shù)據(jù)

在前面的例子中,內(nèi)存映射文件直接與某個(gè)特定的磁盤文件相對(duì)應(yīng),其實(shí)也可以不用創(chuàng)建磁盤文件而直接使用Windows的分頁(yè)文件。這種方式是實(shí)現(xiàn)進(jìn)程間互傳數(shù)據(jù)的典型方式。

調(diào)用MemoryMappedFile.CreateNew或CreateOrOpen方法可以在系統(tǒng)內(nèi)存中直接創(chuàng)建一個(gè)內(nèi)存映射文件,這個(gè)內(nèi)存映射文件所對(duì)應(yīng)的“物理文件”是Windows的系統(tǒng)分頁(yè)文件。兩個(gè)方法都需要給映射文件指定一個(gè)唯一的名稱。不同之處在于CreateOrOpen方法在指定名稱的映射文件存在時(shí)就直接將其返回給進(jìn)程,而CreateNew方法始終是新創(chuàng)建一個(gè)內(nèi)存映射文件。

擴(kuò)充閱讀

Windows的系統(tǒng)分頁(yè)文件和休眠文件

默認(rèn)情況下,在安裝Windows的分區(qū)根目錄下,會(huì)找到兩個(gè)具有“隱藏”屬性文件:pagefile.sys和hiberfil.sys。

  • pagefile.sys是Windows的分頁(yè)文件,用于保存從物理內(nèi)存中換出的內(nèi)存頁(yè),可以用它的一部分來創(chuàng)建內(nèi)存映射文件。
  • hiberfil.sys則是“系統(tǒng)休眠”文件,當(dāng)Windows啟用了休眠功能時(shí),就會(huì)在硬盤上找到這個(gè)文件,它的內(nèi)容是系統(tǒng)休眠時(shí)物理內(nèi)存中的數(shù)據(jù),當(dāng)計(jì)算機(jī)從休眠中“醒”過來時(shí),通過從此文件中加載信息以恢復(fù)上次工作的狀態(tài)。

內(nèi)存映射文件創(chuàng)建好以后,可以如同前面介紹的方法一樣創(chuàng)建視圖訪問對(duì)象,然后使用Read和Write系列方法存取

只要指定同一個(gè)名字,那么多個(gè)進(jìn)程就可以使用同一個(gè)內(nèi)存映射文件交換數(shù)據(jù)。示例UseMMFBetweenProcess展示了在兩個(gè)進(jìn)程間相互交換一個(gè)結(jié)構(gòu)變量的情況(見圖15-19)。

圖15-19 使用內(nèi)存映射文件在進(jìn)程間傳送值類型數(shù)據(jù)

兩個(gè)進(jìn)程要交換的數(shù)據(jù)格式如下:

public struct MyStructure
{
   public int IntValue { get;set;}
   public float FloatValue { get;set;}
}

啟動(dòng)UseMMFBetweenProcess程序的兩個(gè)實(shí)例,在其中一個(gè)窗體上輸入兩個(gè)數(shù)字之后,點(diǎn)擊“保存”按鈕,然后在另一個(gè)進(jìn)程的窗體上點(diǎn)擊“提取”,可以看到文本框中出現(xiàn)了前一個(gè)進(jìn)程寫入的信息。

示例程序采用MemoryMappedFile.CreateOrOpen方法創(chuàng)建或打開一個(gè)內(nèi)存映射文件,然后調(diào)用MemoryMappedViewAccessor類的Write<T>和Read<T>泛型方法向內(nèi)存映射文件中寫入和讀取數(shù)據(jù)。

Write<T>和Read<T>方法中的泛型參數(shù)T必須是值類型(比如整型int和結(jié)構(gòu)),特別地,對(duì)于用戶自定義的結(jié)構(gòu)(struct),要求其成員也必須是值類型。

例如,以下結(jié)構(gòu)將無(wú)法寫入到內(nèi)存映射文件中,因?yàn)槠涑蓡TInfo是string類型的,屬于引用類型。

public struct ErrorStruct
{
   public string Info;
}

之所以要求泛型參數(shù)不能是引用類型,其道理非常簡(jiǎn)單:

如果結(jié)構(gòu)中的某個(gè)成員是引用類型,那么在程序運(yùn)行時(shí),計(jì)算機(jī)無(wú)法知道應(yīng)該向內(nèi)存映射文件中寫入多少個(gè)字節(jié),因?yàn)橐妙愋偷淖兞克玫膶?duì)象位于托管堆中,其占用存儲(chǔ)空間的大小不經(jīng)過計(jì)算是難以確定的,而完成這個(gè)計(jì)算工作將耗費(fèi)不少的系統(tǒng)資源(想想一個(gè)對(duì)象可能又會(huì)引用到另一個(gè)對(duì)象就明白了),這有可能會(huì)嚴(yán)重影響內(nèi)存映射文件讀寫操作效率。

兩個(gè)進(jìn)程不能交換引用類型的數(shù)據(jù),這個(gè)限制似乎還不小,但可以通過對(duì)象序列化技術(shù)來突破這個(gè)限制,在兩個(gè)進(jìn)程間交換任意大小的對(duì)象(只要內(nèi)存映射文件有足夠的容量)。請(qǐng)看下一小節(jié)的示例UseMMFBetweenProcess2。

利用序列化技術(shù)通過內(nèi)存映射文件實(shí)現(xiàn)進(jìn)程通信(見圖15-20)

圖15-20 利用序列化技術(shù)通過內(nèi)存映射文件實(shí)現(xiàn)進(jìn)程通信

如圖15-20所示,運(yùn)行示例程序UseMMFBetweenProcess2的多個(gè)實(shí)例,加載圖片并輸入圖片說明,點(diǎn)擊相應(yīng)按鈕后,可以在多個(gè)進(jìn)程間直接交換以下格式的信息:

[Serializable]
class MyPic
{
   public Image pic;//圖片
   public string picInfo;//圖片信息說明
}

請(qǐng)注意這是一個(gè)引用類型,并且它附加了“[Serializable]”標(biāo)記。

如果要向內(nèi)存映射文件中序列化對(duì)象,必須將內(nèi)存映射文件轉(zhuǎn)換為可順序讀取的流。MemoryMappedFile類的CreateViewStream方法可以創(chuàng)建一個(gè)MemoryMappedViewStream對(duì)象,通過它即可序列化對(duì)象,其代碼框架如下:

//創(chuàng)建或打開內(nèi)存映射文件
MemoryMappedFile memoryFile=MemoryMappedFile.CreateOrOpen(...);
//創(chuàng)建內(nèi)存映射流
MemoryMappedViewStream stream=memoryFile.CreateViewStream();
//創(chuàng)建要在進(jìn)程間交換的信息對(duì)象
MyPic obj=...;
//向內(nèi)存映射流中序列化對(duì)象
IFormatter formatter=new BinaryFormatter();
stream.Seek(0,SeekOrigin.Begin);
formatter.Serialize(stream,obj);

請(qǐng)讀者自行閱讀源碼了解更多技術(shù)細(xì)節(jié),此處不再贅述。

15.3.4 使用WCF通過管道實(shí)現(xiàn)進(jìn)程通信

本小節(jié)從進(jìn)程通信的角度,介紹如何使用WCF通過“命名管道(Named Pipe)”實(shí)現(xiàn)同一臺(tái)計(jì)算機(jī)中的多個(gè)客戶端進(jìn)程同時(shí)向一個(gè)服務(wù)端進(jìn)程傳送消息。

什么是管道?

管道(pipe)”是Windows所提供的一種進(jìn)程間通信機(jī)制,用于在兩個(gè)進(jìn)程之間相互傳送數(shù)據(jù)。

Windows提供了兩種類型的管道。

一種稱為“匿名管道(Anonymous Pipe)”,這種類型的管道只允許單向通信,由于沒有名字,因此要通信的兩個(gè)進(jìn)程應(yīng)該是父子關(guān)系,父進(jìn)程在創(chuàng)建子進(jìn)程時(shí),負(fù)責(zé)將代表匿名管道的句柄管道是由Windows操作系統(tǒng)實(shí)現(xiàn)的,在使用管道時(shí),Windows會(huì)在系統(tǒng)核心為管道創(chuàng)建相應(yīng)的數(shù)據(jù)結(jié)構(gòu)(即系統(tǒng)核心對(duì)象),因此,進(jìn)程需要通過句柄來引用這一數(shù)據(jù)結(jié)構(gòu),而不能直接修改它。傳送給子進(jìn)程,子進(jìn)程獲取此句柄后,即可接收從父進(jìn)程發(fā)來的信息。這種管道雖然占用的資源少,效率較高,但要求通信進(jìn)程必須為父子關(guān)系,因此限制了使用場(chǎng)景,在實(shí)際開發(fā)中用得不多。

另一種稱為“命名管道(Named Pipe)”,這種類型的管道擁有一個(gè)在本機(jī)唯一的名字,可以用于在一個(gè)服務(wù)進(jìn)程和多個(gè)客戶進(jìn)程間同時(shí)進(jìn)行單向(也可以是雙向的)通信。命名管道支持基于消息的通信模式,這就是說,一個(gè)進(jìn)程一次可以向另一方進(jìn)程連續(xù)發(fā)送多個(gè)消息(消息之間通過消息的定界符進(jìn)行劃分),而接收方能夠正確地從接收到的數(shù)據(jù)流中找到消息的定界符,從而可以提取出完整的消息。

從.NET 3.5開始,基類庫(kù)中增加了一個(gè)System.IO.Pipes命名空間,其中提供了幾個(gè)類用于實(shí)現(xiàn)基于管道的進(jìn)程間通信,比如AnonymousPipeClientStream和AnonymousPipeServerStream可用于實(shí)現(xiàn)匿名管道,而NamedPipeClientStream和NamedPipeServerStream可用于實(shí)現(xiàn)命名管道,MSDN中也提供了相應(yīng)的代碼實(shí)例。

使用System.IO.Pipes中的類實(shí)現(xiàn)管道比較繁瑣,WCF也可以使用管道進(jìn)行進(jìn)程間通信,而且更為簡(jiǎn)便和靈活,所以本書不介紹System.IO.Pipes中的類,而是直接介紹在WCF應(yīng)用程序中使用命名管道的方法。

WCF應(yīng)用程序使用命名管道實(shí)現(xiàn)進(jìn)程通信

WCF提供了一個(gè)NetNamedPipeBinding綁定,此綁定在底層使用命名管道實(shí)現(xiàn)進(jìn)程通信。

請(qǐng)看示例UseNamedPipeBetweenProcess(見圖15-21)。

圖15-21 WCF使用命名管道實(shí)現(xiàn)進(jìn)程通信

如圖15-21所示,啟動(dòng)服務(wù)端進(jìn)程之后,可以再啟動(dòng)多個(gè)客戶端進(jìn)程,每個(gè)客戶端進(jìn)程都可以向服務(wù)端進(jìn)程發(fā)送信息。

在服務(wù)端項(xiàng)目的App.config文件中,確定了服務(wù)協(xié)定和服務(wù)的終結(jié)點(diǎn)地址,并指定使用命名管道綁定實(shí)現(xiàn)進(jìn)程通信:

<system.serviceModel>
   <services>
      <service name="WCFServer.frmServer">
         <endpoint address="net.pipe://localhost/WCFServer"
            binding="netNamedPipeBinding"
            contract="WCFServer.IWCFServerService">
         </endpoint>
      </service>
   </services>
</system.serviceModel>

這個(gè)示例中由于使用了NetNamedPipeBinding綁定,因此要求所有進(jìn)程必須運(yùn)行于同一臺(tái)計(jì)算機(jī)上,然而只需改動(dòng)一下App.config文件使用其他類型的綁定,程序代碼不用作太多修改就可實(shí)現(xiàn)網(wǎng)絡(luò)中的多臺(tái)計(jì)算機(jī)上的多個(gè)進(jìn)程間相互通信,這實(shí)際上就是一個(gè)簡(jiǎn)單分布式系統(tǒng)了。

本小節(jié)的這個(gè)實(shí)例,讀者在掌握了WCF的基礎(chǔ)知識(shí)之后,再來看會(huì)發(fā)現(xiàn)它非常的簡(jiǎn)單,而對(duì)WCF開發(fā)分布式系統(tǒng)技術(shù)的介紹,超出了本書的范疇。因此,請(qǐng)讀者參考WCF的相關(guān)技術(shù)資料閱讀本小節(jié)的示例代碼,此處不再贅述。

15.3.5 實(shí)現(xiàn)進(jìn)程間的“通知機(jī)制”

通過前面介紹的內(nèi)容,我們已經(jīng)可以采用很多種方式實(shí)現(xiàn)進(jìn)程之間的數(shù)據(jù)傳送,但大多數(shù)進(jìn)程通信手段都缺乏一種通知機(jī)制,這就是說,如果一個(gè)進(jìn)程負(fù)責(zé)“生產(chǎn)”數(shù)據(jù),另一個(gè)進(jìn)程負(fù)責(zé)“消費(fèi)”數(shù)據(jù),那么應(yīng)該在這兩個(gè)進(jìn)程之間建立一種通知的機(jī)制,當(dāng)“生產(chǎn)者”產(chǎn)生出來數(shù)據(jù)以后,主動(dòng)通知“消費(fèi)者”可以提取數(shù)據(jù)了。

要實(shí)現(xiàn)進(jìn)程之間的通知機(jī)制,可以有很多種方法。本節(jié)介紹一種比較簡(jiǎn)便的利用.NET線程同步對(duì)象Mutex和EventWaitHandle實(shí)現(xiàn)進(jìn)程通知機(jī)制的方法。

交叉鏈接

在.NET應(yīng)用程序中,Mutex和EventWaitHandler主要用于同一進(jìn)程內(nèi)部的線程同步,但如果給它們起一個(gè)名字,則這些對(duì)象就成為“全局”的了,可以被同一臺(tái)計(jì)算機(jī)上的所有進(jìn)程所訪問。第17章《線程同步與并發(fā)訪問共享資源》集中介紹.NET線程同步技術(shù)的內(nèi)容,其中包括Mutex和EventWaitHandler。

請(qǐng)看示例解決方案ProcessSynchronizeExample,此解決方案中包含兩個(gè)Windows Forms項(xiàng)目,ProcessSynchronizeEventSource項(xiàng)目是“發(fā)出通知”者,稱其為“通知發(fā)送端”,而ProcessSynchronizeEventResponsor則是“等待通知者”,稱其為“通知接收端”。

當(dāng)兩個(gè)程序都運(yùn)行起來以后,點(diǎn)擊通知發(fā)送端程序主窗體上的按鈕,則通知接收端程序會(huì)顯示出它所接收到的“按鈕點(diǎn)擊通知”的次數(shù)(見圖15-22)。

圖15-22 示例解決方案ProcessSynchronizeExample

本示例的關(guān)鍵點(diǎn)在于這兩個(gè)進(jìn)程之間的“通知”機(jī)制是如何建立的。

由通知發(fā)送端程序負(fù)責(zé)創(chuàng)建一個(gè)有名字的EventWaitHandle對(duì)象,并將其置于non-signal狀態(tài),通知接收端程序在啟動(dòng)時(shí)嘗試連接這一對(duì)象,如果能連接上,則通知接收端程序會(huì)啟動(dòng)一個(gè)后臺(tái)線程,調(diào)用EventWaitHandle.WaitOne方法等待EventWaitHandle對(duì)象的狀態(tài)轉(zhuǎn)換為signaled狀態(tài)。

在通知發(fā)送端程序的按鈕單擊事件中,調(diào)用EventWaitHandle.Set方法將EventWaitHandle對(duì)象的狀態(tài)轉(zhuǎn)換為signaled狀態(tài),從而使另一端等待的進(jìn)程得到通知。

通知接收端程序得到“按鈕點(diǎn)擊”通知以后,它更新界面顯示,調(diào)用EventWaitHandle.ReSet方法將EventWaitHandle對(duì)象的狀態(tài)恢復(fù)為non-signaled狀態(tài),然后繼續(xù)循環(huán)調(diào)用EventWaitHandle.WaitOne方法等待下一次通知。

示例的另外一個(gè)關(guān)鍵點(diǎn)是如何保證這兩個(gè)程序都只運(yùn)行一個(gè)實(shí)例。

對(duì)于通知發(fā)送端程序而言,它只要調(diào)用EventWaitHandle.OpenExisting方法檢測(cè)一下EventWaitHandle對(duì)象是否己存在即可知道是否自身已有一個(gè)實(shí)例運(yùn)行。

對(duì)于通知接收端程序而言,它啟動(dòng)時(shí)調(diào)用Mutex.OpenExisting方法檢測(cè)一個(gè)特定名字的Mutex對(duì)象是否存在,從而推知自身是否已經(jīng)有一個(gè)實(shí)例在運(yùn)行。

另外通知接收端程序除了需要保證自身只運(yùn)行一個(gè)實(shí)例之外,還必須保證在它運(yùn)行前已有一個(gè)通知發(fā)送端程序在運(yùn)行,代碼中實(shí)現(xiàn)起來也很簡(jiǎn)單,直接用上面介紹的方法檢測(cè)一下EventWaitHandle對(duì)象是否已存在即可。

提示

本節(jié)的示例是有缺陷的,那就是實(shí)現(xiàn)的僅是“單向通知”,換句話說,就是通知發(fā)出方只管發(fā)出通知,它不知道通知接收方是否真的接到了通知。

如果希望實(shí)現(xiàn)消息的“雙向通知”,使用兩個(gè)EventWaitHandle對(duì)象即可。

請(qǐng)讀者結(jié)合本章前面介紹的進(jìn)程通信手段和本節(jié)的進(jìn)程通知機(jī)制,編程實(shí)現(xiàn)經(jīng)典的“生產(chǎn)者”與“消費(fèi)者”模式:“生產(chǎn)者”進(jìn)程負(fù)責(zé)提供產(chǎn)品,“消費(fèi)者”進(jìn)程負(fù)責(zé)消費(fèi)產(chǎn)品,只有“消費(fèi)者”消費(fèi)了產(chǎn)品之后,“生產(chǎn)者”才能繼續(xù)生產(chǎn)。

舉個(gè)例子,可以編寫這樣的一個(gè)實(shí)例:兩個(gè)進(jìn)程通過內(nèi)存映射文件分塊傳送一個(gè)很大的文件。

順便提一下:17.8.3節(jié)《自動(dòng)鎖定的集合——BlockingCollection》介紹了如何使用.NET 4.0新增的BlockingCollection實(shí)現(xiàn)線程間的“生產(chǎn)者-消費(fèi)者”合作模式。

主站蜘蛛池模板: 皋兰县| 吴桥县| 庆安县| 宜黄县| 博兴县| 津市市| 津市市| 通渭县| 东乌| 积石山| 尼勒克县| 新晃| 东乡族自治县| 都兰县| 中山市| 韶山市| 睢宁县| 沐川县| 健康| 延津县| 宣化县| 大同市| 宁阳县| 关岭| 宁德市| 德江县| 旅游| 延寿县| 师宗县| 霞浦县| 泰宁县| 敦化市| 新化县| 陆丰市| 陇西县| 山阳县| 启东市| 泰宁县| 兴国县| 滁州市| 集安市|