到底什么时候该用多线程

郑志强Aloha 郑志强Aloha     2022-10-13     462

关键词:

  我想大多数人在学习多线程时都会对此问题有所顾虑,尽管多线程的概念不难理解,那我们什么时候该用它呢?在大多数情况下,我们写了程序,发现有时必须使用多线程才能得到理想的运行结果,于是我们按照资料调用相关的线程类库或API改善程序,并使其正常运行;但是,到底存不存在一种判断依据,能够明确的指导我们正确地使用多线程机制来解决问题呢?笔者对此进行了一番思考,在此说说我的想法以供参考。

    在开始之前,先引入几个问题,这些问题最终都会在这篇文章里找到答案。

问题情景[0]:设计一个简单的UI:包括一个文本标签和一个按钮,在点击按钮时文本显示由0~10的增长,每秒增长量为1。

问题情景[1]:某同学编写的坦克大战程序中,每一个坦克和子弹均使用一个独立的线程,这是否合理?(当然不合理。。。)如果是你,你会怎么编写这个程序?

 

    笔者认为,多线程的使用离不开“阻塞”这个概念,不过,我想先对这个概念加以扩充,首先先来回想一下阻塞概念原本的意思,简单的说,就是程序运行到某些函数或过程后等待某些事件发生而暂时停止CPU占用的情况;也就是说,是一种CPU闲等状态,不过有时我们使用多线程并不一定是保持闲等时的程序响应,例如在追求高性能的程序中,某条线程在进行高强度的运算,此时若对运算性能不满意,我们也许会再启动若干条运算线程(当然,是在CPU有运算余力的情况下),此时,高强度运算应该归为一种“忙等”状态。

    说到这,多线程归根究底是为了解决"等"的问题,那我们这样定义一个阻塞过程:程序运行该过程所消耗的时间有可能在运行上下文间产生明显的卡顿;这里使用“可能”是因为有些情况下,诸如Socket通信,如果数据源源不断的进入,那么阻塞的时间可能非常小,但我们还是使用了一条线程(nio另说)来处理它,因为我们无法保证数据到来的持续性和有效性;"卡顿"带有主观臆想,也就是说是使用者(人或一些自动化程序)不可接受的。

接下来,对什么时候使用多线程做一个回答:编写程序过程中需要使用某些阻塞过程时,我们才使用多线程,或者更进一步讲,使用多线程的目的是对阻塞过程中的实际阻塞的抽象提取。前半句话应该很好理解,而后面的一句虽然不太好懂,不过它对一个程序应具有的合理线程数量进行了阐释(这点接下来解释)。

 

好了,接下来我们回顾一些两个问题,并对它们做出解答:

问题情景[0]

    为了方便表达,笔者在此采用伪Java代码来阐释解答过程。首先我们有一个Label textShower用于显示文本,Button textChanger作为点击的按钮

这个问题是笔者还是一名小菜时遇到的,当时笔者是这么写的:

[java] view plain copy
 
  1. public class MyFrame  
  2. {  
  3.     Label textShower;  
  4.     Button textChanger;  
  5.     public MyFrame//实例化等省略  
  6.     {  
  7.        textChanger.setOnClickListener(new OnClickListener(){  
  8.     public void onClick(MouseEvent e){  
  9.         for(int i = 1;i <= 10;i++){  
  10.             textShower.setText(i+"");//设置文字  
  11.             Thread.sleep(1000);//等待一秒  
  12.         }  
  13.     }  
  14.     });  
  15.     }  
  16. }  

 

    程序的执行结果是点击后10秒没有响应,然后数值被设定为10;现在知道了,是由于AWT消息线程同时负责着图像的绘制刷新操作,而Thread.sleep属于之前的阻塞过程,导致画面停止响应。

当时老师是这么教给我的:

 

[java] view plain copy
 
  1. public class MyFrame  
  2. {  
  3.     Label textShower;  
  4.     Button textChanger;  
  5.     public MyFrame//实例化等省略  
  6.     {  
  7.        textChanger.setOnClickListener(new OnClickListener(){  
  8.     public void onClick(MouseEvent e){  
  9.     new Thread(){  
  10.         public void run()  
  11.         {  
  12.             for(int i = 0;i < 10;i++){  
  13.                 textShower.setText(i+"");//设置文字  
  14.                 Thread.sleep(1000);//等待一秒  
  15.             }     
  16.         }  
  17.     }.start();  
  18.     }  
  19.     });  
  20.     }  
  21. }  

 

    当然,这样确实能够满足题目要求,我也因此开心了一阵,不过不久我就有了新的问题:每按一次按钮产生一个线程是否合理呢?如果这样的文本组合再多几个,我也要创建更多的线程吗?要是使用者是个熊孩子来这里一通狂按这程序还受得了么....

    后来,在面向对象思想深入人心后,稍微懂面向对象的人都会知道使用抽象来简化程序,只不过在上面的问题中,我们需要抽象的不是具体的实体,而是“实际阻塞”这种抽象概念。

    在上面的代码中,笔者写的第一个onClick函数属于一个阻塞过程,其中sleep属于“实际阻塞”。

    而改版的代码中,只是使用多线程将原本的阻塞过程变为了非阻塞过程,实际上是使用了一个独立的线程将整个阻塞过程包含在内,并没有做任何的抽象。

问题情景[1]:在这个问题中,将主要讨论实际阻塞的抽象和合理线程数量的问题。

    这个情景是不久前一位网友问我的,他的毕业设计是编写一个坦克大战的游戏,在编的差不多的时候,突然想到每一辆坦克、每一发子弹都是用单独的线程不是很合理,问我如何改进。用这个例子说明实际阻塞的抽象再合适不过了,我们先看看他写的代码片段:

坦克类:

 

[java] view plain copy
 
  1. public class Tank extends Thread{  
  2.     float x;//这里以横向移动为例子,只写一个属性  
  3.     float speed = 1f;  
  4.     public void run()  
  5.     {  
  6.         drawtank();//清除上一次的绘制,根据横坐标x画一个坦克  
  7.         x+=speed;  
  8.         Thread.sleep(17);//约合一秒60次  
  9.     }  
  10. }  


子弹类:

 

 

[java] view plain copy
 
  1. public class Bullet extends Thread{  
  2.     float x;//这里以横向移动为例子,只写一个属性  
  3.     float speed = 10f;  
  4.     public void run()  
  5.     {  
  6.         drawbullet();//清除上一次的绘制,根据横坐标x画一个子弹  
  7.         x+=speed;  
  8.         Thread.sleep(17);//约合一秒60次  
  9.     }  
  10. }  



 


    其实这样异步的绘制会使画面产生明显的抖动,而且用于同步的逻辑也十分复杂,并不是一个好的方案。

 

其实上面两个类中的run方法中,只有sleep属于实际阻塞,也就是说是可以被抽象出来的,我们只要一个线程,每过17毫秒执行一些列非阻塞过程即可。

上述过程中,绘制及坐标的运算属于非阻塞过程,我们将其抽象为一个接口:

 

[java] view plain copy
 
  1. public interface Drawable  
  2. {  
  3.     public void draw();  
  4. }  

之后我们书写抽象实际阻塞的线程类:

 

 

[java] view plain copy
 
  1. public class BlockThread extends Thread   
  2. {  
  3.     Collection<Drawable> c = new Collection<Drawable>();  
  4.     public void run()  
  5.     {  
  6.         for(Drawable d:c)  
  7.         {  
  8.             d.draw();  
  9.         }  
  10.         Thread.sleep(17);  
  11.     }  
  12.     //封装对成员c的同步CRUD不赘述  
  13.     public void addDrawable(Drawable d);  
  14.     public void removeDrawable(Drawable d);  
  15.     ...  
  16. }  

最后,坦克和子弹的改动:
坦克类:

 

 

[java] view plain copy
 
  1. public class Tank implements Drawable{  
  2.     float x;//这里以横向移动为例子,只写一个属性  
  3.     float speed = 1f;  
  4.     @Override  
  5.     public void draw()  
  6.     {  
  7.         drawtank();//清除上一次的绘制,根据横坐标x画一个坦克  
  8.         x+=speed;  
  9.     }  
  10. }  

子弹类:

 

 

[java] view plain copy
 
  1. public class Bullet implements Drawable{  
  2.     float x;//这里以横向移动为例子,只写一个属性  
  3.     float speed = 10f;  
  4.     @Override  
  5.     public void draw()  
  6.     {  
  7.         drawbullet();//清除上一次的绘制,根据横坐标x画一个子弹  
  8.         x+=speed;  
  9.     }  
  10. }  

    我们可以发现:原有的实际阻塞过程已经被抽象到一个线程之中,而非阻塞过程,诸如绘制和坐标运算依然作为方法保留到对应类中,这样,无论有多少坦克和炮弹,只要非阻塞过程的运算压总和力不至于逼近阻塞的程度,使用一个线程即可完成所有工作。

 

    而且,如果想要添加游戏元素,例如其他类型的子弹,只需要实现Drawable接口即可。

    写到这,UI的问题也就解决了,诸如sleep这样纯粹延时的阻塞非常容易抽象,我们可以如法炮制,使用一个线程解决所有的数值延时自增的问题。但并不是所有实际阻塞都易于抽象,如socket.read(byte[] b);这样的方法显然没有抽象的余地,因此才引出后来的nio方案。

 

    最后,我们对什么时候使用多线程,以及使用线程的数量做一个总结:在编写程序时,遇到了阻塞过程而不想使整个程序停止响应时,应使用多线程;一个程序的合理线程数量取决于对实际阻塞的抽象程度。

各种机械键盘轴的区别,我到底该用什么轴?

...键盘什么牌子好?》3.《各种机械键盘轴的区别,到底什么轴好》——这篇文章是前辈总结的机械键盘什么轴好之所以有各种机械键盘轴,是为了使用户能 查看详情

为什么要用多线程

1. 先讨论一下CApp类的实质。App就是一个用户界面线程。1.1App对象使一个线程具有了处理消息的能力,而线程本身并没有这个能力,(了解这一点的目的在于弄清楚线程消息的概念。)拥有这种能力的线程也就是用户界面线程... 查看详情

到底该用img还是background-image?(代码片段)

在前端页面的实现过程中,我们经常会遇到这个情况:有一个盒子,盒子里面需要放一张图片。这个时候,我们既可以通过添加image标签来实现,也可以通过设置背景图的形式实现,哪种更好呢?一般情况下,可能就是哪种顺手... 查看详情

springbean什么时候用单例模式什么时候用多例?(代码片段)

SpringBean单例与多例在Spring中,bean可以被定义为两种模式:prototype(多例)和singleton(单例)单例多例说明singleton(单例)只有一个共享的实例存在,所有对这个bean的请求都会返回这个唯一的实... 查看详情

apsx页面if(!ispostback)其用法和作用什么时候该用?

...次打开这个页面的时候要做的事. 一般做一些初始化什么的东西..假设你在page_load()里定义textbox为空,在button_click事件里给写一段往数据库存textb 查看详情

公司ceo和我说:在系统优化的时候,不要轻易用多线程(代码片段)

(一)前言最近一段时间整个公司有不少应用上线,上线后慢慢开始暴露一些问题,除去bug之外,一个很值得关注的点就是系统的优化。毕竟优化系统不仅可以使得程序更加稳定,还能节省一些资源的浪费... 查看详情

公司ceo和我说:在系统优化的时候,不要轻易用多线程(代码片段)

(一)前言最近一段时间整个公司有不少应用上线,上线后慢慢开始暴露一些问题,除去bug之外,一个很值得关注的点就是系统的优化。毕竟优化系统不仅可以使得程序更加稳定,还能节省一些资源的浪费... 查看详情

executorservice的shutdown到底什么时候关闭(代码片段)

最近看了下Java线程池的源码,ExecutorService里面关于shutdown和shutdownNow的注释不太能理解啥意思。直接翻译字面意思是,开启一个有序的关闭,先前提交的任务会被执行,但不接受新任务。如果已关闭,则调用不会产生任何其他影... 查看详情

stringstringbuffer与stringbuilder

...关于这三个类在字符串处理中的位置不言而喻,那么他们到底有什么优缺点,到底什么时候该用谁呢?下面我们从以下几点说明一下  1.三者在执行速度方面的比较:StringBui 查看详情

stringstringbufferstringbuilder的区别(转载)

...关于这三个类在字符串处理中的位置不言而喻,那么他们到底有什么优缺点,到底什么时候该用谁呢?下面我们从以下几点说明一下  1.三者在执行速度方面的比较:StringBuilder > StringBuffer & 查看详情

stringstringbuffer与stringbuilder比较

...关于这三个类在字符串处理中的位置不言而喻,那么他们到底有什么优缺点,到底什么时候该用谁呢?下面我们从以下几点说明一下 1、三者在执行速度方面的比较:    StringBuilder> StringBuffer > St... 查看详情

面对对象-封装

...是大家在工作的时候,又不会真正全部都适用public,那么到底什么情况改用什么修饰符呢?1.属性通常使用pri 查看详情

nsstring属性什么时候用copy,什么时候用strong?

...,通常有两种选择(基于ARC环境):strong与copy。那这两者有什么区别呢?什么时候该用strong,什么时候该用copy呢?让我们先来看个例子。示例我们定义一个类,并为其声明两个字符串属性,如下所示:@interfaceTestStringClass()@property(n... 查看详情

tcp,udp的区别及使用场景

传输层的两大协议TCP和UDP,到底用哪一个?这是所有基于网络通讯的应用程序在设计、开发时需要考虑的。下面,我们先分别来看看TCP和UDP的优缺点,在后面,再来分析一下,什么时候该用TCP、什么时候该用UDP。TCP的优点:可靠... 查看详情

nsstring属性什么时候用copy,什么时候用strong?

...,通常有两种选择(基于ARC环境):strong与copy。那这两者有什么区别呢?什么时候该用strong,什么时候该用copy呢?让我们先来看个例子。示例我们定义一个类,并为其声明两个字符串属性,如下所示:1234@interface TestStringClass&nb... 查看详情

工程实践:到底要不要使用智能指针(代码片段)

工程实践:到底要不要使用智能指针前言从需求开始探讨问题智能指针现状unique_ptr示例shard_ptr接口该不该使用智能指针智能指针作为函数参数智能指针作为函数返回值如何选择智能指针注意不要踩的“坑”总结参考资料前言... 查看详情

stringstringbuffer与stringbuilder之间区别

关于这三个类在字符串处理中的位置不言而喻,那么他们到底有什么优缺点,到底什么时候该用谁呢?下面我们从以下几点说明一下  1.三者在执行速度方面的比较:StringBuilder > StringBuffer > String  2.String&l... 查看详情

stringstringbuffer与stringbuilder之间区别

关于这三个类在字符串处理中的位置不言而喻,那么他们到底有什么优缺点,到底什么时候该用谁呢?下面我们从以下几点说明一下  1.三者在执行速度方面的比较:StringBuilder > StringBuffer > String  2.String&l... 查看详情