我们先来了解一下学习知识点
- 多任务:理解什么是多任务,为什么使用多任务
- 进程:理解什么是进程,怎么使用进程完成多任务
- 线程:理解什么是进程,怎么使用进程完成多任务
- 注意事项:理解进程和线程的联系和区别,以及使用中的注意事项
一、多任务
我们之前写的代码,以函数为例, 都是先执行完一个函数之后,才能执行下一个函数,但实际情况是, 我们需要多个函数同时进行。同一时间段内同时执行多个任务(函数或方法),就是多任务
多任务有两种表现形式:并发和并行
现在我们的计算机CPU都是多核CPU,我们之前写的代码是从上往下单任务执行的,只能使用CPU中的一个核心,使用多任务可以充分利用CPU多个核心的计算能力,提高执行效率
并发:CPU核心数小于任务数,CPU轮流(交叉)执行多个任务
并行*CPU核心数大于等于任务数,每个CPU核心可以单独执行一个任务
二、进程
从代码开始运行起来我们就称之为进程,所有代码执行完毕,进程结束
代码执行需要向操作系统申请内存资源,而进程是操作系统资源分配的最小单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行
2.1多进程的使用:
程序运行起来, 在所有代码执行之前,会先默认创建一个主进程(即内存资源分配的过程),该进程会默认包含一个线程(即主线程),代码的执行实际上是由线程完成的。
使用多进程,其实就是由主进程,创建了多个子进程,每个子进程也都有对应要执行的任务
# 导入进程包
import multiprocessing
# 创建子进程对象并指定执行的任务
sub_process = multiprocessing.Process (target=任务名)
# 启动子进程执行任务
sub_process.start()
import multiprocessing
import time
# 任务1
def task1(count):
for i in range(count):
print("任务1执行中..")
time.sleep(0.2)
# 任务2
def task2(num):
for i in range(num):
print("任务2执行中..")
time.sleep(0.2)
if __name__ == '__main__':
# group: 表示进程组,目前只能使用None
# target: 表示执行的目标任务名(函数名、方法名)
# name: 进程名称, 默认是Process-1, .....
# args: 以元组的方式给任务传入参数
# kwargs: 表示以字典方式传入参数
sub_process1 = multiprocessing.Process(target=task1, args=(5,))
sub_process2 = multiprocessing.Process(target=task2, kwargs={"num": 3})
# 启动子进程执行对应的任务
sub_process1.start()
sub_process2.start()
2.2 获取进程编号
获取进程编号的目的是验证主进程和子进程的关系,可以得知子进程是由那个主进程创建出来的。
获取进程编号的两种操作
- 获取当前进程编号:os.getpid()
- 获取当前父进程编号 os.getppid()
2.3、进程的注意点
- 进程之间不共享全局变量
- 多进行的执行是无序的
- 主进程会等待所有的子进程执行结束再结束
- 设置守护主进程方式: 子进程对象.daemon = True
- 销毁子进程方式: 子进程对象.terminate()
- 进程同步:子进程对象.join()
三、线程
线程是cpu调度的最小单位,是代码的真正执行者,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程
3.1多线程的使用:
import threading
import time
# 任务1
def task1(count):
for i in range(count):
print("任务1执行中..")
time.sleep(0.2)
# 任务2
def task2(num):
for i in range(num):
print("任务2执行中..")
time.sleep(0.2)
if __name__ == '__main__':
# group: 表示线程组,目前只能使用None
# target: 表示执行的目标任务名(函数名、方法名)
# name: 线程名称, 默认是Process-1, .....
# args: 以元组的方式给任务传入参数
# kwargs: 表示以字典方式传入参数
sub_thread1 = threading.Thread(target=task, args=(5,))
sub_thread2 = threading.Thread(target=task, kwargs={"num": 3})
# 启动子线程执行对应的任务
sub_thread1.start()
sub_thread2.start()
3.2、线程的注意点
-
线程之间执行是无序的
-
线程之间共享全局变量
-
线程之间共享全局变量数据容易出现错误问题
-
主线程会等待所有的子线程执行结束再结束
-
设置守护主线程的方式:
-
sub_thread = threading.Thread(target=task, daemon=True)
-
sub_thread.setDaemon(True)
-
-
线程同步:子线程对象.join()
-
3.3、互斥锁
在编程中,为了避免共享数据出现错误,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象
互斥锁的使用
# 创建锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
# 释放锁
mutex.release()
注意点:
- acquire和release方法之间的代码同一时刻只能有一个线程去操作
- 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。
- 互斥锁避免资源竞争
- 互斥锁阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 可能会造成死锁
四、进程和线程比较
1.进程是操作系统资源分配的最小单位,线程是CPU调度的最小单位
2.线程依附于进程,没有进程就没有线程,一个进程默认提供一个线程(主线程),进程可以创建多个线程
3.进程不共享全局变量,同一个进程中的线程共享全局变量(资源竞争–>互斥锁,互斥锁可能产生死锁)
4.进程开销大但可以利用多核(并行),线程开销小但不能利用多核(并发)
1.多任务
概念:在同一时间段内执行多个任务
好处呢就是 充分利用CPU,提高执行效率
表现形式:1.并发 CPU在一段时间内交替执行多个任务.
2.并行 CPU在一段时间内只执行一个任务, 利用多个CPU完成执行多任务的功能
2.进程
概念:代码运行起来我们就称之为进程
是操作系统资源分配的最小单位
进程间不共享全局变量
使用方式:
1导包-import multiprocessing
2创建子进程:无参数 p = multiprocessing.Process(target=目标函数)
元组方式传参:p = multiprocessing.Process(target=目标函数, args=(1,))
字典方式传参:p = multiprocessing.Process(target=目标函数, kwargs={"num": 1})
3.启动子进程:p.start()
获取进程编号 获取当前进程编号:os.getpid()
获取父进程编号:os.getppid()
主进程与子进程的执行顺序:
正常情况下主进程会等待子进程执行结束后再结束
设置守护主进程主进程代码执行完毕, 会关闭该子进程
销毁子进程 子进程对象.terminate()
等待子进程结束 子进程对象.join()
多进程执行是无序的
3.线程
概念:线程是CPU调度的最小单位
线程依附于进程,一个进程默认提供一个线程(主线程),进程可以创建多个线程
一个进程中的所有线程共享全局变量
使用方式:
导包 import threading
创建子线程 无参数:t = threading.Thread(target=目标函数)
元组方式传参:t = threading.Thread(target=目标函数, args=(1,))
字典方式传参:t = threading.Thread(target=目标函数, kwargs={"num": 1})
启动子线程:t.start()
主线程与子线程的执行顺序:
正常情况下主线程会等待子线程执行结束后再结束
设置守护主线程主线程代码执行完毕, 会关闭该子线程 (t = threading.Thread(target=目标函数, daemon=True)) (线程对象.setDaemon(True))
多线程执行是无序的
4.进程线程比较
1.进程是操作系统资源分配的最小单位,线程是CPU调度的最小单位
2.线程依附于进程,没有进程就没有线程,一个进程默认提供一个线程(主线程),进程可以创建多个线程
3.进程不共享全局变量,同一个进程中的线程共享全局变量(资源竞争-->互斥锁,互斥锁可能产生死锁)
4.进程开销大但可以利用多核(并行),线程开销小但不能利用多核(并发)