概念
进程的定义
进程(Process),是程序的一次运行,是系统资源分配和处理器调度的基本单位,是操作系统结构的基础。
进程的特点
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生、动态消亡的;
- 并发性:任何进程都可以同其他进程一起并发执行;
- 独立性:进程是一个能独立运行的基本单位,同时也是系统资源分配和调度的独立单位;
- 异步性:由于进程间存在相互制约,使进程具有执行的间断性,即进程按各自独立、不可预知的速度向前推进。
线程的定义
线程(Thread),“轻量级进程”,是操作系统能够进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每个线程并行执行不同的任务。
线程的特点
- 线程实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。在同一进程中的各个线程,都可以共享该进程所拥有的资源。
- 在引入线程的操作系统中,不仅进程之间可以并发执行,而且同一个进程中的多个线程之间也可以并发执行。
为什么要引入线程
- 在引入线程之前,进程是资源分配和CPU调度的基本单位;然而,进程的创建、撤销和切换存在较大的时空开销,无法满足日益复杂的计算机程序。因此,需要引入“轻量级”进程,提高操作系统的并发性。
- 在引入线程之前,单进程并不能有效地利用多核处理器;但在引入线程以后,同一个进程中的线程可以并行运行在不同的核中。
进程vs线程
| 进程 | 线程 | |
|---|---|---|
| 资源分配 | 进程是资源分配的基本单位。 | 线程基本不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源,例如程序计数器、栈等。 |
| 处理器调度 | 进程是处理器调度的基本单位。 | 线程是处理器调度的最小单位。 |
| 系统开销 | 创建、撤销、切换进程时时空开销大。 | 创建、撤销、切换线程时时空开销小。 |
| 并发性 | 不同进程之间可以并发执行。 | 同一进程中的线程、不同进程中的线程都可以并发执行。 |
当前,在一般的计算程序中,为了满足高并发性,一般在程序中采用多线程编程。
有一个十分通俗的例子,当我们打开word应用时,就会创建一个进程,这个进程需要做很多事情,包括接收键盘的输入、显示键盘的输入等,word进程采用一个个线程来执行这些事情,保证了我们能够在键盘上敲出文字以后,就能马上在屏幕上看到显示。
实现
Java创建线程
在Java中,使用Thread类代表线程,创建线程的常见方式包含三种。
1. 继承Thread类并重写run方法
import java.lang.Thread;
// 1.继承Thread类
class MyThread extends Thread{
// 2.重写run()方法,run()方法里面是线程的实际执行逻辑
public void run(){
for(int i=0;i<10;i++){
System.out.println(i);
}
}
}
public class ThreadExample {
public static void main(String[] args){
// 3.创建线程对象,调用start()方法启动线程
MyThread t1 = new MyThread();
t1.start();
}
}
2. 实现Runnable接口并重写run方法
import java.lang.Runnable;
// 1.实现Runnable接口
class MyRunnable implements Runnable{
// 2.重写run()方法
public void run(){
for(int i=0; i<10;i++){
System.out.println(i);
}
}
}
public class ThreadExample2 {
public static void main(String[] args){
// 3.创建runnable实现类的对象r,并用r作为参数创建Thread类对象t
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
// 4.调用start()方法启动线程
t.start();
}
}
3. 实现Callable接口并重写call方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.io.InterruptedIOException;
import java.util.concurrent.ExecutionException;
// 1.实现Callable接口,重写call方法
// 注:call方法必须有返回值,返回一个对象
class MyCallable implements Callable{
public Integer call() throws Exception{
int sum = 0;
for (int i=0;i<10;i++){
sum += i;
}
return sum;
}
}
public class ThreadExample3 {
public static void main(String[] args){
// 2.创建实现Callable接口的对象
MyCallable c = new MyCallable();
// 3.创建FutureTask对象来接收call()方法中的返回值
FutureTask<Integer> futuret = new FutureTask<Integer>(c);
// 4.创建Thread类的对象,并调用start()方法启动线程
new Thread(futuret).start();
// 5.使用get()方法来获取线程的返回值
try{
System.out.println("线程的返回值:"+futuret.get());
}catch(InterruptedException e){
e.printStackTrace();
}catch(ExecutionException e){
e.printStackTrace();
}
}
}
第一种方法是最为基础的方法,使用第二、三种方法创建的线程可以提交到线程池中,而使用第三种方法创建的线程可以有返回值,并且可以抛出异常。一般常用第二、三种方法来创建线程。
Python创建线程
在Python中,使用threading模块或者线程池来进行线程相关的开发,创建线程的方式主要有如下三种。
1. 使用threading.Thread类的构造器创建线程
from threading import Thread
# 1.定义target函数(一个普通函数即可)作为线程的执行体
# *args可以接收多个以元组形式传入的多个非关键字参数
# **kwargs可以接收字典形式的参数
def process(*args):
for i in args:
print(i)
# 2.定义参数
args = (1,2,3,4,5,6,7,8)
# 3.基于Thread类的构造器,利用创建好的target函数作为参数创建线程t
t = Thread(target=process,args=args,kwargs=None)
# 4.调用start()函数启动线程
t.start()
2. 继承threading.Thread类并重写run()方法
在这种方法中可以自定义一个函数来接收线程的返回值。
import threading
# 1.继承Thread类
class MyThread(threading.Thread):
def __init__(self,args):
super().__init__()
self.args = args
self.sum = 0
# 2.重写run()方法
def run(self):
for i in self.args:
self.sum += i
print(i)
# 可以定义一个函数来接收线程的返回结果
def get_result(self):
return self.sum
if __name__=="__main__":
args = [1,2,3,4,5,6,7,8]
# 3.创建Thread子类的对象
myThread = MyThread(args=args)
# 4.调用start()方法启动线程
myThread.start()
# 此处需要等子线程执行完毕才能执行主线程
myThread.join()
print(myThread.get_result())
3. 使用ThreadPoolExecutor
Python中要想获取线程的返回值,除了常见的在自定义的线程类中写一个函数返回,还可以使用concurrent.futures.ThreadPoolExecutor来创建线程池,然后使用as_completed依次取出线程池中线程执行的方法。
import concurrent.futures
# 1.定义线程的执行函数
def sumFunction(*args):
sum = 0
for num in args[0]:
sum += num
return sum
# 2. 定义参数
args1 = (12,34,56,7,8)
args2 = (2,3,34,45,6)
args3 = (3,4,5,52,3,4)
args = (args1,args2,args3)
# 3.使用concurrent.futures.ThreadPoolExecutor创建线程池
# 注:使用with可以自动关闭线程池
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as thread_pool_excutor:
# 4.使用submit()方法将需要执行的函数提交到线程池中
tasks = [thread_pool_excutor.submit(sumFunction,tup) for tup in args]
# 5.使用as_completed按顺序取出线程的执行结果
for future in concurrent.futures.as_completed(tasks):
ans = future.result()
print(ans)
此外,由于Python存在全局解释器锁,每个线程在执行时都需要先获取全局解释器锁,因此,在Python程序中,不论处理器有多少核,实际上都只有一个线程在运行。因此,在Python中,一般不会采用多线程,而是采用多进程的方式。
总结
- 进程的提出是为了实现程序的并发执行,而线程作为“轻量级进程”的概念提出则是为了进一步提升程序的并发性。
- 在Java和Python中创建线程的方式都主要包括继承Thread类、利用线程池等方法。
- 创建线程的方法说起来非常简单,实际上只有去动手写代码才能体会到其中的很多易错点。
1. Java中创建线程的三种方法对比
| 创建方式 | 创建的线程能否作为线程池中的线程 | 线程能否有返回值 | 能否声明抛出异常 |
|---|---|---|---|
| 继承Thread类,重写run()方法 | × | × | × |
| 实现Runnable接口,重写run()方法 | √ | × | × |
| 实现Callable接口,重写call()方法 | √ | √ | √ |
2. Python中创建线程的三种方法对比
| 创建方式 | 线程能否有返回值 | 能否异步获取线程的返回值 |
|---|---|---|
| 使用threading.Thread类的构造器创建线程 | × | × |
| 继承threading.Thread类并重写run()方法 | √ | × |
| 使用ThreadPoolExecutor,即使用线程池的方式来创建。 | √ | √ |