一个异步响应式WinForm实例

一个异步响应式WinForm实例,第1张

公司需要一个中控平台查看、管理玩家及服务器数据,后端 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主页 查看完整代码!如有分析不到位或不正确的地方欢迎探讨!最后,祝各位策码奔腾!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/721960.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存