Python多任务编程笔记(4)
一 、多进程
多任务介绍
多任务是指在同一时间内执行多个任务,例如:现在电脑安装的操作系统都是多任务操作系统,可以同时运行多个软件
多任务的执行
- 并行
- 并发
并发:在一段时间内交替去执行任务
并行:对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的软件,多个内核是真正的一起执行软件。这里需要注意多核cpu是并行的执行多任务,始终有多个软件一起执行
小结
- 使用多任务就能充分利用CPU资源,提高程序的执行效率,让程序具备处理多个任务的能力
- 多任务执行有两种方式:并发和并行,这里并行才是多个真正意义一起执行。
进程
进程的介绍
在Python程序中,想要实现多任务可以使用进程来完成,进程是实现多任务的一种方式
进程的概念
一个正在运行的程序或者软件就是一个进程,他是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行
比如:现实生活中公司可以理解成一个进程,公司提供办公资源(电脑、办公椅等)真正干活的是员工,员工可以理解成线程
注意:
一个程序运行后至少有一个进程,一个进程默认有一个线程,进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程
进程的作用
多进程可以完成多任务,每个进程就好比一家独立的公司,每个公司都在各自运营,每个进程也各自在运行,执行各自的任务
小结
- 进程是操作系统进行资源分配的基本单位
- 进程是Python程序中实现多任务的一种方式
多进程的使用
导入进程包
import multiprocessing
Process进程类的说明
Process([group [, target [, name [, args [,kwargs]]]]])
- group:指定进程组,目前只能使用None
- target:执行的目标任务名
- name:进程名字
- args:以元组的方式给执行任务传参
- kwargs:以字典方式为执行任务传参
Process创建的实例对象的常用方法
- start():启动子进程实例(创建子进程)
- join():等待子进程执行结束
- terminate():不管任务是否完成,立即终止子进程
Process创建的实例对象的常用属性
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
多进程完成多任务的代码
# 1.调用进程包
import multiprocessing
import time
# 跳舞任务
def dance():
for i in range(3):
print("跳舞中...")
time.sleep(0.1)
# 唱歌任务
def sing():
for i in range(3):
print("唱歌中...")
time.sleep(0.3)
# 2.导入子进程
dance_process = multiprocessing.Process(target=dance)
sing_process = multiprocessing.Process(target=sing)
# 3.启动进程执行对应的任务
dance_process.start()
sing_process.start()
获取进程编号
获取进程编号的目的
获取进程编号的目的是验证主进程和子进程的关系,可以得知子进程是由哪个主进程创建出来的。
获取进程编号的两种操作:
- 获取当前进程编号
- 获取当前父类进程编号
获取当前进程编号
os.getpid()
# 1.调用进程包
import multiprocessing
import time
# 跳舞任务
def dance():
for i in range(3):
print("跳舞中...")
time.sleep(0.1)
# 唱歌任务
def sing():
for i in range(3):
print("唱歌中...")
time.sleep(0.3)
# 2.导入子进程
dance_process = multiprocessing.Process(target=dance)
sing_process = multiprocessing.Process(target=sing)
# 3.启动进程执行对应的任务
dance_process.start()
sing_process.start()
import multiprocessing
import time
import os
# 跳舞任务
def dance():
# 获取当前进程的编号
print("dance:", os.getpid())
# 获取当前进程
print("dance:", multiprocessing.current_process())
for i in range(5):
print("跳舞中...")
time.sleep(0.2)
# 扩展:根据进程编号杀死指定进程
os.kill(os.getpid(), 9)
# 唱歌任务
def sing():
# 获取当前进程的编号
print("sing:", os.getpid())
# 获取当前进程
print("sing:", multiprocessing.current_process())
for i in range(5):
print("唱歌中...")
time.sleep(0.2)
if __name__ == '__main__':
# 获取当前进程的编号
print("main:", os.getpid())
# 获取当前进程
print("main:", multiprocessing.current_process())
# 创建跳舞的子进程
# group: 表示进程组,目前只能使用None
# target: 表示执行的目标任务名(函数名、方法名)
# name: 进程名称, 默认是Process-1, .....
dance_process = multiprocessing.Process(target=dance, name="myprocess1")
sing_process = multiprocessing.Process(target=sing)
# 启动子进程执行对应的任务
dance_process.start()
sing_process.start()
获取当前父进程编号
os.getppid()
表示获取当前父进程编号
import multiprocessing
import time
import os
# 跳舞任务
def dance():
# 获取当前进程的编号
print("dance:", os.getpid())
# 获取当前进程
print("dance:", multiprocessing.current_process())
# 获取父进程的编号
print("dance的父进程编号:", os.getppid())
for i in range(5):
print("跳舞中...")
time.sleep(0.2)
# 扩展:根据进程编号杀死指定进程
os.kill(os.getpid(), 9)
# 唱歌任务
def sing():
# 获取当前进程的编号
print("sing:", os.getpid())
# 获取当前进程
print("sing:", multiprocessing.current_process())
# 获取父进程的编号
print("sing的父进程编号:", os.getppid())
for i in range(5):
print("唱歌中...")
time.sleep(0.2)
if __name__ == '__main__':
# 获取当前进程的编号
print("main:", os.getpid())
# 获取当前进程
print("main:", multiprocessing.current_process())
# 创建跳舞的子进程
# group: 表示进程组,目前只能使用None
# target: 表示执行的目标任务名(函数名、方法名)
# name: 进程名称, 默认是Process-1, .....
dance_process = multiprocessing.Process(target=dance, name="myprocess1")
sing_process = multiprocessing.Process(target=sing)
# 启动子进程执行对应的任务
dance_process.start()
sing_process.start()
# main: 9080
# main: <_MainProcess(MainProcess, started)>
# sing: 2860
# sing: <Process(Process-2, started)>
# dance: 29708
# dance: <Process(myprocess1, started)>
# sing的父进程编号: 9080
# 唱歌中...
# dance的父进程编号: 9080
# 跳舞中...
# 唱歌中...
# 唱歌中...
# 唱歌中...
# 唱歌中...
进程执行带有参数的任务
Process类执行任务并给任务传参数有两种方式:
- args 表示以元组的方式给执行执行任务传参
- kwargs 表示以字典方式给执行任务传参
args 参数的使用
def show_info(name, age):
print(name, age)
if __name__ == '__main__':
# 元组方式传参
# sub_process = multiprocessing.Process(target=show_info, args=("李四", 20))
#字典方式传参
# sub_process = multiprocessing.Process(target=show_info, kwargs={"name": "李四", "age": 20})
# 结合
sub_process = multiprocessing.Process(target=show_info, args=("李四",), kwargs={"age": 20})
sub_process.start()
进程的注意点
创建子进程其实就是对主进程资源进行拷贝,子进程其实就是主进程的一个副本
注意点介绍
- 进程之间不共享全局变量
- 主进程会等待所有的子进程执行结束再结束
进程之间不共享全局变量
#定义全局列表
g_list = list()
# 添加数据的任务
def add_data():
for i in range(3):
g_list.append(i)
print("add:", i)
time.sleep(0.2)
print("添加完成:", g_list)
# 读取数据的任务
def read_data():
print("read:", g_list)
# 提示:对于Linux和mac主进程执行的代码不会进程拷贝,但是对于Windows系统来说主进程执行的代码也会进行拷贝
# 对应Windows来说创建子进程的代码如果进程拷贝执行相当于递归无线制进行创建子进程,会报错
if __name__ == '__main__':
# 添加数据的子进程
add_process = multiprocessing.Process(target=add_data)
# 读取数据的主进程
read_process = multiprocessing.Process(target=read_data)
# 启动进程
add_process.start()
# 当前进程(主进程)等待添加数据的进程执行完成以后代码继续往下执行
add_process.join()
read_process.start()
拓展
if __name__ == '__main__':
的作用
# 对应Windows来说创建子进程的代码如果进程拷贝执行相当于递归无线制进行创建子进程,会报错
# 理解说明:直接执行的模块就是主模块,那么直接执行的模块里面就应该添加判断是否是主模块的代码
# 1. 防止别人导入文件的时候执行main里面的代码
# 2. 防止Windows系统递归创建子进程
二、线程
线程
在Python中,想要实现多任务除了使用进程,还可以使用线程来完成,线程是实现多任务的另一种方式。
概念
线程是进程中执行的一个分支,每个执行分支(线程)要想工作执行代码需要CPU调度,也就是说线程是CPU调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程
线程的作用
多线程可以完成多任务
主进程 ------> 执行代码:一个函数执行完成再去执行另一个函数,他是单任务的方式执行
主进程 --------> 函数1
子进程 --------> 函数2 执行代码:两个函数都可以执行,他是多任务的方式执行
说明:程序启动默认会有一个主线程,程序员自己创建的线程可以称为子线程,多线程可以完成多任务
多线程的使用
导入线程模块
import threading
线程类Thread参数说明
Thread([group[,target[,name[,args[,kwargs]]]]])
- group:线程组,目前只能使用None
- target:执行的目标任务名
- args:以元组的方式给执行任务传参
- kwargs:以字典的方式给执行任务传参
- name:线程名,一般不用设置
启动线程
启动线程使用start方法
多线程完成多任务的代码
def dance():
current_thread = threading.current_thread()
print("dance:", current_thread)
for i in range(3):
print("跳舞....")
time.sleep(0.2)
def sing():
current_thread = threading.current_thread()
print("sing:", current_thread)
for i in range(3):
print("唱歌...")
time.sleep(0.2)
if __name__ == '__main__':
# 获取当前线程
current_thread = threading.current_thread()
print("main:", current_thread)
dance_thread = threading.Thread(target=dance)
sing_thread = threading.Thread(target=sing)
dance_thread.start()
sing_thread.start()
线程执行带有参数的任务
线程执行带有参数任务的介绍
Thread类执行任务并给任务传参有两种方式:
- args 表示以元组的方式给执行任务
- kwargs 表示以字典方式给执行任务传参
def show_info(name, age):
print("name: %s age: %d" % (name, age))
if __name__ == '__main__':
# 创建子线程
# 以元组的方式传参, 要保证元组里面元素的顺序和函数的参数顺序一致
# sub_thread = threading.Thread(target=show_info, args=("李四", 20))
# sub_thread.start()
# 以字典的方式传参, 要保证字典里面的key和函数的参数名保持一致
sub_thread = threading.Thread(target=show_info, kwargs={"name": "李四", "age": 20})
sub_thread.start()
线程的注意点
线程的注意点介绍
- 线程之间执行是无序的
- 主线程会等待所有的子线程执行结束再结束
- 线程之间共享全局变量
- 线程之间共享全局变量数据出现错误问题
# 定义全局变量
g_num = 0
def task1():
for i in range(1000000):
global g_num
g_num = g_num + 1
print("task1", g_num)
def task2():
for i in range(1000000):
global g_num
g_num = g_num + 1
print("task2", g_num)
if __name__ == '__main__':
first_thread = threading.Thread(target=task1)
second_thread = threading.Thread(target=task2)
first_thread.start()
# 线程等待,让第一个线程先执行,然后让第二个线程在执行,保证数据不会有问题
first_thread.join() # 主线程等待第一个子线程执行完成以后代码再继续执行下去
second_thread.start()
全局变量数据错误的解决方法:
线程同步:保证同一时刻只能有一个线程去操作全局变量 同步:就是协同步调,按照预定的先后次序进行运行。
线程同步的方式:
- 线程等待(join)
- 互斥锁
互斥锁
互斥锁:对共享数据进行锁定,保证同一个时刻只能有一个线程去操作
注意:
- 互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完放后,其他等待的线程再去抢这个锁
互斥锁的使用
threading模块中定义Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁
互斥锁使用步骤:
# 创建锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
...这里编写代码能保证同一时刻只能有一个线程去操作,对共享数据进行锁定...
# 释放锁
mutex.release()
注意点:
- acquire和release方法之间的代码同一时刻只能有一个线程去操作
- 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时的acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁
互斥锁案例
# 定义全局变量
g_num = 0
# 创建互斥锁,Lock本质就是一个函数
lock = threading.Lock()
def task1():
# 上锁
lock.acquire()
for i in range(1000000):
global g_num
g_num = g_num + 1
print("task1", g_num)
lock.release()
def task2():
lock.acquire()
for i in range(1000000):
global g_num
g_num = g_num + 1
print("task2", g_num)
lock.release()
if __name__ == '__main__':
first_thread = threading.Thread(target=task1)
second_thread = threading.Thread(target=task2)
first_thread.start()
second_thread.start()
# 互斥锁可以保证同一时刻只有一个线程去执行代码,能够保证全局变量的数据没有问题
# 线程等待和互斥锁都是把多任务改成单任务去执行,保证了数据的准确性,但是执行性能会下降
小结
- 互斥锁可以保证同一时刻只有一个线程去操作共享数据,保证共享数据不会出现错误问题
- 使用互斥锁的好处确保其某阶段关键代码只能由一个线程从头到尾完整的去执行
- 使用互斥锁会影响代码的执行效率,多任务改成单任务执行
- 互斥锁如果没有使用好容易出现死锁的情况
死锁
死锁:一直等待对方释放锁的情景就是死锁
死锁的结果:
- 会造成应用程序的停止响应,不能再处理其他任务了
lock = threading.Lock()
def get_value(index):
lock.acquire()
my_list = [1, 4, 6]
if index >= len(my_list):
print("下标越界", index)
lock.release()
return
value = my_list[index]
print(value)
lock.release()
if __name__ == '__main__':
for i in range(10):
sub_thread = threading.Thread(target=get_value, args=(i, ))
sub_thread.start()
# 1
# 4
# 6
# 下标越界 3
# 下标越界 4
# 下标越界 5
# 下标越界 6
# 下标越界 7
# 下标越界 8
# 下标越界 9
进程和线程的对比
关系对比:
- 线程是依附再进程里面的,没有进程就没有线程
- 一个进程默认提供一条路线,进程可以创建多个线程
区别对比:
- 进程之间不共享全局变量
- 线程之间共享全局变量,但是要注意资源竞争的问题,解决办法:互斥锁或者线程同步
- 创建进程的资源开销要比创建线程的资源开销要大
- 进程是操作系统分配资源的基本单位,线程是CPU调度的基本单位
- 线程不能够独立执行,必须依存在进程中
- 多进程开发比单进程多线程开发稳定性要强
优缺点对比:
-
进程的优缺点:
优点:可以用多核
缺点:资源开销大
-
线程的优缺点:
优点:资源开销小
缺点:不能使用多核
小结
- 进程和线程都是完成多任务的一种方式
- 多进程要比多线程消耗得多,但是多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其他进程
- 多进程可以使用cpu的多核运行,多线程可以共享全局变量
- 线程不能单独执行必须依附在进程里面