你是否在你的程序中使用过线程?你是否使用线程的时候麻烦不断?你是否感觉你的思路特别对,可就是线程报错?本篇主要通过我之前做的一个连连看小游戏中游戏计时功能,讲解一些我关于C#编程中多线程入门级别的一些问题。
进程与线程
进程
使用电脑,笔记本的时候,我们经常会打开任务管理器,查看当前运行的应用,从任务管理器的进程项中我们可以看到:
从进程项里面可以看到当前所有正在运行的进程。那么究竟什么是进程呢?
官方说法:
进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。是应用程序的一个运行例程,是应用程序的一次动态执行过程。
线程
而从任务管理器的性能项中我们可以看到:
上面用红框标记的位置可以看到,当前总共运行的线程数,那么什么又是线程呢?
官方说法:
线程(Thread)是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
多线程
引入多线程
1、CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,从宏观角度来说是多线程并发的,因为CPU速度太快,察觉不到,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。
2、目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是多个CPU在同一时刻就可以运行多个线程。
多线程的优点:
- 可以同时完成多个任务;
- 可以使程序的响应速度更快;
- 可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;
- 可以随时停止任务;
- 可以设置每个任务的优先级以优化程序性能。
多线程的缺陷:
- 线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。
- 多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。
- 线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
- 线程太多会导致控制太复杂,最终可能造成很多程序缺陷。
一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码。
在C#中,线程是使用Thread类处理的,该类在System.Threading命名空间中。
使用Thread类创建线程时,只需要提供线程入口,线程入口告诉程序让这个线程做什么。
通过实例化一个Thread类的对象就可以创建一个线程。
创建新的Thread对象时,将创建新的托管线程。
Thread类接收一个ThreadStart委托或ParameterizedThreadStart委托的构造函数,该委托包装了调用Start方法时由新线程调用的方法,示例代码如下:
1 | Thread thread=new Thread(new ThreadStart(method)); //创建线程 |
上面代码实例化了一个Thread对象,并指明将要调用的方法method(),然后启动线程。
ThreadStart委托中作为参数的方法不需要参数,并且没有返回值。
ParameterizedThreadStart委托一个对象作为参数,利用这个参数可以很方便地向线程传递参数,示例代码如下:
1 | Thread thread=new Thread(new ParameterizedThreadStart(method));//创建线程 |
创建多线程的步骤:
- 1、编写线程所要执行的方法
- 2、实例化Thread类,并传入一个指向线程所要执行方法的委托。(这时线程已经产生,但还没有运行)
- 3、调用Thread实例的Start方法,标记该线程可以被CPU执行了,但具体执行时间由CPU决定
多线程的System.Threading.Thread的方法
方法名称 | 说明 |
---|---|
Abort() | 终止本线程。 |
GetDomain() | 返回当前线程正在其中运行的当前域。 |
GetDomainId() | 返回当前线程正在其中运行的当前域Id。 |
Interrupt() | 中断处于 WaitSleepJoin 线程状态的线程。 |
Join() | 已重载。 阻塞调用线程,直到某个线程终止时为止。 |
Resume() | 继续运行已挂起的线程。 |
Start() | 执行本线程。 |
Suspend() | 挂起当前线程,如果当前线程已属于挂起状态则此不起作用 |
Sleep() | 把正在运行的线程挂起一段时间。 |
使用多线程
时间线程
本个连连看小游戏是用Form窗体程序做的:
其中游戏计时功能的设计思路主要为:
后台逻辑模块中,即时对于程序计算的实现与程序各种状态的监听,将是整个程序运行的基础。
此模块中将实现对于游戏剩余时间限制和游戏状态的监听与处理。
对于游戏剩余时间的监听,将开启单独的线程进行处理,从而不至于影响主程序逻辑的运行。
游戏的状态的监控处理中,将会实现对于连通的两个图标的消除(即游戏界面的更新),游戏输赢的监听判断,游戏暂停与否等(暂停状态需要同时将剩余时间暂停,而时间监听线程需要知道所处状态,此二者紧密联系)。
在程序中先定义线程,及线程要执行的方法:
1 | private int xx = 100; //设置时间 |
Sleep() 作用把正在运行的线程挂起一段时间。参数即为挂起时间,单位ms,这里设置参数1000,将时间线程挂起1s,实现时间走动1s.
游戏初始化
在窗体加载的时候创建线程,即在Form1_Load()中,写入代码:
1 | t1 = new Thread(new ThreadStart(SS1)); |
在线程t1创建之后,Start()之前,线程他t1的状态是t1.ThreadState = Unstarted,
游戏的开始按钮一开始显示‘开始’成游戏开始功能,点击游戏开始,该按钮显示变为‘暂停’成游戏暂停功能,运行时,点击游戏暂停,该按钮显示变为‘继续’成游戏继续功能。
具体代码如下:
1 | private void button3_Click(object sender, EventArgs e) //开始/暂停/继续 |
游戏开始
游戏刚开始,玩家点击游戏开始按钮,后台的时间线程开启,即在按钮button3的点击事件中添加判断:
1 | if (t1.ThreadState == ThreadState.Unstarted) //游戏刚开始 |
t1.Start()之后的状态是 t1.ThreadState = Running
游戏暂停
游戏运行时,玩家点击游戏暂停按钮,后台的时间线程挂起,即在按钮button3的点击事件中添加判断:
1 | if (isstart == true) //游戏当前为运行状态 |
t1.Suspend()之后的状态是 t1.ThreadState = Suspended
注:只有当线程运行时且未挂起时,才能Suspend(),否则会报错。
游戏继续
游戏暂停时,玩家点击游戏继续按钮,后台的挂起的时间线程恢复,即在按钮button3的点击事件中添加判断:
1 | else //游戏当前不在运行状态(未开始/暂停) |
t1.Resume()之后的状态是 t1.ThreadState = Running
注:只有当线程挂起时,Resume(),否则会报错。
游戏重新开始
游戏重新开始按钮功能:使几乎所有数据初始化,分数,求助次数,时间,地图等等,这个主要针对时间线程讲解时间线程代码:
1 | private void button2_Click(object sender, EventArgs e) //重新开始 |
重新开始时,对时间线程做的最主要的就是:停止线程并使得线程处于预发状态,这样开始按钮功能才能顺利启动线程。
这又分两种情况的下 点击重新开始:
- 游戏时间未耗尽
- 游戏时间耗尽
1、游戏时间未耗尽时,又分两种情况:(1)游戏暂停状态下,(2)游戏运行状态下。
游戏暂停状态下点击重新开始,当前时间线程处于挂起状态,已经满足重新开始的需求,故可忽略操作。
游戏运行状态下点击重新开始,当前时间线程处于非挂起状态,不满足重新开始的需求,故可进行线程挂起操作:
1 | //挂起线程 |
一开始的思路是当前线程处于Running状态,判断时为if (t1.ThreadState == ThreadState.Running),可这样程序的时间线程并不起挂起效果,调试时,发现直接跳过了当前这个操作,可状态明明是对的呀,我通过Thread.ThreadState属性读出它当前的状态,也是ThreadState.Running状态。这让我很纳闷。
一番思考过后,我决定不走‘这条直线’,拐一下弯,不判断当前状态是否等于ThreadState.Running,直接判断当前状态是否不等于ThreadState.Suspended,效果虽然一样,但这时候判断之前的条件应为:线程处于运行状态。即符合挂起之前的状态,才能被挂起。
2、游戏时间耗尽时,由一开始时间线程执行的方法中可知,时间耗尽,当前时间线程t1.Abort(),即终止线程,此时点击重新开始,当前时间线程处于终止状态,不满足重新开始的需求,故可进行线程重启操作:
1 | //线程重启 |
注意:线程Abort之后不能通过start来重启线程,得重新new操作。
这里既进行了t1.ThreadState == ThreadState.Aborted,可为什么还要进行t1.ThreadState == ThreadState.Stopped的判断呢?
一开思路本身就是终止了线程,线程处于终止状态,判断未终止状态时,重新new操作,可程序运行报错,一番调试后发现,当前线程并未处于终止状态,查阅资料后,了解到:
线程Abort()后 ,并不是马上就为ThreadState.Aborted状态
因为公用语言运行时管理了所有的托管的线程,同样它能在每个线程内抛出异常。Abort方法能在目标线程中抛出一个ThreadAbortException异常从而导致目标线程的终止。不过Abort方法被调用后,目标线程可能并不是马上就终止了。因为只要目标线程正在调用非托管的代码而且还没有返回的话,该线程就不会立即终止。而如果目标线程在调用非托管的代码而且陷入了一个死循环的话,该目标线程就根本不会终止。不过这种情况只是一些特例,更多的情况是目标线程在调用托管的代码,一旦Abort被调用那么该线程就立即终止了。
我们得思路是,得知当前线程所初得状态,通过判断后,使得线程重新new操作,可我们目前不知道线程当前的状态是什么。
解决方法是,通过Thread.ThreadState属性读出它的状态,在测试得时候我通过该属性读出了当前线程所处得状态是ThreadState.Stopped
得知了当前状态就好办了,为了保险起见,既进行t1.ThreadState == ThreadState.Aborted,也要进行t1.ThreadState == ThreadState.Stopped的判断,
而要配合下一步挂起线程,new操作后,还得t1.Start()启动线程。
关闭游戏
玩家关闭游戏时,程序最终要做的是将终止线程,即Abort()。
关闭窗口代码为:
1 | private void Form1_FormClosing(object sender, FormClosingEventArgs e) |
关闭游戏得情况分三种:
- 游戏正在运行
- 游戏暂停状态
- 游戏时间耗尽
转换成线程的状态就是:
- ThreadState.Running
- ThreadState.Suspended
- ThreadState.Aborted
1、游戏正在运行时,时间线程当前状态为ThreadState.Running,要终止线程的操作为:
1 | if (t1.ThreadState != ThreadState.Aborted) |
2、游戏暂停状态时,时间线程当前状态为ThreadState.Suspended,这里如果直接用上面的操作是会报错的,注意的是:当线程处于挂起状态Suspended时(暂停),需先将其恢复Resume(),才能终止Abort()。
所以要在之前多加一个判断:
1 | if (t1.ThreadState == ThreadState.Suspended) |
3、游戏时间耗尽,时间线程当前状态为ThreadState.Aborted,已经进行了终止操作,所以本轮无需多加任何操作。
结言
掌握多线程线程的基本Thread方法,并熟知每个方法的前提,从而控制线程的创建、挂起、停止、销毁。
在不知道当前线程所处状态的时候,可以试着用通过Thread.ThreadState属性读出它的状态。
多线程本身就有利有弊,再加上线程状态的操作流程,所以多线程用得好,可以让程序变得更加灵活,用得不好,麻烦可就有得受了。
若对我的连连看小游戏有兴趣,可点击这的连连看通往我的GitHub上查看,觉得不错,可以Star或者fork