关于多进程与多线程基础的学习,必要的概念很多,但我觉得不着急去一次性去死记
多进程与多线程的基础使用很简单,几个方法只需上手 运行一下 文中的代码就可以很快熟悉起来
进阶部分篇幅有点长,放在了另一篇文章:
Python之路 34:万字总结:并发与并行、锁(GIL、同步锁、死锁与递归锁)、信号量、线程队列、生消模型、进程(基础使用、进程通信、进程池、回调函数)、协程
一、操作系统的历史
进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的。
并发编程(线程 进程 协程) - Yuan先生 - 博客园 (cnblogs.com)
二、进程与线程的概念
进程:最小的资源单元
线程:最小执行单元
2.1、进程
(1)概念
进程:三个相似的答案
- 本质上就是一段程序的运行过程(进程是一个抽象的概念)
- “进程就是一个程序在一个数据集上的一次动态执行的过程”
- 正在进行的一个过程或者说一个任务。而负责执行任务则是cpu。
进程的组成:
- 程序
- 数据集
- 进程控制块
假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),
而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。
是不是在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让
程序B暂停,然后让程序A继续执行?
当然没问题,但这里有一个关键词:切换
既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资
源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B
分别需要什么资源,怎样去识别程序A和程序B等等,所以就有了一个叫进程的抽象
进程定义:
进程就是一个程序在一个数据集上的一次动态执行过程。
进程一般由程序、数据集、进程控制块三部分组成。
我们编写的程序用来描述进程要完成哪些功能以及如何完成;
数据集则是程序在执行过程中所需要使用的资源;
进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系
统感知进程存在的唯一标志。
举一例说明进程:
想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需
的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),
而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。
现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他
照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这
里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程
拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他
离开时的那一步继续做下去。
(2)进程与程序的区别
程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。
注意:
同一个程序执行两次,那也是两个进程,比如打开腾讯视频,虽然都是同一个软件,但是一个可以播放《生活大爆炸》,一个可以播放《老友记》。
2.2、线程 Thead
(1)概念
线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一件事的缺陷,使得进程内并发成为可能
多线程(即多个控制线程):在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
注意:
1 一个程序至少有一个进程,一个进程至少有一个线程.(进程可以理解成线程的容器)
2 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
3 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和
程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
4 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调
度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程
自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)但是
它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
(2)线程的创建开销小
1、创建进程的开销要远大于线程?
如果我们的软件是一个工厂,该工厂有多条流水线,流水线工作需要电源,电源只有一个即cpu(单核cpu)
一个车间就是一个进程,一个车间至少一条流水线(一个进程至少一个线程)
创建一个进程,就是创建一个车间(申请空间,在该空间内建至少一条流水线)
而建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小
2、进程之间是竞争关系,线程之间是协作关系?
车间直接是竞争/抢电源的关系,竞争(不同的进程直接是竞争关系,是不同的程序员写的程序运行的,迅雷抢占其他进程的网速,360把其他进程当做病毒干死)
一个车间的不同流水线式协同工作的关系(同一个进程的线程之间是合作关系,是同一个程序写的程序内开启动,迅雷内的线程是合作关系,不会自己干自己)
(3)线程与进程的区别
- Threads share the address space of the process that created it; processes have their own address space.
- Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
- Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
- New threads are easily created; new processes require duplication of the parent process.
- Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
- Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
- 线程共享创建它的进程的地址空间;进程有自己的地址空间。
- 线程可以直接访问其进程的数据段;进程有自己的父进程数据段的副本。
3.线程可以直接与进程中的其他线程通信;进程必须使用进程间通信来与同级进程通信。 - 新线程很容易创建;新进程需要复制父进程。
- 线程可以对同一进程的线程进行相当大的控制;进程只能对子进程进行控制。
- 主线程的改变(取消,优先级的改变,等等)可能会影响进程中其他线程的行为;对父进程的更改不会影响子进程。
(4)为什么要用多线程
多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:
- 多线程共享一个进程的地址空间
- 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
- 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
- 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
三、并发与并行
并行处理(Parallel Processing)是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。
并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行
简单来说:
- 并发:指系统具有处理多个任务的能力
- 并行:指系统具有 同时 处理多个任务的能力
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
所以说,并行是并发的子集
无论是并行还是并发,在用户看来都是’同时’运行的,不管是进程还是线程,都只是一个任务而已,真是干活的是cpu,cpu来做这些任务,而一个cpu同一时刻只能执行一个任务
并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,(并行也属于并发)
并行: 并行:同时运行,只有具备多个cpu才能实现并行
单核下,可以利用多道技术,多个核,每个核也都可以利用多道技术(多道技术是针对单核而言的)
有四个核,六个任务,这样同一时间有四个任务被执行,假设分别被分配给了cpu1,cpu2,cpu3,cpu4,
一旦任务1遇到I/O就被迫中断执行,此时任务5就拿到cpu1的时间片去执行,这就是单核下的多道技术
而一旦任务1的I/O结束了,操作系统会重新调用它(需知进程的调度、分配给哪个cpu运行,由操作系统说了算),可能被分配给四个cpu中的任意一个去执行
四、线程
threading模块
官网链接:threading — Thread-based parallelism — Python 3.10.2 documentation
先上总结:
常用的方法:
- t1 = threading.Thread(target=函数名,args=(传入变量(如果只有一个变量就必须在后加上逗号),),name=取一个线程名) # 把一个线程实例化给t1,这个线程负责执行target=你写的函数名
- t1.start() # 执行启动这个线程
- t1.join() # 必须要等t1这个子线程执行完成,即使主进程结束了也不会退出程序
- t1.setDeamon(True) # :当你的主线程执行完毕后,不管子线程有没有执行完成都退出主程序,注意不能和t1.join()一起使用。
- threading.current_thread().name # 打印出线程名
其他方法:
Thread实例对象的方法
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
threading模块提供的一些方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线
程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与
- len(threading.enumerate())有相同的结果。
4.1、线程的调用
开启线程有两种方式
方法一:threading.Thread
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('hello %s ' % name)
if __name__ == '__main__':
t = Thread(target=sayhi, args=('coder',))
t.start()
print('主线程')
运行结果:
主线程
hello coder
进程已结束,退出代码为 0
可以看到程序先输出了 “主线程” ,2s 后输出 “hello coder ”
方法二:通过继承的方式
from threading import Thread
import time
class Sayhi(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
time.sleep(2)
print('hello %s ' % self.name)
if __name__ == '__main__':
t = Sayhi('coder')
t.start()
print('主线程')
运行结果:
主线程
hello coder
进程已结束,退出代码为 0
可以看到程序先输出了 “主线程” ,2s 后输出 “hello coder ”
4.2、join方法
t1.join() # 必须要等t1这个子线程执行完成,即使主进程结束了也不会退出程序
注意一下两个示例
示例一:
import threading
import time
def hi():
print("hi,time1")
time.sleep(3)
print("hi,time2")
def hello():
print("hello.time1")
time.sleep(1)
print("hello.time2")
if __name__ == "__main__":
fun1 = threading.Thread(target=hi, args=()) # 不可写为 args=None
fun2 = threading.Thread(target=hello, args=())
fun1.start()
fun2.start()
fun1.join()
fun2.join()
print("end")
运行结果:
hi,time1
hello.time1
hello.time2
end
hi,time2
进程已结束,退出代码为 0
示例二:
稍稍修改一下
import threading
import time
def hi():
print("hi,time1")
time.sleep(3)
print("hi,time2")
def hello():
print("hello.time1")
time.sleep(1)
print("hello.time2")
if __name__ == "__main__":
fun1 = threading.Thread(target=hi, args=()) # 不可写为 args=None
fun2 = threading.Thread(target=hello, args=())
fun1.start()
fun1.join()
fun2.start()
fun2.join()
print("end")
运行结果:
hi,time1
hi,time2
hello.time1
hello.time2
end
进程已结束,退出代码为 0
4.3、setDaemon方法和继承式调用
t1.setDeamon(True) # 当你的主线程执行完毕后,不管子线程有没有执行完成都退出主程序,注意不能和t1.join()一起使用。
这就是所谓的:守护线程,守护着主线程运行完毕后伴随着被销毁
示例一:
from threading import Thread
import time
def sayhi(name):
print("t1 start\n")
time.sleep(2)
print('hello %s ' % name)
if __name__ == '__main__':
t = Thread(target=sayhi, args=('coder',))
t.setDaemon(True) # 必须在t.start()之前设置
t.start()
print('主线程')
print(t.is_alive())
运行结果:
t1 start
主线程
True
进程已结束,退出代码为 0
示例二:
这个例子比较迷惑人
from threading import Thread
import time
def foo():
print("t1 start")
time.sleep(1)
print("t1 end")
def bar():
print("t2 start")
time.sleep(3)
print("t2 end")
if __name__ == '__main__':
t1 = Thread(target=foo)
t2 = Thread(target=bar)
t1.daemon = True
t1.start()
t2.start()
print("\n主线程\n")
运行结果:
t1 start
t2 start
主线程
t1 end
t2 end
进程已结束,退出代码为 0
大家可能回想为啥设置了守护线程,咋就没作用呢
原因:t1设置了守护线程,t2没设置啊,打印完“主线程”后t2仍在执行,所以t1这个子线程就没有中断
如果你把函数bar()中的sleep(3)改成sleep(0.5),运行结果就变成了:
t1 start
t2 start
主线程
t2 end
进程已结束,退出代码为 0
因为t2不是守护线程,主线程结束了,主线程和t2结束后,不管t1有没有结束,程序到此终止
4.4、其他方法
Thread实例对象的方法
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
threading模块提供的一些方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线
程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与
- len(threading.enumerate())有相同的结果。
五、进程
仅看使用方法,跟线程几乎一样,还是那几个功能
5.1、进程的调用
方法一:
from multiprocessing import Process
import time
def func(name):
time.sleep(1)
print('hello', name, time.ctime())
if __name__ == '__main__':
p_list = []
for i in range(3):
p = Process(target=func, args=('coder',))
p_list.append(p)
p.start()
for i in p_list:
p.join()
print('end')
运行结果:
hello coder Thu Feb 3 14:45:55 2022
hello coder Thu Feb 3 14:45:55 2022
hello coder Thu Feb 3 14:45:55 2022
end
进程已结束,退出代码为 0
方法二:
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self):
super(MyProcess, self).__init__()
# self.name = name
def run(self):
time.sleep(1)
print('hello', self.name, time.ctime())
if __name__ == '__main__':
p_list = []
for i in range(3):
p = MyProcess()
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('end')
运行结果:
hello MyProcess-2 Thu Feb 3 14:48:28 2022
hello MyProcess-1 Thu Feb 3 14:48:28 2022
hello MyProcess-3 Thu Feb 3 14:48:28 2022
end
进程已结束,退出代码为 0
用PID了解父进程子进程
from multiprocessing import Process
import os
import time
def info(title):
print("title:", title)
print('parent process:', os.getppid()) # os.getppid() ==》 父进程的PID
print('process id:', os.getpid()) # os.getpid() ==》 本身的PID
def f(name):
info('function f')
print('hello', name)
if __name__ == '__main__':
info('main process line')
time.sleep(1)
print("------------------")
p = Process(target=info, args=('coder',))
p.start()
p.join()
运行结果:
title: main process line
parent process: 21960
process id: 7088
------------------
title: coder
parent process: 7088
process id: 14012
进程已结束,退出代码为 0
5.2、进程的相关方法
构造方法:
Process([group [, target [, name [, args [, kwargs]]]]])
group: 线程组,目前还没有实现,库引用中提示必须是None;
target: 要执行的方法;
name: 进程名;
args/kwargs: 要传入方法的参数。
实例方法:
is_alive():返回进程是否在运行。
join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
start():进程准备就绪,等待CPU调度
run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
terminate():不管任务是否完成,立即停止工作进程
属性:
daemon:和线程的setDeamon功能一样
name:进程名字。
pid:进程号。