欢迎光临散文网 会员登陆 & 注册

C#实际案例分析(第五弹)——By 流星

2021-12-15 17:39 作者:ForeverMeteor  | 我要投稿

实验五

题目要求

2.2 模拟QQ聊天程序。具体地,同时打开两个程序(Q1,Q2),Q1和Q2分别通过一个文件Msg.dat进行消息交换,要求:
1)QQ程序分为两个线程,线程A负责用户的界面显示,界面输入; 线程B负责监控Msg.dat是否有新的消息到达;
2)B监控到如果有消息到达,把消息传递给线程A;同时,线程B监控是否对方正在进行键盘输入。如果有,让A展示“对方正在输入”的提示;
3)Q1和Q2可以互相传递消息;且仅通过Msg.dat这一个文件进行信息交换。(可以规定存储的策略,方便后继读取和写入)

环境设置

操作系统: Windows 10 x64

SDK: .NET Framework 4.7.2

IDE: Visual Studio 2019

题意分析

这道题的需求较为复杂,我们拆开并归纳一下,大概有以下几个要求:

① 同时打开两个程序,每个程序有两个线程
② 两个程序互相传递消息,且只通过Msg.dat写入或获取信息
③ A线程接受B线程的信息,并负责用户的界面显示(接收到对方正在输入的状态且展示,接收到消息到达且展示),界面输入(写消息,发送消息)
④ B线程监控Msg.dat是否有新的消息到达,并负责给A线程发送信息(对方正在输入,消息到达)

①要求在每个程序运行时拥有两个不同的线程控制不同的功能,这需要用到C#的多线程功能,我们将在下面详细介绍。②要求每个程序之间的信息通信只能靠Msg.dat这一文件,为此我们需要复习先前文件读写的内容。注意,.dat是二进制文件,即我们读和写的时候应以二进制的形式打开。对于③和④,我们需要用全局函数等方式,在线程的while循环中通过改变参数,进而控制他们的功能,在下面我们将详细解析。

我们来捋一下本代码基本的逻辑:首先需要一个父窗体,他下属有两个子窗体,每个子窗体都有两个线程在运行。这两个线程既要发送消息,又要接收消息。如果直接在平行关系的两个窗口的这么多线程之间直接互相传递消息,是非常困难的。于是我们想:能不能借用一个公共平台,也就是他们的父窗体来进行消息之间的传递呢?但是光是这样想,很容易搞出来一大堆全局变量,笔者第一次写的时候就是这样。所以,我们需要将必要的函数封装到子类里面。这样一来思路就变得清晰了,他们的逻辑关系可以归纳如下:

基本逻辑

为了实现我们的代码,接下来我们先介绍多线程。

多线程

一个程序开始运行时他就是一个进程,进程包括执行中的程序和程序所使用到的内存及系统资源。

准确地来说,一个进程就是“一个具有独立功能的程序关于某个数据集合的一次运行活动。”。为了简单理解,我们把它看做一个运行中的程序就行了。

我们往期写过的程序,在同一时间只能干唯一的一件事情(也就是所谓的单线程)。但是,许多情景下要求我们能够同时运行多个程序;或者说,同一时间内执行不同的功能,例如并行计算。于是,多线程应运而生:

每一个进程是由多个线程组成的。线程是程序中的一个执行流。每个线程都有自己的专有寄存器,但代码区共享。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。

也就是说,多线程就是一个程序的“下属”。一个程序可以有好多个这样的下属,在同一时间干着不同的事情。多线程的好处在于可以提高CPU的利用率。比如一个线程等待资源时可以运行另外的线程。线程的状态有就绪、阻塞、运行等。其中阻塞表示暂时停下该线程的运转直到某一特定时刻。一般使用C#中的Sleep函数阻塞本线程一段时间,或是使用Join方法等停下当前线程,直到某线程运行结束。

C#为我们提供了许多有关多线性的类,其中最重要的就是Thread类,其他还有Monitor类,Mutex类等。我们将在接下来的代码解析中学习Thread类的用法。

①:参考文献2

完整代码

Form2/父窗体

Form1/子窗体

代码片段分析

我们先从父窗体(Form2)开始讲起:

在类Form2中

注意!Control.CheckForIllegalCrossThreadCalls = false;是非常不好的做法,它是线程不安全的。C#不允许跨线程直接修改非本线程发起者的控件内容,除非加入上面说的那个语句。但是这么做将造成线程不安全的问题,即不同的线程可能修改同一控件,引发异常。虽然在我们的这次实验中不会出现这样的问题(单机运行情况下无法两窗体同时发送信息),但是我们仍需知道正确的解决方法:使用Invoke函数进行跨线程的调用。鉴于本代码的生命周期,本文不再赘述,读者可自行查阅相关资料了解。

清空文件的做法则是防止上次运行已存在的文件影响本次运行。当然,也可以在此程序运行结束,释放资源时将这个文件删除掉。
②参考网站:https://blog.csdn.net/weixin_38211198/article/details/90708008
https://blog.csdn.net/hongkaihua1987/article/details/7439231

在Form2.button1_Click()中

将创建出来的Q1和Q2的父窗体(Owner)设置为自己,它和接下来要讲的Form1中的这两行代码遥相呼应:

((Form2)Owner).EventComing(Text, BType.KeyBoardTyping);//Form1.richTextBox1_TextChanged()
((Form2)Owner).EventComing(Text, BType.MsgReceive);//Form1.button1_Click()

其中Form中的EventComing函数如下:

是不是有点绕?根据之前的逻辑。我们需要借助父窗体来实现两个子窗体的互相通信。设置好了父窗体之后,我们要通过调用父窗体的public方法来更改子窗体的变量,实现向子窗体输出信息的功能。就像下面的这张图一样:

而能这么做的前提就是设置好父窗体。这里注意窗体的Owner调用出来返回的结果是Form类,而非父窗体的那个子类(Form2),因而需要强制转换,否则将找不到对应的函数。本程序的逻辑我们已经在之前捋清楚了,这些代码就是基本逻辑的实现。

Form2的代码解析已经结束了,但是请不要忘记它!它是父窗体,接下来的许多事情都和它有关系。

在类Form1中

现在可以来解析Thread类的详细运用了。一个线程中只会运行一个函数。创建线程有静态方式(适合无参)和委托方式(都可以,适合有参)。这里我们使用委托的方式,即创建一个委托实例,并将它传到有参线程创建的构造函数ParameterizedThreadStart中,就像下面这样:

而状态区的枚举类型表示线程运行中的状态,根据这些状态去执行不同的操作;调用方字符串表示发起人是“谁”。他们的作用我们会在接下来看到。我们继续往下看:

这段代码就是本次实验的精髓了,我们将详细地剖析它。

首先令人疑惑是事情就是欧·亨利式while (true)。为什么要死循环?其原因在于,线程执行给他的函数只能顺序执行,若不加while(true),先不说其初始状态是不是Normal,它也只能完成一次功能,这显然不是我们想要的。所以我们添加了while循环让它反复执行直至整个exe结束运行或者窗体被关掉。

其次,根据之前的逻辑,线程调用的引起窗体内容变化的,都是它被发起的那个实例中的函数(我把这一集合叫做本地函数,区别于其他),而消息的接收则体现在bType的改变。while循环不停地读取bType的值,一旦发现它改变就作出反应,发给A线程,刚好符合程序的基本逻辑。而发起方string的作用也在此体现,一旦A线程开始执行某一函数,就能获取发起方的消息并显示了。本地函数都是简单的修改对象控件的内容,这里就不再赘述了。

接下来是多线程里线程状态的改变,还记得它都有什么状态吗?

就绪、阻塞、运行……先前的代码中有A.Start()这一函数,表示线程的启动;而这个地方的A.Abort()则是“踩下线程的刹车”,试图让他停下,即线程的终止。刹车踩下不会马上停止,而是要等一段时间,线程也一样。在其他场景下这可能会引发问题,比如在销毁过程中再度调用,故Abort函数常常配合Jion函数使用,阻塞线程直至他完全被终止。由于这次实验中我们只在最后关闭并释放资源的时候才销毁,就不存在这样的问题了。

总结

最终程序的运行结果如下:

最终运行结果

通过这次实验,我们学习到了C#中多线程的知识。多线程的知识很重要,它是现代软件制造必须的内容,不仅在这次实验中用到,在将来的软件开发,Web编程中都将使用得到。本次实验次重要的地方在于捋清楚父窗体和子窗体、子窗体和子窗体间传递消息的组织逻辑,不要凌乱。

但是这个程序仍然存在缺陷。最大的问题在于,委托与事件机制的不够完善。有许多函数甚至是全局变量可以通过委托-事件机制,甚至是接口来解决,从而提高封装的程度,有兴趣的读者可以自己完成。

参考文献

李春葆,曾平,喻丹丹.C#程序设计教程(第3版):清华大学出版社,2015

汪维华,汪维清,胡章平.C#程序设计实用教程(第2版):清华大学出版社,2011

Copyright @ 2021, Bilibili: ForeverMeteor, all rights reserved. 

C#实际案例分析(第五弹)——By 流星的评论 (共 条)

分享到微博请遵守国家法律