基于Sockets的编程中多任务同步的处理机制
目录
摘要
多任务同步的场景和问题
利用委托同步的解决方案
利用线程同步解决的方案
两种方案若干问题探讨
摘要:
基于Sockets的网络编程中,由于Sockets的通讯机制是往返的消息发送机制,因此使得单个任务而多个步骤(每个步骤也可以称作一个小的任务)的完成必定依赖于或者取决于前导的任务,因此编程处理的异步性就体现出来。而使得这些相互连贯的或者有联系的任务得以按照预定的定义流程去执行,很重要的一个关键问题就是如何去同步。采用何种同步的机制,实质上是采用什么样的技术路线去协调多个任务,当然任何一种机制实现同步并非适用于任何的场合,关键还是看需求。本文将在给出多任务同步的一些基本概念以及场景、问题,然后提出采用线程同步和委托的方式实现同步,并做出探讨,指出其存在问题和适用之处。
1 多任务同步的场景和问题
1.1 多任务中涉及到的一些概念
任务:逻辑上完成一步操作或者多步操作的集合,这个概念很泛,原则上没有严格的要求去定义它,需要读者自己去理解。
当前任务:正在执行的任务;
前导任务:当前任务在开始之前必须完成的任务。这实际上是一种前后的关系,不仅前导任务完成之后,后面一个任务才能执行,而且当前任务如何去做的逻辑取决于任务前导任务完成的状态。
后继任务:当前任务完成之后启动的任务。当前任务就是前导任务的后继任务。
1.2 多任务的一个场景和其中的问题
在笔者参与的一个项目中,有这样的一个需求,用于分布式的文件存储服务:
在这个系统里,有多个客户机和一台服务器,每个客户机贡献出一定的磁盘空间组成网络上虚拟磁盘的一部分,因此具体的文件数据是存储在客户机上,而服务器只存储关于文件的信息,如目录的信息,文件的信息(文件名、大小、拥有者、文件的分布情况)。因此用户上传到该系统的文件的物理存储位置实际上是分布在若干台客户机上(每台客户机上是该文件的一个分块)。那么当用户请求下载该文件得时候,需要从所有存储有该文件的分块的机器上下载所有的分块,然后在本机合并为原文件。假如当前有一个文件名为A.rar的文件被分割为3个部分(分割为A.rar.1,A.rar.2,A.rar.3)分别存储在不同的机器(X,Y,Z)上。现在有一台机器H现在希望下载A.rar文件,那么实际上它将从X机器上获得A.rar.1,然后从Y机器上获得A.rar.2,最后从Z机器上获得A.rar.3然后到本机合并成A.rar,可以用流程图表示如下
这三次传输就可以称为一次任务,而这个任务中又包含了三个子任务,而且这三个子任务是否执行是有关系的,假设当前任务是和Y传输A.rar.2文件,之所以能够执行到该任务,必须是在前导任务(和X传输A.rar.1文件)成功的前提下,当然如果当前任务如果执行不成功的话,那么后继任务(和Z传输A.rar.3文件)必然无法开始。很显然这三个任务之间存在着一个同步的问题。
2.利用委托同步的解决方案
当笔者在设计过程中遇到上述问题,笔者想到了使用委托来同步这三个子任务,也就是说在当前任务也就是往X机器传输A.rar.1完成,通过委托回调执行后继任务也就是往Y机器传输A.rar.2任务。先给出文件传输函数:
public void ReadFileData(System.Net.Sockets.Socket sock,string filename,long filesize ,MessageEventHandler downLoadFileCall)
{
FileStream fout = new FileStream(filename, FileMode.Create, FileAccess.Write) ;
//根据文件名将文件读成文件流
NetworkStream nfs = new NetworkStream(sock) ;
//绑定网络流到特定的Socket
long size=filesize ;
long rby=0 ;
try
{
while(rby < size)
//利用判断传输的长度是不是总长度来传输整个文件
{
byte[] buffer = new byte[32767] ;
int i = 0;
try
{
i = nfs.Read(buffer,0,buffer.Length) ;
//将文件流中的特定长度读入缓存。
}
catch(Exception e)
{
string temp = e.ToString();
}
fout.Write(buffer,0,(int)i) ;
//将缓存写入网络流
rby=rby+i ;
ShowProgress(filename+"|DownLoad|"+(rby*100/size));
Thread.Sleep(100);
}
nfs.Flush();
nfs.Close();
nfs = null;
fout.Flush();
fout.Close() ;
downLoadFileCall("DownLoadFileOK");
//上面通过回调执行下面的任务
return;
}
//异常处理
catch(Exception ed)
{
fout.Flush();
fout.Close() ;
this.ViewMessage("A Exception occured in file transfer"+ed.ToString());
}
}
可以看出这个函数的参数表中有一个委托参数downLoadFileCall,同时可以看到在整个文件传输完成的时候调用该委托。下面我们再来看一下这个函数是如何被调用的以及该委托回调的是什么函数:
private void DownSingleFile(string state)
{
if(hustfile != null)
{
hustfile.Dispose();
hustfile = null;
}
SpliteFileInfo currentFile=(SpliteFileInfo)down_FileList[FileIndex];
hustfile = new FileSocket(currentFile.LocationIP,currentFile.UserPort,new Callback(this.ViewMessage),new Callback(this.ViewProcess));
hustfile.Connect();
Thread.Sleep(1000);
if(this.FileIndex<this.down_FileList.Count-1)
{
FileIndex++;
hustfile.GetFile(currentFile.FileName,currentFile.Size,new Callback(this.DownSingleFile));
}
//最后一个文件下在完毕后应该恢复文件
else
{
hustfile.GetFile(currentFile.FileName,currentFile.Size,new Callback(this.RecoverFile));
}
}
public void GetFile(string filename,long filesize,Callback FileDownOver)
{
Socket sock=client_socket;
if(sock!=null)
{
WriteLog("请求下载文件");
this.DownloadFile = FileDownOver;
string cmd="GetFile:";
string Msg=cmd+filename;
this.filename=filename;
this.filesize=filesize;
sender = System.Text.Encoding.ASCII.GetBytes(Msg) ;
sock.Send(sender);
WriteLog("文件下载开始");
(new FileTransfer(ShowProcess)).ReadFileData(sock,FileName,filesize,DownloadFile);
}
}
我们可以关注一下DownSingleFile这个函数中IF-ELSE语句,在判断条件成立的时候,都是调用GetFile函数,同时将DownSingleFile函数自身作为回调函数传入到GetFile中,而在GetFile函数,它将这个传入的委托直接传给了前面给出的ReadFileData函数。这表示在前面两个任务的时候都是ReadFileData在一个任务完成的时候回调DownSingleFile执行下一个传输任务。同时我们可以看到当到达A.rar.2传输完成的时候,回调DownSingleFile此时将执行ELSE中的语句此时传给GetFile,然后由GetFile传给ReadFileData的回调函数将不再是DownSingleFile而是一个叫做RecoveFile,很明显在我们所有的传输任务完成后,下一步将是合并这个文件。
笔者在解决三个子任务的协调的时候是将当前任务的是否执行的决策交给了前导任务,通过委托回调的方式来实现。当前导任务完成后就回调执行下一个任务。当然相同的是后继任务的决策交给了当前任务。只有在当前任务完成的情况下,才回调执行后继任务。
3.利用线程同步解决的方案
在遇到上面的问题的时候,当然也可以使用线程同步的方法来实现。先看看下图:
图中主线程函数顺序和X,Y,Z传输相应的文件,每次启动文件传输线程后,主线程阻塞。而监听线程是在监听两台主机之间的命令传输,当监听线程接收到对方传来的文件传送完毕的消息后就唤醒主线程继续往下执行,其函数实现在此我用伪代码表示如下:
主线程:
首先要定义一个用于线程同步的对象m_Sync.
private ManualResetEvent m_Sync=new ManualResetEvent(false);
接下来就是主线程中负责文件传输的代码。
MainThread()
{
filename = ………;//获得需要传输的文件名。
fileSocket = ………;//生成和X传输的套接子。
(new FileTransfer).transfer(fileSocket,filename,…);//和X机器之间传输文件
m_Sync.WaitOne();
filename = ………;//获得需要传输的文件名。
fileSocket = ………;//生成和Y传输的套接子。
(new FileTransfer).transfer(fileSocket,filename,…);//和Y机器之间传输文件
m_Sync.WaitOne();
filename = ………;//获得需要传输的文件名。
fileSocket = ………;//生成和Z传输的套接子。
(new FileTransfer).transfer(fileSocket,filename,…);//和Z机器之间传输文件
RecoverFile();//恢复整个文件
}
再来关注一下监听线程中如何唤醒阻塞的主线程的。
因为笔者在设计整个系统的时候,有一个专门的监听线程一直负责两端的通讯,在该线程接收到对方文件传输完成的命令后,它将会去调用一个m_Sync.Set()来唤醒被阻塞主线程。从而使得主线程继续下一个任务。用以上的方法也是可以实现了多个任务之间同步的。
4.两种方案若干问题探讨
我们纵观上面的两种方法都是在我们事先确定三个子任务的执行为串行的时候,所采取的解决方案。而我们在解决这个问题的时候,使用串行的传输是不是我们的唯一或者是最优的解决方案呢。答案显然是不一定的,我们也可以使用并行的处理方式,就是三次传输文件任务,我们可以选择并发,这样当三次传输过程都不存在问题的时候,显然要比我们使用的串行的方式来的优越,但是并发的时候遇到网络问题或者其他问题,我们的某个传输不成功,那么理论上说,另外两个传输将不能继续执行下去。也就是说这个里面也有一个多任务的同步问题,这个问题如何去解决,在此将不再去讨论。