目录
3.2 使用 FileSystemWatcher 实现进程同步
1 操作系统的进程与线程管理
1.1 进程的创建与运行
在 Windows 中,使用 Win32 API 中的 CreateProcess 函数创建一个进程,应用程序发出对 CreateProcess 函数的调用指令之后,操作系统其实是启动了一个复杂的处理流程,简单地说,首先是打开可执行程序文件读取相关信息,然后创建与进程相关的核心对象,初始化进程核心对象的各个属性,紧接着为其创建第一个线程(称为 “主线程(Main Thread)”),然后主线程执行应用程序的入口函数,至此整个进程创建完成并投入了运行。
1.2 进程中的线程
线程是 CPU 调度的基本单位,它拥有一个线程标识(Thread ID),一组 CPU 寄存器,两个堆栈(Thread Stack)(注:一个堆栈用于核心模式,另一个堆栈用于用户模式)和一个专有的线程局部存储区(Thread Local Storage,TLS)。
属于同一个进程的线程共享进程所拥有的资源。
关于线程与进程,有以下结论:
进程是系统分配各种资源(比如内存)的单位,而线程则是操作系统分配 CPU(即处理机调度)的基本单位。
1.3 CLR 如何管理进程与线程
.NET 是一个托管的运行环境,在其上运行的进程称为 “托管进程(Managed Process)”,而在托管进程中创建的线程自然就是 “托管线程(Managed Thread)” 了。
对应地,操作系统直接创建和管理的进程和线程被称为 “本地进程(Native Process)” 和 “本地线程(Native Thread)”。
Process 类用于代表托管进程,它实际封装的是操作系统的本地进程,每一个托管进程对象都对应着一个操作系统真是的本地进程。
Thread 类用于表示托管线程,每个 Thread 对象都代表着一个托管线程,而每个托管线程都对应着一个函数(称为 “线程函数”),托管线程的执行过程就是线程函数代码的执行过程,线程函数代码执行完,线程也就执行完了。
在操纵系统中,线程直接运行与进程内部,而在 .NET 托管环境下,托管线程与托管进程之间还有一个中间层次,叫做 “应用程序域(Application Domain)”。
2 进程的启动与终止
2.1 进程启动
Process 类
.NET 应用程序控制进程的核心是 Process 类,它所处的继承层次如图所示:
如图所示,Process 类继承自 Component 类,所以,通常又称其为 “Process 组件”。Process 组件代表一个托管进程,底层封装的是操作系统的本地进程。
ProcessStartInfo 类,这个类封装了进程启动时的各种控制参数。
注:继承自 Component 的类称为 “组件”,类似地,继承自 “Container” 的类称为 “容器”。
使用 Proces.Start 方法启动进程
Process 类的 Start 静态方法用于启动一个进程,例如以下代码将会运行一个 IE 浏览器的实例:
Process.Start("IExplore.exe");
Start 方法有多个重载的版本,以下这个版本可能是比较常用的,它可以向进程提供一个启动参数:
Process.Start("IExplore.exe", "www.baidu.com");
注:如果要启动的 EXE 文件路径不在 Windows 的环境变量中,则必须指明其绝对路径。
Process.Start(@"C:\Users\Devin\Desktop\Best.txt");
向要启动的进程传送信息
在某些情况下,我们可能希望向进程传送一些控制信息,比如希望此进程启动时窗体自动最小化,可以通过向进程传送一个 ProcessStartInfo 控制信息对象完成此功能,示例代码如下:
ProcessStartInfo startInfo = new ProcessStartInfo(fileName);
startInfo.WindowStyle = ProcessWindowStyle.Minimized ; // 启动后最小化
Process.Start(startInfo); // 启动进程
2.2 中止一个进程
可以使用两种方法中止一个进程,调用相应 Process 组件的 CloseMainWindow 方法和 Kill 方法。
请求关闭进程
对于拥有可视化用户界面的 Windows 应用程序,其主线程创建的窗体称为主窗体。在主线程所属的进程对象上调用 CloseMainWindow 方法,将会请求关闭主窗体并结束进程。
通过调用 CloseMainWindow 方法发出的结束进程运行的请求不会强制应用程序立即退出,它相当于用户直接点击主窗口上的 “关闭” 按钮。应用程序可以在退出前请求用户确认,也可以拒绝退出。
private void m_btn_Close_Click(object sender, EventArgs e)
{
Process myProcess = Process.Start(startInfo); // 获取要关闭进程所对应的 Process 对象的引用
myProcess.CloseMainWindow(); // 关闭主窗体
myProcess.Close(); // 释放进程所占用的资源
}
强制关闭进程
可以使用 Process 类的 Kill 方法强制关闭一个进程:
private void KillProcess()
{
Process myProcess = Process.Start(startInfo); // 获取要关闭进程所对应的 Process 对象的引用
myProcess.Kill(); // 强制关闭进程
}
与 CloseMainWindow 方法不同,Kill 方法实际上是请求操作系统直接结束进程,它不给要关闭的进程保存数据的机会,因此除非要结束的进程没有任何数据需要保存,否则不要采用 Kill 方法直接结束某个进程。
3 进程间通信
所谓 “进程通信”,是指正在运行的进程之间相互交换信息。
每个进程都拥有自己的地址空间,其它进程不能直接访问,因此,通常需要通过一个第三方媒介间接地在进程之间交换信息。
剪贴板、共享一个数据文件、COM 组件、内存映射、WCF
3.1 使用剪贴板在进程间传送对象
剪贴板简介
剪贴板是一个供应用程序使用的公有区域。在 Windows 上运行的所有程序,在需要时都可以使用剪贴板存放信息。
由于剪贴板可以保存多种类型的数据,因此 .NET 定义了一个 DataFormats 类,此类包容了一些静态字段,定义了剪贴板中可以存放的数据类型,如表所示。
通过 Clipboard 类使用剪贴板
使用 .NET Framework 提供的 Clipboard 类来读写剪贴板。
Clipboard.SetDataObject("Hello World"); // 将一段文本放入剪贴板中
IDataObject data = Clipboard.GetDataObject(); // 从剪贴板中取出数据到容器中
if (data.GetDataPresent(DataFormats.Text)) // 判断是否为文本类型的数据
{
Console.WriteLine(data.GetData(DataFormats.Text).ToString());
}
Clipboard.GetDataObject 方法用于从剪贴板取出数据,其定义如下:
public static IDataObject GetDataObject();
此方法返回一个实现了 IDataObject 接口的对象(称为 “数据对象”)。IDataObject 接口定义了四个方法群,每个都拥有多个重载形式,如表所示:
使用剪贴板保存自定义的类
由于剪贴板时所有进程共享的,所以可以很方便地使用它在不同进程间共享信息。
示例程序定义了一个类 MyInfo,它所创建的对象可被放置在剪贴板上共享:
[Serializable]
class MyInfo
{
public string message;
public int num;
}
这里特别要注意的是必须给 MyPic 类加上 [Serializable] 标记,表明此类是可以序列化的。
注:对象的序列化是指将对象的当前属性和字段值保存到流中,以便在合适的时候从流中重新创建对象。
将自定义类型对象放到剪贴板的关键是 DataObject 类,它实现了 DataObject 接口。可以将它看成是一个数据容器,存放那些将被放置在剪贴板上的数据。
MyInfo m_objInfo = new MyInfo{ message = "Hello World",num = 322 }; // 创建自定义类的对象
IDataObject data = new DataObject(m_objInfo); // 创建一个数据对象,并将MyInfo类型的对象装入
data.SetData(DataFormats.Text, "Demo"); // 装入其它类型的数据
Clipboard.SetDataObject(data, true); // 放入到剪贴板中,第二个参数表明程序退出时不清空剪贴板
特别需要注意的是,当使用 Clipboard.SetDataObject 方法将一个 DataObject 对象放到剪贴板以后,外界想要访问此 DataObject 对象所包容的 “真正” 对象时,必须指明这一 “真正” 对象的完整类型名称:
// 判断剪贴板上的数据是否是我需要的
if (Clipboard.ContainsData("ConsoleApplication1.MyInfo"))
{
IDataObject readData = Clipboard.GetDataObject(); // 从剪贴板中读取数据
// 将数据转换为需要的自定义类型
MyInfo m_readInfo = readData.GetData("ConsoleApplication1.MyInfo") as MyInfo;
Console.WriteLine(m_readInfo.message); // 输出 Hello World!
Console.WriteLine(m_readInfo.num.ToString()); // 322
Console.WriteLine(readData.GetData(DataFormats.Text)); // 输出 Demo
}
如果只允许剪贴板上的数据被指定的进程所用,则仅需创建一个 DataObject 对象,并在其构造函数中传入可序列化的对象就够了,这时,其它类型的进程不能读取剪贴板的数据(因为它们不知道具体的数据类型)。
小结:剪贴板是由操作系统所提供的供各进程共享的数据存储区,使用起来非常方便,但其弱点在于一个进程将数据放到剪贴板后,它没法通知其它进程数据已放到剪贴板上了。除非在等待接收数据的进程中设计一个辅助线程定时监控剪贴板,在数据来到时主动从剪贴板中获取数据,但这并非最佳方式,后面将介绍更合适的进程间通信方式。
3.2 使用 FileSystemWatcher 实现进程同步
FileSystemWatcher 是 .NET Framework 所提供的一个组件,它可以监控特定的文件夹或文件,比如在此文件夹中某文件被删除或内容被改变时引发对应的事件。
通过使用 FileSystemWatcher 组件,让多个进程同时监控一个文件,就可以让此文件充当“临时的”进程间通信渠道。
本示例包含两个项目:FileReader 和 FileWriter
同时启动 FileReader 和 FileWriter,如果 FileWriter 和 FileReader 都选择了同一个文件,则在 FileWriter 程序中单击 “保存” 按钮时,可以看到在 FileReader 会同步显示出文件更新后的内容。
正确设置文件的共享于读写权限
由于文件会被多个进程所访问,因此 FileWriter 在保存文件时,必须正确设定文件流的共享权限为 FileShare.Read
FileWriter
private string fileName;
private void m_btn_Open_Click(object sender, EventArgs e)
{
openFileDialog1.InitialDirectory = @"C:\Users\Devin\Desktop";
openFileDialog1.Filter = "Word (*.doc;*.docx)|*.doc;*.docx|TXT (*.txt)|*.txt|All files(*.*)|*.*";
openFileDialog1.FilterIndex = 0;
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
fileName = openFileDialog1.FileName;
this.Text = fileName;
LoadFile();
}
}
private void LoadFile()
{
try
{
using(StreamReader sr = new StreamReader(new FileStream(fileName,FileMode.Open,FileAccess.Read, FileShare.ReadWrite),Encoding.Default))
{
m_rtxt_word.Text = sr.ReadToEnd();
}
}
catch (System.Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void m_btn_Save_Click(object sender, EventArgs e)
{
using (StreamWriter sw = new StreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Read), Encoding.Default))
{
sw.Write(m_rtxt_word.Text);
}
}
而 FileReader 在打开文件时,必须将文件流的共享权限设为 FileShare.ReadWrite
FileReader
private void m_btn_Open_Click(object sender, EventArgs e)
{
openFileDialog1.InitialDirectory = @"C:\Users\Devin\Desktop";
openFileDialog1.Filter = "Word (*.doc;*.docx)|*.doc;*.docx|TXT (*.txt)|*.txt|All files(*.*)|*.*";
openFileDialog1.FilterIndex = 0;
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
fileName = openFileDialog1.FileName;
this.Text = fileName;
LoadFile();
SetupFileSystemWatch();
}
}
private void SetupFileSystemWatch()
{
fileSystemWatcher1.Filter = Path.GetFileName(fileName);
fileSystemWatcher1.Path = Path.GetDirectoryName(fileName);
fileSystemWatcher1.NotifyFilter = NotifyFilters.Size;
}
private void LoadFile()
{
try
{
using (StreamReader sr = new StreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Encoding.Default))
{
m_rtxt_Word.Text = sr.ReadToEnd();
}
}
catch (System.Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void fileSystemWatcher1_Changed(object sender, FileSystemEventArgs e)
{
LoadFile();
}
监控文件内容的改变
实现进程通信的关键在于 FileSystemWatcher 组件(它放在 Visual Studio 工具箱的 “组件” 面板中)。它可以监控特定文件或文件夹一个或多个属性的改变。
FileSystemWatcher 小结
FileSystemWatcher 在实际开发中是比较使用的,举个例子,在网络应用程序中可以使用此组件监控特定的专用于上传文件的文件夹,当发现用户上传了文件之后,系统可以自动地启动一系列的处理流程。
3.3 使用内存映射文件实现进程通信
操作系统很早就开始使用 “内存映射文件(Memory Mapped File)” 来作为进程间的共享存储区,这是一种非常高效的进程通信手段。Win32 API 中也包含有创建内存映射文件的函数,然而这些函数都运行于非托管环境下,在 .NET 中只能通过平台调用机制来使用它们,用起来很不方便。幸运的是, .NET 4.0 新增加了一个 System.IO.MemoryMappedFiles 命名空间,其中添加了几个类和相应的枚举类型,从而使我们可以很方便地创建内存映射文件。
内存映射文件原理
所谓 “内存映射文件”,其实就是在内存中开辟出一块存放数据的专用区域,这区域往往与硬盘上特定的文件相对应。进程将这块区域映射到自己的地址空间,访问它就像是访问普通的内存一样。
在 .NET 中,使用 MemoryMappedFile 对象表示一个内存映射文件,通过它的 CreateFromFile 方法根据磁盘现有文件创建内存映射文件,此方法有多个重载形式。
以下示例代码动态地在当前文件夹中创建或打开一个名为 MyFile.dat 文件,然后将其映射到系统内存中创建一个内存映射文件,分配给此映射文件一个 “MyFile” 的名字,并设定其容量为 1MB:
MemoryMappedFile memoryFile = MemoryMappedFile.CreateFromFile("Myfile.dat", FileMode.OpenOrCreate, "MyFile", 1024 * 1024);
提示:用于创建内存映射文件的文件流必须是可读写的。
注:默认情况下,在调用 MemoryMappedFile.CreateFromFile 方法基于现有磁盘文件创建内存映射文件时,如果不指定内存映射文件的容量,那么创建的内存映射文件的容量等同于磁盘文件的现有大小。在设定内存映射文件的容量时,其值不能小于磁盘文件的现有长度,可以比它大。但要注意这将导致一个戏剧化的结果:磁盘文件自动增长到内存映射文件声明的容量大小!
当 MemoryMappedFile 对象创建之后,并不能直接对其进行读写,必须通过一个 MemoryMappedViewAccessor 对象(可称之为 “内存映射视图访问对象”)来访问这个内存映射文件。MemoryMappedFile.CreateViewAccessor 方法可以创建 MemoryMappedViewAccessor 对象,而此对象提供了一系列读写的方法,用于向内存映射文件中读取和写入数据。
MemoryMappedViewAccessor accessor = memoryFile.CreateViewAccessor(0, 1024);
for (int i = 0; i < 1024;i += 2)
{
accessor.Write(i, 'c');
}
注意在上述代码中要创建内存映射试图访问对象时,需要指定它所能访问的内存映射文件的内容范围,这个 “范围” 称为 “内存映射图(Memory Mapped View)”。可以将它与放大镜类比,当使用一个放大镜阅读书籍时,一次只能放大指定部分的文字。类似地,只能在内存映射视图所规定的范围内存取内存映射文件。
在上述代码中,我们看到内存映射视图访问对象 accessor 只提取了内存映射文件开头 1024 个字节的内容,然后,向其中写入了 512 个 “c” 字符。
当调用内存映射视图访问对象的 Write 方法时,要指明从哪个位置(即方法的第一个参数)开始写入数据,并且需要计算清楚要写入的数据占几个字节,这样,当写入下一个数据时,就知道应该从哪个位置开始。
注:Write 方法中的存取位置是相对 “内存映射视图” 而非内存映射文件本身的,因此,此位置数值再加上 “内存映射视图” 距内存映射文件开头的偏移量才是写入的数据在文件中的真实位置。
Write 方法有多个重载形式,可以向内存映射文件中写入多种类型的数据,但要注意计算清楚其写入的位置,避免造成数据覆盖问题。
在同一进程内同时读写同一内存映射文件
示例项目 UseMMFInProcess 运行时会在程序的当前目录下创建一个“MyFile.dat”文件,然后,创建了两个内存映射视图对象,分别向文件的前半部分和后半部分写入不同的数据,然后再从中读出来(图 2)。
private MemoryMappedFile memoryFile = null; // 引用内存映射文件
private const int FileSize = 512;
private MemoryMappedViewAccessor accessor1, accessor2, accessor; // 用于访问内存映射文件的存取对象
// 创建内存映射文件
private void CreateMemoryMapFile()
{
try
{
FileStream fs = new FileStream("Myfile.dat", FileMode.OpenOrCreate, FileAccess.ReadWrite);
memoryFile = MemoryMappedFile.CreateFromFile(fs, "MyFile", FileSize, MemoryMappedFileAccess.ReadWrite, null, System.IO.HandleInheritability.None, false);
accessor1 = memoryFile.CreateViewAccessor(0, FileSize / 2); // 访问文件前半段
accessor2 = memoryFile.CreateViewAccessor(FileSize / 2, FileSize / 2); // 访问文件前半段
accessor = memoryFile.CreateViewAccessor(); // 访问全部文件
lblInfo.Text = "内存文件创建成功";
}
catch (System.Exception ex)
{
lblInfo.Text = ex.Message;
}
}
// 关闭并释放资源
private void DisposeMemoryMapFile()
{
if (accessor1 != null)
accessor1.Dispose();
if (accessor2 != null)
accessor2.Dispose();
if (memoryFile != null)
memoryFile.Dispose();
}
private void btnWrite1_Click(object sender, EventArgs e)
{
if (textBox1.Text.Length == 0)
{
lblInfo.Text = "请输入一个字符";
return;
}
char[] chs = textBox1.Text.ToCharArray();
char ch = chs[0];
for (int i = 0; i < FileSize / 2; i += 2)
accessor1.Write(i, ch);
lblInfo.Text = "字符" + ch + "已写到文件前半部分";
}
private void btnWrite2_Click(object sender, EventArgs e)
{
if (textBox2.Text.Length == 0)
{
lblInfo.Text = "请输入一个字符";
return;
}
char[] chs = textBox2.Text.ToCharArray();
char ch = chs[0];
for (int i = 0; i < FileSize / 2; i += 2)
accessor2.Write(i, ch);
lblInfo.Text = "字符" + ch + "已写到文件后半部分";
}
private void btnShow_Click(object sender, EventArgs e)
{
StringBuilder strText = new StringBuilder(FileSize);
strText.Append("上半段内容:\n");
int j = 0;
for (int i = 0; i < FileSize / 2; i += 2)
{
strText.Append("\t");
char ch = accessor.ReadChar(i);
strText.Append(j);
strText.Append(":");
strText.Append(ch);
j++;
}
strText.Append("\n下半段内容:\n");
for (int i = FileSize / 2; i < FileSize; i += 2)
{
strText.Append("\t");
char ch = accessor.ReadChar(i);
strText.Append(j);
strText.Append(":");
strText.Append(ch);
j++;
}
richTextBox1.Text = strText.ToString();
}
使用内存映射文件在进程间传送值类型数据
在前面的例子中,内存映射文件直接与某个特定的磁盘文件相对应,事实上,我们也可
以不用创建磁盘文件而直接使用 Windows 的分页文件。 这种方式是实现进程间互传数据的典型方式。
调用 MemoryMappedFile.CreateNew() 或 MemoryMappedFile.CreateOrOpen() 方法可以在系统内存(System Memory)中直接创建一个内存映射文件,这个内存映射文件所对应的“物理文件”是 Windows 的系统分页文件。 两个方法都需要给映射文件指定一个唯一的名称。不同之处在于 CreateOrOpen ()方法在指定名称的映射文件存在时就直接将其返回给进程,而CreateNew()方法始终是新创建一个内存映射文件。
内存映射文件创建好以后,可以如同前面介绍的方法一样创建视图对象,然后使用 Read和 Write 系列方法存取。只要指定同一个名字,那么,多个进程就可以使用同一个内存映射文件交换数据。示例 UseMMFBetweenProcess 展示了在两个进程间相互交换一个结构体变量的情况:
One.cs
private const int FileSize = 1024 * 1024;
private MemoryMappedFile file = null;
private MemoryMappedViewAccessor accessor = null;
private MyStructure data;
public struct MyStructure
{
public int IntValue { get; set; }
public float FloatValue { get; set; }
}
private void InitMemoryMappedFile()
{
file = MemoryMappedFile.CreateOrOpen("Devin", FileSize);
accessor = file.CreateViewAccessor();
lblInfo.Text = "内存文件创建或连接成功";
}
private void btnSave_Click(object sender, EventArgs e)
{
try
{
data.IntValue = int.Parse(textBox1.Text);
data.FloatValue = float.Parse(textBox2.Text);
accessor.Write<MyStructure>(0, ref data);
lblInfo.Text = "数据已经保存到内存文件中";
}
catch (Exception ex)
{
lblInfo.Text = ex.Message;
}
}
Two.cs
private void btnLoad_Click(object sender, EventArgs e)
{
accessor.Read<MyStructure>(0, out data);
textBox1.Text = data.IntValue.ToString();
textBox2.Text = data.FloatValue.ToString();
lblInfo.Text = "成功从内存中提取了数据";
}
利用序列化技术通过内存映射文件实现进程通讯