多線程開發掃盲系列第一編:進程與進程間通信
進程(process)是一個具有獨立功能的程序在一個數據集合上的一次動態執行過程。這個定義太理論化了,用一句通俗的話取代它:進程可以簡單理解為一個正在運行的程序。
程序與進程的區別可以用圖形像地表達出來。
Window設計了兩種代碼運行環境,用戶模式(User Mode)和核心模式(kernel Mode),普通的應用程序運行於用戶模式中,而操作系統的關鍵代碼(比如負責分配與回收內存、創建和銷毀進程等功能的代碼)運行於核心模式下。 在windows中,”系統調用”主要指win32API中的特定函數,所以,windows應用程序通過調用win32API函數來實現從”用戶模式”到”核心模式”的轉換
句柄與系統核心對像
位於操作系統內核中,僅允許運行於”核心模式”下的代碼訪問的數據被稱為”核心對像”,操作系統在運行時,會在系統核心不斷地創建和銷毀”核心對像”,為了便於跟蹤和訪問這些對像,操作系統為這些對像分配了標識,這是一個32位的整數,被稱為”句柄”。許多win32 API函數通過句柄來定位所要訪問的系統核心對像。在.NET托管環境中,.NET應用程序對”普通對像”和”核心對像”不加區分,使用New關鍵字就可以創建任何一種類型的對像,而對像的銷毀工作郵CLR負責。
Windows操作系統使用線程作為CPU調度的基本單位,一個進程可以划分多個線程,也可以只有一個線程。它擁有一個線程標識(ThreadID),一組CPU寄存器,兩個堆棧和一個專有的線程局部存儲區(Thread Local Storage,TLS)。屬於同一個進程的線程共享進程所擁有的資源。
進程是系統分配各種資源(比如內存)的單位,而線程則是操作系統分配CPU(即處理機調度)的基本單位。
.NET應用程序控制進程的核心是Process類,Process類繼承自Component類,通常又稱為Process組件。Process組件代表一個托管進程,底層封裝的是操作系統的本地進程。另一個重要的類是ProcessStartInfo類,這個類封裝了進程啟動時的各種控制參數。
如下繼承結構圖
使用Process.Start方法啟動進程
Process.Start(“IExplore.exe”)
Process.Start(“IExplore.exe”,”www.baidu.com”)
有時候我們希望向進程傳送一些控制信息,比如此進程打開一個網頁時最小化,可以這么來做
ProcessStartInfo info = new ProcessStartInfo("IExplore.exe");
info.WindowStyle=ProcessWindowStyle.Minimized; //自動最小化
info.Arguments="www.sina.cn"; //自動訪問新浪網
Process.Start(info); //啟動進程
通過調用CloseMainWindow方法發出的結束進程運行的請求不會強制應用程序立即退出,它相當於用戶直接點擊主窗口上的關閉按鈕。應用程序可以在退出前請求用戶確認,也可以拒絕退出。
Kill方法強制關閉一個進程,與CloseMainWindow方法不同,Kill方法實際上是請求操作系統直接結束進程,它不給要關閉的進程保存數據的機會,因此除非要保存的進程沒有任何數據需保存,否則不要采用Kill方法直接結束某個進程。
所謂進程通信,是指正在運行的進程之間相互交換信息。
每個進程都擁有自己的地址空間,其他進程不能直接訪問,因此通常需要通過一個第三方媒介間接地在進程之間交換信息。
剪貼板是最常用的進程間交換信息的媒介之一。
剪貼版相當於一個"物品臨時寄存器",一次只能保存一個"物品",而且這個"物品"是大家共享的,比如使用work復制了一段文本在剪貼板上,現在又使用"畫圖"程序將一幅圖放在剪貼板上,則圖片數據將替換掉文本數據。再比如使用畫圖程序將一幅畫放在剪貼板上,則work,寫字板,photoshop等其它應用程序都可以從剪貼板中獲取這些數據。
剪貼板中可以保存多種類型數據,.NET定義了一個DataFormats類,定義了剪貼板中可以存放的數據類型,如下圖
字段名稱 |
說明 |
Bitmap |
Windows位圖格式 |
Dib |
Windows與設備無關的位圖(DIB)格式 |
EnhancedMetafile |
Windows增強型圖元文件格式 |
Html |
由Html數據組成的文本 |
MetafilePict |
Windows圖元文件格式,Windows客體不直接使用此格式 |
OemText |
標准Windows原始設備制造商(OEM)文本格式 |
Palette |
Windows調色板格式 |
Rtf |
由Rich Text Format(RTF)數據組成的文本 |
Serializable |
可序列化的對像 |
StringFormat |
Windows窗體字符串類格式,Windows窗體使用此格式存儲字符串對像 |
Text |
標准ANSI文本格式 |
UnicodeText |
標准Windows Unicode文本格式 |
如下示例,復制幾個文件或圖片,點擊剪貼板上有什么按鈕,看結果
實現代碼:
private void btnShowBoard_Click(object sender, EventArgs e)
{
IDataObject data = Clipboard.GetDataObject(); //獲取剪貼板上的數據
richTextBox1.Text = "";
if (data == null)
return;
string[] str = data.GetFormats(); //獲取剪貼板上數據類型
foreach (string s in str)
{
richTextBox1.AppendText(s+"\n");
}
}
如下示例,即基於剪貼板交換數據,打開兩個此程序,程序A點裝入圖片-復制到剪貼板,程序B點從剪貼板粘貼。即可看到數據在兩個進程間交換
核心代碼:
//裝入圖片
private void btnLoadImage_Click(object sender, EventArgs e)
{
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
bmp = new Bitmap(openFileDialog1.FileName);
}
}
//保存到剪貼板
private void btnCopyToBoard_Click(object sender, EventArgs e)
{
MyPic mypic = new MyPic { image = bmp, info = info };
IDataObject dataobj = new DataObject(mypic);
dataobj.SetData(DataFormats.UnicodeText, info);
dataobj.SetData(DataFormats.Bitmap, bmp);
Clipboard.SetDataObject(dataobj, true);
}
//從剪貼板粘貼
private void btnPasteFromBoard_Click(object sender, EventArgs e)
{
if (Clipboard.ContainsData("UseClipboard.MyPic") == false)
return;
IDataObject clipobj = Clipboard.GetDataObject();
//將數據轉換為需要的類型
MyPic mypicobj = clipobj.GetData("UseClipboard.MyPic") as MyPic;
//從數據對象中分解出需要的數據
info = mypicobj.info;
pictureBox1.Image = mypicobj.image;
}
剪貼板用起來非常方便,但它有個缺點,它沒法通知其他進程數據已放到剪貼板上了,除非在等待接收數據的進程中設計一個輔助線程定時監控剪貼板,在數據來時主動從剪貼板中獲取數據,但這並不是最佳方式。
FileSystemWatcher是.Net Framework所提供的一個組件,它可以監控特定的文件夾或文件,比如在此文件夾中某文件被刪除或內容被改變時引發對應的事件。如下所示兩個程序一個用於讀,一個用於寫,當在frmwrite修改了文件點保存時,frmreader會同步顯示文件更新后的內容
文件寫入的方法
using (StreamWriter sw = new StreamWriter(new FileStream(FileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Read), Encoding.Default))
{
sw.Write(richTextBox1.Text);
}
文件監控的代碼
namespace FileReader
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
string FileName;
//載入文件
public void LoadFile()
{
try
{
using (StreamReader sr = new StreamReader(new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Encoding.Default))
{
richTextBox1.Text = sr.ReadToEnd();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
public void SetupFileSystemWatch()
{
fileSystemWatcher1.Filter = Path.GetFileName(FileName); //監控的文件
fileSystemWatcher1.Path = Path.GetDirectoryName(FileName); //監控的文件路徑
fileSystemWatcher1.NotifyFilter = NotifyFilters.Size; //當文件大小改變時,觸發事件
}
private void btnCheck_Click(object sender, EventArgs e)
{
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
FileName = openFileDialog1.FileName;
LoadFile();
SetupFileSystemWatch();
}
}
private void fileSystemWatcher1_Changed(object sender, FileSystemEventArgs e)
{
LoadFile();
}
}
}
FileSystemWatcher組件的常用事件
Changed |
當更改指定文件夾中的文件和目錄時發生 |
Created |
當在指定文件夾中創建文件和目錄時發生 |
Deleted |
刪除指定文件夾或目錄時發生 |
Renamed |
重命名指定文件夾中 或目錄時發生 |
所謂內存映射就是在內存中開辟出一塊存放數據的專用區域,這區域往往與硬盤上特定的文件相對應。進程將這塊內存區域映射到自己的地址空間中,訪問它就像是訪問普通的內存一樣,.NET中使用MemoryappedFile對像表示一個內存映射文件,通過它的CreateFromFile方法根據磁盤現有文件創建內存映射文件,MemoryMappedFile對像創建之后,不能直接對其讀寫,還須通過一個MemoryMappedViewAccessor對像來訪問。
如下示例:輸入兩個數,保存到內存取映射文件,然后再打開一個程序點擊提取即可把內存映射中的數據提取出來
代碼如下:
private int FileSize = 1024 * 1024; //設為映射文件大小
private MemoryMappedFile file = null;
private MemoryMappedViewAccessor accor = null;
private void Init()
{
file = MemoryMappedFile.CreateOrOpen("UseMMFBetweenProcess", FileSize); //創建內存取映射文件
accor = file.CreateViewAccessor(); //創建映射文件視圖
toolStripStatusLabel1.Text = "內存文件映射或創建成功";
}
private MyStructure data;
private void btnSave_Click(object sender, EventArgs e)
{
data.IntValue = Convert.ToInt32(txtInt.Text);
data.FloatValue =float.Parse(txtFloat.Text);
accor.Write<MyStructure>(0,ref data); //將結構對像保存到映射文件中
toolStripStatusLabel1.Text = "數據已保存到內文件中";
}
private void btnGet_Click(object sender, EventArgs e)
{
accor.Read<MyStructure>(0, out data); //從映射文件中取出結構對像
txtInt.Text = data.IntValue.ToString(); ;
txtFloat.Text = data.FloatValue.ToString();
toolStripStatusLabel1.Text = "成功從內存中提取了數據";
}
進程之間的數據傳送有多種方式,但大多數進程通信手段都缺乏一種通知機制,本節介紹一種比較簡便的.NET線程同步對像Mutext和EventWaitHandle實現進程通知機制的方法
示例如下:點擊發送端程序中的的click me,接收端窗體會記錄點擊次數
//發送端代碼
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private EventWaitHandle handle;
private const string ProcessSynchronizeEventName = "ProcessSynchronizeEvent";
private void button1_Click(object sender, EventArgs e)
{
handle.Set();
}
private void Form1_Load(object sender, EventArgs e)
{
try
{
handle = EventWaitHandle.OpenExisting(ProcessSynchronizeEventName);
if (handle != null)
{
MessageBox.Show("只能運行一個實例");
handle = null;
Close();
}
}
catch (WaitHandleCannotBeOpenedException)
{
handle = new EventWaitHandle(false, EventResetMode.ManualReset, ProcessSynchronizeEventName);
labInfo.Text = "eventhandle已創建";
}
}
}
//接收端代碼
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private EventWaitHandle hEvent = null;
private const string MyProcess = "ProcessSynchronizeEventResponsor";
private const string ProcessSynchronizeEventName = "ProcessSynchronizeEvent";
private void GetEventHandle()
{
try
{
hEvent = EventWaitHandle.OpenExisting(ProcessSynchronizeEventName);
if (hEvent != null)
{
Thread th = new Thread(WaitForHandle);
th.IsBackground = true;
th.Start();
}
}
catch (WaitHandleCannotBeOpenedException)
{
MessageBox.Show("請先運行程序ProcessSynchronizeEventSource的一個實例");
Close();
}
}
private int count;
void WaitForHandle()
{
while (true)
{
hEvent.WaitOne();
count++;
string info="服務端進程點擊了" + count+"次";
Action<string> showinfo = delegate(string a)
{
labInfo.Text = a;
};
this.Invoke(showinfo, info);
hEvent.Reset();
}
}
private void Form1_Load(object sender, EventArgs e)
{
try
{
Mutex m = Mutex.OpenExisting(MyProcess);
if (m != null)
{
MessageBox.Show("已有一個實例在運行");
Close();
}
}
catch (WaitHandleCannotBeOpenedException)
{
Mutex m = new Mutex(false, MyProcess);
}
GetEventHandle();
}
}