上一次,我添加了调度队列。这次,我将加入状态机以及线程池。
我先来说一说状态机,然后再说线程池。
写代码也是在干工作。既然干工作,就是为了解决问题。那加入状态机,有什么作用呢?或者说解决了什么问题呢?
首先,没有状态机可以不?当然可以,而且没有任何问题,就像我前面的代码一样。加入状态机,就是为了让程序的逻辑更加清楚,运行更加灵活,同时也可以带来更高的效率。不过事情总是两面的,加入状态机后,程序的运行状态变得更多了,代码的行为更加难以掌控,出现错误后,排查起来也更困难了。总之一句话,风险还是很大的。但是我还是要加的,因为收益也是很大很大的。
目前,我的代码里面有3个主要的线程:一个是管理socket的,主要工作是遍历处于等待队列里面的socket;一个是监听客户端连接请求的;一个是遍历接收队列里面的socket,接收客户端发来的信息的。
让我先从第一个,也就是管理socket的线程说起。
它在刚刚启动的时候,自动进入IDLE状态,在这个状态里面,线程会挂起,直到被唤醒为止。线程被唤醒后,可能:
1,再次进入IDLE状态,继续挂起等待;
2,进入RUNNING状态;
3,进入SHUTDOWN状态。
当线程进入RUNNING状态后,它首先检测当前socket的个数,如果为0,就重新进入到IDLE状态下面。如果不为0,就像先前做的那样,从等待队列里面取出一个socket进行判断,如果可以接收数据,就把socket放到接收队列里面。这个处理过程还是跟以前一样,没有什么变化。
如果线程进入了SHUTDOWN状态,就清空队列里面的socket,然后线程退出。
这个线程的代码在类ConnectionManager里面,主要变化的部分如下:
首先,添加了一个枚举类型——ConnectionStates,包括3个状态,就是上面提到的那3个。
然后,给这个类添加一个私有成员:
private ManualResetEvent _event = new ManualResetEvent(false);
这个event,用来控制线程的运行和挂起。
接着添加一个方法:
public void SignalEvent(ConnectionStates state)
{
this._state = state;
this._event.Set();
}
这个方法,主要是提供给外部调用者使用的,用来改变线程的状态。但是这里有一个陷阱,一定要先改变state,再调用event的Set方法,唤醒线程。否则很可能会出现很多古怪的问题,原因在后面会有解释。
修改一下AddSocket方法,在保存socket的代码之后,添加下面的代码:
SignalEvent(ConnectionStates.RUNNING);
这行代码的目的,就是当有新的连接的时候,就激活当前线程,进入RUNNING状态。
现在,来看一下加入了状态机后的startWait方法:
主要改变在while里面,如下:
其中被我用...标注的地方,表示没有变化。
private void startWait()
{
...
while(_start)
{
switch(_states)
{
case ConnectionStates.IDLE:
//等待被激活
_event.Reset();
_event.WaitOne();
break;
case ConnectionStates.RUNNING:
//如果没有socket,就进入IDLE状态
if (allsockets.Count == 0)
{
//no socket,change to IDLE
SignalEvent(ConnectionStates.IDLE);
break;
}
try
{
...
if (...)
{
...
//如果有socket可以接受收据,就激活Receiver
Core.GetInstance.Receiver.SignalEvent(ReceiverStates.RUNNING);
}
}
catch(...)
{
//如果出错,就删除这个socket
log.Error(se.Message);
DelSocket(asocket);
}
break;
case ConnectionStates.SHUT_DOWN:
//释放socket,然后退出
_start = false;
RemoveAllSockets();
break;
}
}
}
ConnectionManager主要的改变就是这些了,此外,还对功能做了一些完善,添加了下面的东西:
public void Stop()
{
SignalEvent(ConnectionStates.SHUT_DOWN);
}
现在,我来解释一下SignalEvent里面那两行代码的顺序问题。
假如反过来,像下面这样:
this._event.Set();
this._state = state;
会有什么问题呢?举个可能的例子,如下:
线程1:进入while循环,然后进入case ConnectionStates.IDLE,执行case下面的_event.Reset()和_event.WaitOne(),开始等待
线程2:调用SignalEvent(ConnectionStates.SHUT_DOWN),执行到this._event.Set();
线程1:被激活,从新进入switch,这时候状态还是IDLE,重新开始等待。
线程2:执行this._state = state,改变状态。
好了,现在问题来了。虽然调用了SignalEvent,但是没起作用。那难到后执行Set就可以了么?其实也不是100%安全,但是我们可以想办法去避免。
现在把顺序调整过来,像代码里那样:
this._state = state;
this._event.Set();
看看此时的漏洞:
线程1:进入while循环,然后进入case ConnectionStates.IDLE
线程2:调用SignalEvent,执行上面那两行代码
线程1:执行case下面的_event.Reset()和_event.WaitOne(),开始等待
现在的问题是,线程1没有被成功激活!但是这有个前提,就是在线程1还没有完全执行完IDLE状态的代码的时候,就被调用了SignalEvent。但是这个是调用的问题,比较好解决。
现在,ConnectionManager的工作基本完成了,下面是Receiver的修改。
同样,在Receiver里面,我也加入了状态机,依然是上面提到过的那3个状态。主要的修改在startReceive里面,下面列出代码中主要进行了修改的部分:
private void startReceive()
{
...
while(...)
{
switch(_states)
{
case ReceiverStates.IDLE:
//等待被激活
_event.Reset();
_event.WaitOne();
break;
case ReceiverStates.RUNNING:
current = Core.GetInstance.ConnectionManager.ReceiverQueue.First;
while(...)
{
try
{
//获取socket,下面的处理过程不变。
...
}
catch (System.Exception ex)
{
log.Error(ex.Message);
Core.GetInstance.ConnectionManager.DelSocket(asocket);
}
}
if (Core.GetInstance.ConnectionManager.ReceiverQueue.Count == 0)
{
SignalEvent(ReceiverStates.IDLE);
}
break;
case ReceiverStates.SHUT_DOWN:
_start = false;
break;
}
}
}
整个的状态转换,也与ConnectionManager很相似,因此我就不多说了。
不过还是有一个地方要做点修改,只不过是在ConnectionMananger的AddSocket里面。当调用SignalEvent把ConnectionManager自己激活的时候,顺便再调用一下Receiver的SignalEvent,将Receiver也激活。
接下来,是Listener的修改。它的状态也是3个,跟前面的一样,状态转换的方式也一样。主要改变都在ListenOnAccept里面,下面是代码:
private void ListenOnAccept()
{
...
while(...)
{
switch(_states)
{
case ListenerStates.IDLE:
//这里有点区别,做了点优化。不进行等待,而是直接转换到RUNNING状态。
SignalEvent(ListenerStates.RUNNING);
break;
case ListenerStates.RUNNING:
try
{
//这里进行监听,和以前一样,没有变化
}
catch(...)
{
...
_states = ListenerStates.SHUT_DOWN;
}
break;
case ListenerStates.SHUT_DOWN:
listener.Stop();
_start = false;
break;
}
}
}
因为listener比较简单,我没有为它添加event,因此它的SignalEvent里面只有状态转换,看起来有点名不副实,呵呵。
经过了这么大的修改,上层的Core肯定也会有不小的改变。主要就是start和stop,如下:
public void Start()
{
listener.Start();
connectionManager.Start();
receiver.Start();
}
public void Stop()
{
listener.Stop();
connectionManager.Stop();
receiver.Stop();
}
其实也很好看懂。
真累啊,终于把状态机说完了。下面我说线程池。
其实这个东西很好说,因为我借用了P.Adityanad先生的IOCP组件,这个东西在网上还是很有名的,而且真的很不错。用法很简单,就是下载来,然后添加引用,然后,嗯,就是把线程池的使用添加到Receiver里面了。每当Receiver从一个socket里面读取到一组数据的时候,就把数据打包成一个task,然后丢给线程池去跑就ok了,很简单。
先是声明一个线程池:
private Sonic.Net.ThreadPool threadPool;
再声明一个task
private MyTask task;
然后是实例化:
threadPool = new Sonic.Net.ThreadPool((short)ConfigProvider.GetInstance.MaxThreads, (short)ConfigProvider.GetInstance.ConCurrentThreads);
task = (MyTask)Activator.CreateInstance(Type.GetType(ConfigProvider.GetInstance.Task));
然后是使用,当有数据可以接收的时候,就打包:
//接收数据
byte[] buffer = new byte[32];
int bytes_read = asocket.GetStream().Read(buffer, 0, 10);
//打包成任务
MyTask atask = task.NewTask();
atask.Buffer = buffer;
//放到线程池里
threadPool.Dispatch(atask);
退出的时候,在SHUT_DOWN状态下面添加关闭就可以了,如下:
threadPool.Close();
接着再说一下MyTask这个东西。就是一个接口:
using Sonic.Net;
namespace Server4Win.Util
{
public interface MyTask : ITask//ITask是线程池组件里面的接口
{
MyTask NewTask();//创建一个Task
byte[] Buffer { get;set; }
}
}
再解释一下下面这行代码:
task = (MyTask)Activator.CreateInstance(Type.GetType(ConfigProvider.GetInstance.Task));
这是一个反射调用。我写了一个类,叫做DemoTask,它实现了MyTask接口。然后我把这个DemoTask配置到了配置文件里面,这样就可以通过反射,动态的指定调用哪个接口的实现了。
配置文件里面的内容如下:
<CustomProperty
IP ="127.0.0.1"
Port ="9527"
ConCurrentThreads ="5"
MaxThreads ="5"
Connection ="Server4Win.Core.ConnectionManager"
Task ="Server4Win.Util.DemoTask"//这个就是新添加的Task
/>
这次的内容有点多,写得也有些乱,希望别看晕了。
下回我会重新回到linux下面,把这段时间的成果在linux上面实现一下。