服务器端的高性能实现(六)——状态机和线程池的引入

上一次,我添加了调度队列。这次,我将加入状态机以及线程池。

我先来说一说状态机,然后再说线程池。

写代码也是在干工作。既然干工作,就是为了解决问题。那加入状态机,有什么作用呢?或者说解决了什么问题呢?

首先,没有状态机可以不?当然可以,而且没有任何问题,就像我前面的代码一样。加入状态机,就是为了让程序的逻辑更加清楚,运行更加灵活,同时也可以带来更高的效率。不过事情总是两面的,加入状态机后,程序的运行状态变得更多了,代码的行为更加难以掌控,出现错误后,排查起来也更困难了。总之一句话,风险还是很大的。但是我还是要加的,因为收益也是很大很大的。

目前,我的代码里面有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上面实现一下。
 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值