一个异步响应式WinForm实例

公司需要一个中控平台查看、管理玩家及服务器数据,后端 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
{
    /// <summary>
    /// 异步网络实现方案一:维护一个阻塞队列和一个守护线程,缺点:守护线程死循环,长期占用系统资源
    /// </summary>
    public class MessageQueue
    {

        private static bool startFlag = false;

        private static  BlockingCollection<Msg> queue = new BlockingCollection<Msg>();

        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<string, object>() {
                { "username","liz" },
                { "password","123456" }
            }, (res) => {
                Console.WriteLine(res);
            })) ;
        }
    }

    public class Msg
    {
        public SynchronizationContext Sc;
        public Dictionary<string, object> Data;
        public SendOrPostCallback Act;

        public Msg(SynchronizationContext sc,Dictionary<string,object> data, SendOrPostCallback act)
        {
            this.Sc = sc;
            this.Data = data;
            this.Act = act;
        }
    }
}

上面这段代码有一个致命的弊端:守护线程为了轮询阻塞队列,写成了死循环,这样会占用大量资源!

我们知道系统原语 信号量(semaphore),允许指定数量的线程访问临界区,当并发数超过指定的线程数量时,请求访问临界区的线程会进入 semaphore 维护的等待队列(类似于加锁访问,关于锁机制,我之前的一篇文章 Java并发 - 管程相关的思考和总结 有详细讲述)。

于是有了第二种方案:将 semaphore 的临界线程数量设为1,即可实现主线程(负责界面展示)和子线程(网络处理)交替访问临界区,由于等待队列的存在,子线程的网络请求执行完便可以马上通知主线程展示网络数据。没有多余的资源占用,实现起来也不复杂,甚好!

下面只列出关键代码,文末会给出完整代码的 git。

关于信号量的封装:

using System.Threading;


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>(T _data)
        {
            this.data = _data;
        }

        public T getData<T>()
        {
            return (T)data;
        }

        public void ClearData()
        {
            data = null;
        }
    }
}

网络请求:

// 网络请求前,子线程进入临界区
SemaphoreLicense.getInstance().Acquire();
string res = "";
// network operation......
// 反序列化
T resBody = JsonConvert.DeserializeObject<T>(res);
// 暂存数据
SemaphoreLicense.getInstance().setData<T>(resBody);
// 出临界区
SemaphoreLicense.getInstance().Release();

网络请求完之后的线程同步:

//网络请求
ThreadMgr.DoHttpReq<T>(contentParam, reqMode, url);
// 用于线程间同步
var sc = SynchronizationContext.Current;

ThreadPool.SetMaxThreads(1, 1);
ThreadPool.QueueUserWorkItem((object obj)=> {
    // 主线程进入临界区
  SemaphoreLicense.getInstance().Acquire();
    // 取暂存数据
  T res = SemaphoreLicense.getInstance().getData<T>();
    // 线程同步
  sc.Send(action, res);
    // 清理暂存数据
  SemaphoreLicense.getInstance().ClearData();
    // 主线程出临界区
  SemaphoreLicense.getInstance().Release();
});

理论上这样基本就完成需求了,但实测会出现这样一个 bug:

主线程会比子线程先进入临界区!原因不难分析:主线程顺序执行,肯定比经过线程切换的子线程执行快!于是我们还需要一个门栓,确保子线程进入临界区后再轮到主线程:

/// <summary>
    /// 门栓
    /// </summary>
    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主页 查看完整代码!如有分析不到位或不正确的地方欢迎探讨!最后,祝各位策码奔腾!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值