多线程开发扫盲系列第一编:进程与进程间通信
进程(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+"
");
}
}
如下示例,即基于剪贴板交换数据,打开两个此程序,程序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();
}
}