公司需要一个中控平台查看、管理玩家及服务器数据,后端 springboot,游戏公司技术栈主要是 C#,选用 WinForm 作为前端展示。
首先要处理的第一问题便是网络通信及数据展示。首先可以确定两点:
1.网络通信只能是单线程;
2.展示界面在主线程并且需要异步处理网络线程的数据,避免主线程在网络通信期间阻塞。
于是本能地想到了用队列来处理:即维护一个阻塞队列和一个守护线程,每次请求后端数据即提交一次队列,守护线程不断轮询队列,拿出队列里的 Msg 处理网络 *** 作。
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BackendClient.Code.Util
{
///
/// 异步网络实现方案一:维护一个阻塞队列和一个守护线程,缺点:守护线程死循环,长期占用系统资源
///
public class MessageQueue
{
private static bool startFlag = false;
private static BlockingCollection queue = new BlockingCollection();
private static Thread thread = new Thread(new ThreadStart(exec));
private static void exec()
{
while (true)
{
Msg msg = queue.Take();
if(msg != null)
{
// do network operation
Thread.Sleep(1500);
SendOrPostCallback action = msg.Act;
msg.Sc.Send(action, "helloworld!!!!!!!!!!!");
}
}
}
public static void poll(Msg msg)
{
if (!startFlag)
{
thread.Start();
startFlag = true;
}
queue.Add(msg);
}
public void test()
{
poll(new Msg(new SynchronizationContext(), new Dictionary() {
{ "username","liz" },
{ "password","123456" }
}, (res) => {
Console.WriteLine(res);
})) ;
}
}
public class Msg
{
public SynchronizationContext Sc;
public Dictionary Data;
public SendOrPostCallback Act;
public Msg(SynchronizationContext sc,Dictionary data, SendOrPostCallback act)
{
this.Sc = sc;
this.Data = data;
this.Act = act;
}
}
}
上面这段代码有一个致命的弊端:守护线程为了轮询阻塞队列,写成了死循环,这样会占用大量资源!
我们知道系统原语 信号量(semaphore),允许指定数量的线程访问临界区,当并发数超过指定的线程数量时,请求访问临界区的线程会进入 semaphore 维护的等待队列(类似于加锁访问,关于锁机制,我之前的一篇文章 Java并发 - 管程相关的思考和总结 有详细讲述)。
于是有了第二种方案:将 semaphore 的临界线程数量设为1,即可实现主线程(负责界面展示)和子线程(网络处理)交替访问临界区,由于等待队列的存在,子线程的网络请求执行完便可以马上通知主线程展示网络数据。没有多余的资源占用,实现起来也不复杂,甚好!
下面只列出关键代码,文末会给出完整代码的 git。
关于信号量的封装:
using System.Threading;
/*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Author $zho.li$ *
* *
* Time 2022/2/28 16:55:49 *
* *
* Describe 信号量通知许可证 *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*/
namespace BackendClient.Code.Support.sema
{
public class SemaphoreLicense
{
private object data;
private SemaphoreLicense() { }
private static SemaphoreLicense instance = new SemaphoreLicense();
public static SemaphoreLicense getInstance()
{
return instance;
}
private Semaphore semaphore = new Semaphore(1, 1);
public void Acquire()
{
semaphore.WaitOne();
}
public void Release()
{
semaphore.Release();
}
public void setData(T _data)
{
this.data = _data;
}
public T getData()
{
return (T)data;
}
public void ClearData()
{
data = null;
}
}
}
网络请求:
// 网络请求前,子线程进入临界区
SemaphoreLicense.getInstance().Acquire();
string res = "";
// network operation......
// 反序列化
T resBody = JsonConvert.DeserializeObject(res);
// 暂存数据
SemaphoreLicense.getInstance().setData(resBody);
// 出临界区
SemaphoreLicense.getInstance().Release();
网络请求完之后的线程同步:
//网络请求
ThreadMgr.DoHttpReq(contentParam, reqMode, url);
// 用于线程间同步
var sc = SynchronizationContext.Current;
ThreadPool.SetMaxThreads(1, 1);
ThreadPool.QueueUserWorkItem((object obj)=> {
// 主线程进入临界区
SemaphoreLicense.getInstance().Acquire();
// 取暂存数据
T res = SemaphoreLicense.getInstance().getData();
// 线程同步
sc.Send(action, res);
// 清理暂存数据
SemaphoreLicense.getInstance().ClearData();
// 主线程出临界区
SemaphoreLicense.getInstance().Release();
});
理论上这样基本就完成需求了,但实测会出现这样一个 bug:
主线程会比子线程先进入临界区!原因不难分析:主线程顺序执行,肯定比经过线程切换的子线程执行快!于是我们还需要一个门栓,确保子线程进入临界区后再轮到主线程:
///
/// 门栓
///
public class CountDownLatch
{
private object lockObj = new Object();
private int counter;
public CountDownLatch(int counter)
{
this.counter = counter;
}
public void Await()
{
lock (lockObj)
{
while (counter > 0)
{
Monitor.Wait(lockObj);
}
}
}
public void CountDown()
{
lock (lockObj)
{
counter--;
Monitor.PulseAll(lockObj);
}
}
}
整体代码流程如下:
CountDownLatch countDown = new CountDownLatch(1);// 主程中执行
// ...
// 子线程进入临界区
countDown.CountDown();// 子线程中执行
// ...
countDown.Await();// 主线程中执行
// 主线程请求进入临界区
// ...
感兴趣的朋友可以到我的 github主页 查看完整代码!如有分析不到位或不正确的地方欢迎探讨!最后,祝各位策码奔腾!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)