Java与Python多线程入门

因为有需要,最近去学习了一下多线程,本来是只需要python的,结果又开了java课,没办法,只好顺带一起学了,做个对比也好。

线程

一讲线程就会和进程联系起来。线程和进程的区别是什么?

  • 进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
    线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位

这讲起来有些抽象,一般来说一个单独的应用会被系统分配一个进程,而一个应用里面的某些方法则可以被当作经进程,我这里找到了一个关于线程进程的比较有意思的故事
然后我们通过程序来加深理解。

再举一个简单的例子,按以往的思维,我写一个程序,要做到边听歌边写作业,以往只能做到要么先听歌,要么先写作业,而多线程就是,给听歌和写作业每个分配一个线程,然后同时运行(这个同时在计算机里面也是有先后的,只是太短,我们以为的同时)

java多线程样例

java多线程有两种方法,一个是就是继承Thread类,一个就是实现Runnable接口;区别呢也就是继承Thread类的只能继承一次,接口的可以实现多次。我把两个都写一下,顺别巩固一下接口相关知识。


为了方便起见,我写了一个工具类,用来作为素数求解工具

Prime工具类

package test;

public class Prime {
	/*判断是否为素数*/
	public boolean isPrime(int num) {
		if(num<2||(num!=2&&num%2==0))return false;
		int s=(int) Math.sqrt(num);
		for(int i=3;i<=s;i+=2) {
			if(num%i==0)
				return false;
		}
		return true;
	}
	/*计算区间内素数*/
	public int PrimeNum(int start,int end) {
		int count=0;
		for(int i=start;i<=end;i++) {
			if(isPrime(i))
				count++;
		}
		return count;
	}
}

首先我们先来做个对比的,求五千万以内的素数个数

    public static void main(String[] args) {
		// TODO Auto-generated method stub
		int count = 50000000;//五千万
		
		/* 获取当前系统时间*/
		long Time1 =  System.currentTimeMillis();
		System.out.println("素数 "+Prime.PrimeNum(1,count)+"个");
		long Time2 =  System.currentTimeMillis();
		System.out.println("耗时"+(Time2-Time1)/1000.0+" s");
		
	}

结果如下:
image1
这里花了30多秒计算出来五千万个素数,当然算法也比较优越,如果是最原始的算法,应该会有一个多小时,昨天我跑八百万都花了十几分钟,也可能是电脑太渣了。

继承Thread类

关于继承Thread类的,其实也就是重写Thread类的run方法,run方法里面是线程start(开始)后运行的东西,我们在run函数里面写下我们想让执行的东西,每次start就会开启一个新线程执行run;

package test;
import java.util.Vector;

import test.Prime;
public class CulPrime extends Thread{
	private static int num = 0;
	private int start;
	private int end;
	

	public CulPrime(int start,int end) {
		if(start>end) {
			System.out.println("不合理的区间");
		}else {
			this.start=start;
			this.end=end;
		}	
	}
	

	public void run() {
		System.out.println(Thread.currentThread().getName()+"开始计算"+start+","+end);
		int count = Prime.PrimeNum(start,end);
		num += count;
		System.out.println(Thread.currentThread().getName()+"计算出 素数"+count+"个");
		System.out.println(Thread.currentThread().getName()+"计算完成");
	}


	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int count = 50000000;//五千万
		
		Vector<Thread> threadvector=new Vector<Thread>(); //用于储存所有线程
		/* 区间划分 为了方便,分成五段,每个线程计算一千万 */
		for(int i=1;i<count;i+=count/5) {
			threadvector.add(new CulPrime(i, i+count/5-1));
		}
		long Time3 =  System.currentTimeMillis();
		/* 线程开始 */
		for(Thread t: threadvector) {
			t.start();
		}
		/* 等待所有线程结束 */
		for(Thread t: threadvector) {
			try {
				t.join();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		long Time4 =  System.currentTimeMillis();
		System.out.println("多线程计算: 素数 "+num+"个");
		System.out.println("多线程计算"+"耗时"+(Time4-Time3)/1000.0+" s");
		
	}

}

结果如下:
image3.png
差别很明显,计算结果一样,但是时间差了几倍。

实现Runnable接口

首先,什么是接口,接口又是怎么实现的,我个人认为接口其实就是封装思维的最好体现,多个类之间通过接口访问,而不直接访问内部成员。就像是人和人之间,我们可以通过交流的方式获取别人的信息,而不是直接通过读取别人的脑子一样,这个三体人的样子我还记忆犹新。
我这里找到了一个usb接口的样例,讲道理接口更多像是一种协议,规定了访问方式,访问规则等待。
我们再来看看接口的实现方式,虽然最后时间结果应该是差不多的

package test;
import java.util.Vector;

import test.Prime;

 /*
    * 使用Runnable接口的步骤
    * 1.建立Runnable对象
    * 2.使用参数为Runnable对象的构造方法创建Thread实例
    * 3.调用start()启动线程
*/
public class CulPrime implements Runnable{
	private static int num = 0;
	private int start;
	private int end;
	
	public CulPrime(int start,int end) {
		if(start>end) {
			System.out.println("不合理的区间");
		}else {
			this.start=start;
			this.end=end;
		}	
	}
	
	public void run() {
		System.out.println(Thread.currentThread().getName()+"开始计算"+start+","+end);
		int count = Prime.PrimeNum(start,end);
		num += count;
		System.out.println(Thread.currentThread().getName()+"计算出 素数"+count+"个");
		System.out.println(Thread.currentThread().getName()+"计算完成");
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int count = 50000000;//五千万

		Vector<Thread> threadvector=new Vector<Thread>(); //用于储存所有线程
		/* 区间划分 为了方便,分成五段,每个线程计算一千万 */
		for(int i=1;i<count;i+=count/5) {
			threadvector.add(new Thread(new CulPrime(i, i+count/5-1)));
		}
		long Time3 =  System.currentTimeMillis();
		/* 线程开始 */
		for(Thread t: threadvector) {
			t.start();
		}
		/* 等待所有线程结束 */
		for(Thread t: threadvector) {
			try {
				t.join();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		long Time4 =  System.currentTimeMillis();
		System.out.println("多线程计算: 素数 "+num+"个");
		System.out.println("多线程计算"+"耗时"+(Time4-Time3)/1000.0+" s");
		
	}
}

结果如下
image2
显然,程序上只有一个调用的区别,而实际的计算速度,差别不大。接口的好处在于,可以有多个类实现接口,而继承只能是单个的。比如说我有学生类,老师类,人类等等,我可以每个类都实现接口,然后在一个程序里创造一个对象就可以,而不是每次都继承Thread类。

当然上面的例子里我开了5个线程,那么线程是不是越多越好?或者是有个什么限定?
在这里插入图片描述
很显然,我上面开了100个线程,明显比5个线程的快了一些,但是线程数目不是越多越好,
在这里插入图片描述
对于单核cpu来说,线程只是合理的分配时间,把每个线程都划分一个时间段,以此加快速度,但是对于持久计算来说,先算和后算的区别不是太大;对于多核cpu来说,开的线程数量合适就可以使cpu的每个核都分配一个线程,这样运算速度自然提升。

python多线程样例

Python中使用线程有两种方式:函数或者用类来包装线程对象,简单来说就是,我的线程运行的要么是函数,要么是对象


Python官方提供了两个线程包thread(python3改为_thread))和threading,当然thread的功能还是比较简陋的,所以我这里只拿threading做例子,毕竟人还是要与时俱进。

函数式线程

实际上就是我有几个函数,然后每个函数分配线程而已。同样拿求素数为例子吧,老规矩,先来一个工具类Prime,因为python是解释型语言的原因,我们把计算数据范围调小.

class Prime:
    @staticmethod # 静态方法声明
    def isPrime(num):
        if num < 2 or (num > 2 and num % 2 == 0):
            return False
        n = int(math.sqrt(num))
        for i in range(3, n + 1, 2):
            if num % i == 0:
                return False
        return True

    @staticmethod
	# 该函数用于多线程测试
    def PrimeNum(ThreadName, start, end):
        count = 0
        print(f'{ThreadName}开始计算{start}到{end}区间')
        for i in range(start, end + 1, 1):
            if Prime.isPrime(i):
                count += 1
        print('{0}计算完成'.format(ThreadName))
        return count

    @staticmethod
	# 该函数用于单线程测试
    def PrimeNum(start, end):
        count = 0
        print(f'开始计算{start}到{end}区间')
        for i in range(start, end + 1, 1):
            if Prime.isPrime(i):
                count += 1
        print('计算完成 素数个数为{0}'.format(count))
        return count

然后我们先测试一下单线程状态下的计算速度和时间

if __name__ == '__main__':
    num = 0
    step = 1000000 # 一百万
    StartTime = time.time()
    PrimeNum(1,1*step)
    EndTime = time.time()
    print("耗时: ", EndTime - StartTime, "s")

image5
然后就是多线程下的计算.

if __name__ == '__main__':
    num = 0
    step = 200000 # 二十万
    StartTime = time.time()
    count = [1, step + 1, 2 * step + 1, 3 * step + 1, 4 * step + 1]  # 一百万
    threads = []
    j = 0
    for i in count:
        threads.append(threading.Thread(args=["Thread-" + str(j), i, i + step - 1, ], target=Prime.PrimeNum))
        j += 1
    try:
        StartTime = time.time()
        # 开始所有进程
        for t in threads:
            t.start()
        # 等待所有进程结束
        for t in threads:
            t.join()
        EndTime = time.time()
        print("耗时: ", EndTime - StartTime, "s")
    except Exception as e:
        print("Error: 无法启动线程", e)
    EndTime = time.time()
    print("耗时: ", EndTime - StartTime, "s")

image6

类继承Thread类实现多线程

class myThread (threading.Thread):
	def __init__(self,name):
		self.name=name

    def run(self):
        # 线程主体
		print_name(self.name)

def print_name(name):
	print(name)

# 创建新线程
thread1 = myThread("Thread-1")
thread2 = myThread("Thread-2")

# 开启新线程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("退出主线程")

这里我只给一个模板,实际上继承之后只要像Java那样实现run方法就行了。

python多线程总结

很显然python利用计算素数的效率慢很多,而且多线程并不比单线程计算要快。原因有俩个,一个是python是解释型语言,运行效率比编译型语言慢很多。另一个就是python并不适合高并发、多线程程序的编写

一般来说我们写的python多线程都是单核多线程,也就是说线程与线程之间都是交替执行的,只是速度很快。这不是意味我们并不能用python写多核并发,实际上python是可以利用c/c++
实现多核并发的。
参考: python的优势和劣势

多线程优化的原理

线程的应用主要是并发和并行。

  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行,避免阻塞和更有效利用资源。典型的例子有:在长时间工作的程序中使用工作线程避免界面失去响应。在网络下载程序中,使用多个线程提高对网络的使用效率,更快下载文件。
  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发可以用在单核和多核处理器上,并行只能在多核处理器上。在我举的计算素数例子里面,如果我的电脑是单核的,多线程计算的速度可能并不会有优化,

对于单处理器系统,操作系统会轮流调度每个线程执行一小段时间,然后切换另一个线程,在切换的时候,保存当前线程使用的寄存器上下文和堆栈,并且在下次调度的时候恢复。这样线程中的程序感觉不到自己被中断过。对于多处理器系统,操作系统会将不同的线程调度给多个处理器,让它们并行执行。
以下为我复习计组总结的,如有偏差还望纠正
我们知道,程序运行在计算机里面实际上是下面这个过程

   --> 程序数据存入主存
   --> CPU从主存中取指令
   --> CPU执行指令之间读取数据 
   --> CPU写回运算结果
   --> 输出设备输出结果
  1. 主存读写速度和cpu运算速度是不一致的,一般来说CPU运算速度会比主存读取速度快很多,而慢速的主存则会将快速的CPU速度降下来。
  2. 一台计算机中最核心的组件是 CPU、内存、以及 I/O 设备,这是冯罗伊曼体系结构决定的。三者速度依次减小。
  3. 程序的任务主要分两种类型,一种是I/O密集型(读写型),一种是CPU密集型(运算型)。注意:这个是相对概念

程序的运行一般都是有顺序的,像一个队列一样。假设我们有两个任务,一个大量运算,一个UI显示,很显然,如果我们在执行任务1的时候,UI会卡住。所以我们的用户体验就会贼差,这时候就是并发的优势,我执行一段计算后再执行UI,两者交替执行,因为这个交替的速度很快,所以在用户看来,是同时执行的。

发布了3 篇原创文章 · 获赞 1 · 访问量 62

猜你喜欢

转载自blog.csdn.net/weixin_43323747/article/details/103218368
今日推荐