Python进程与线程 DAY 01 进程&线程
一 进程
一 多任务的引入
我们很多场景事情是同时进行的 最简单的一个比方 看电影的时候我们需要声音和视频同时进行 如果我们需要实现的话我们就得引入多任务的概念
系统默认的执行方式是: 代码从上至下执行 执行完一个语句再执行下一个 比如
import time
def run1():
for i in range(5):
print(f'run1 - {i}')
time.sleep(1)
def run2():
for i in range(5):
print(f'run2 - {i}')
time.sleep(1)
run1()
run2()
#运行结果如下
'''
run1 - 0
run1 - 1
run1 - 2
run1 - 3
run1 - 4
run2 - 0
run2 - 1
run2 - 2
run2 - 3
run2 - 4
'''
二 多任务的概念
现在目前多核心cpu已经普及 但是如果我们需要运行几十个文件 几十项任务 在电脑中也十分常见 但是核心只有四个 这个资源是如何分配的呢
答案就是操作系统轮流让各个任务交替进行 打个比方 任务一执行0.01秒之后 切换到任务二执行0.01秒 再切换到任务三执行0.01秒 表面上看每个任务都是交替进行的 但是由于cpu的执行速度实在是太快了 所以就会感觉每一个任务都再同时进行一样
三 进程
- 进程的介绍
再python程序中 想要实现多任务可以使用进程来完成 进程是实现多任务的一种方式 - 进程的概念
一个正在运行的程序或者软件就是一个进程 它是操作系统进行资源分配的基本单位
一个程序运行后至少有一个进程 一个进程默认有一个线程 进程里面可以创造多个线程 线程是衣服再进程里面的 没有进程就没有线程 - 进程的作用
多个进程可以完成多任务 每个进程就好比一家独立的公司 每个公司都各自再运营 每个进程也各自在运行 执行各自的任务
tips:- 进程是操作系统进行资源分配的基本单位
- 进程是python程序中实现多任务的一种方式
四 多进程的使用方法
- 导入进程包
import multiprocessing
(或) from multiprocessing import Process
Process([group[,target[,name[,args[,kwargs]]]]])
1. group : 指定进程组 目前只能使用None
2. target : 执行的目标任务名
3. name : 进程名字
4. args : 以元组方式给执行任务传参
5. kwargs : 以字典方式给执行任务
Process创建的实例对象的常用方法
1. start() : 启动子进程示例(创建子进程)
2. join() : 等待进程执行结束
Process创建的示例对象的常用属性
name : 当前进程的别名 默认为Process-N N为从1开始递增的整数
def run1():
for i in range(5):
print(f'run1 - {i}')
time.sleep(1)
def run2():
for i in range(5):
print(f'run2 - {i}')
time.sleep(1)
# 创建进程
if __name__ == '__main__':
from multiprocessing import Process
# 通过process创建进程
p1 = Process(target = run1)
p2 = Process(target = run2)
p1.start()
p2.start()
# 结果
'''
run1 - 0
run2 - 0
run2 - 1
run1 - 1
run1 - 2
run2 - 2
run1 - 3
run2 - 3
run2 - 4
run1 - 4
'''
- 多进程的join方法
join方法 : 必须等待第一个进程结束之后 才能进行下一个进程
注意 : 必须得先创建该进程 之后再使用join方法 不然会报错
join方法可以理解为 当遇到join这个语句的时候 我会停下来执行完所有join对应的这个进程 然后才会继续向下执行下面的进程
def run1():
for i in range(5):
print(f'run1 - {i}')
time.sleep(1)
def run2():
for i in range(5):
print(f'run2 - {i}')
time.sleep(1)
# 3. 创建进程
if __name__ == '__main__':
from multiprocessing import Process
# 3. 通过process创建进程
p1 = Process(target = run1)
p2 = Process(target = run2)
# 4. 执行进程
p1.start()
p1.join() # join方法 阻塞 : 必须等待一个进程结束之后 才能进行下一个进程
p2.start()
# 执行结果
'''
run1 - 0
run1 - 1
run1 - 2
run1 - 3
run1 - 4
run2 - 0
run2 - 1
run2 - 2
run2 - 3
run2 - 4
'''
五 获取进程编号
- 获取进程编号的目的
获取进程编号的目的是验证主进程和子进程的关系 可以得知子进程是由那个主进程创建出来的 - 获取进程编号的两种操作
- 获取当前进程的编号 os.getpid()
- 获取当前父进程的编号 os.getppid()
- 获取进程可以查看父子进程的关系
import os
import time
# 1. 导入多进程的包
# 2. 定义两个函数 用多进程的方法来处理
def run1():
# 获取子进程的进程编号
print('run1--',os.getpid())
time.sleep(1)
# 获取主进程的进程编号
print(os.getppid())
def run2():
# 获取子进程的进程编号
print('run2--',os.getpid())
time.sleep(2)
# 获取主进程的进程编号
print(os.getppid())
# 3. 创建进程
if __name__ == '__main__':
from multiprocessing import Process
# 3. 通过process创建进程
p1 = Process(target = run1)
p2 = Process(target = run2)
p1.start()
p2.start()
# 运行结果
'''
run1-- 16924
run2-- 4144
19440
19440
'''
六 进程中执行带有参数的任务
- 进程中执行带有参数任务的介绍
前面我们使用进程执行的任务是没有参数的 加入我们使用进程执行的带有参数 如何处理
Process类执行的任务并给任务传参数有两种方法
1. args 表示以元组的方式给执行任务传参
2. kwargs 表示以字典方式给执行任务传参
给进程传递非关键字参数 需要定义args = (定义的可以为元组 也可以为列表) 并且一定要和函数参数的顺序保持一致
给进程传递关键字参数 需要定义kwargs = (定义的为一个字典)
def run1(name,age = 0):
print('run1--',name,age )
time.sleep(1)
def run2(name,age = 0):
# 获取子进程的进程编号
print('run2--',name,age)
time.sleep(2)
# 3. 创建进程
if __name__ == '__main__':
from multiprocessing import Process
p1 = Process(target = run1,args=('a',),kwargs={'age' : 1})
p2 = Process(target = run2,args=('b',))
p1.start()
p2.start()
# 运行结果
'''
run1-- a 1
run2-- b 0
'''
七 守护进程
- 设置守护进程的目的是 主进程退出子进程销毁 不让主进程等待子进程去执行
打个比方 如果我们正在浏览网页 网页本省就是主进程 网页里面所有部分就是子进程 如果我们关掉了网页 那么网页内所有的进程不管有没有执行完毕 应该都关闭了 这就是守护进程的作用 - 设置 守护主进程的方式
设置的地点为执行子进程之前 (即 子进程名字.start 之前)
子进程对象.daemon = True
举例子:
import time
def run1():
for i in range(5):
print(f'run1 - {i}')
time.sleep(1)
def run2():
for i in range(5):
print(f'run2 - {i}')
time.sleep(1)
# 3. 创建进程
if __name__ == '__main__':
from multiprocessing import Process
# 3. 通过process创建进程
p1 = Process(target = run1)
p2 = Process(target = run2)
# 把进程设置为守护进程
p1.daemon = True
p2.daemon = True
# 4. 执行进程
p1.start()
p2.start()
p1.join()
p2.join()
print('main')
# 运行结果
'''
run1 - 0
run2 - 0
run1 - 1
run2 - 1
run1 - 2
run2 - 2
run2 - 3
run1 - 3
run2 - 4
run1 - 4
main
'''
八 进程数据共享的三种方法
- 进程之间的数据不共享
例子:
lst = []
# 由于list是一个可变类型 所以def里面不需要使用global
def run1():
lst.append('run1')
def run2():
lst.append('run2')
# 3. 创建进程
if __name__ == '__main__':
from multiprocessing import Process
# 3. 通过process创建进程
p1 = Process(target = run1)
p2 = Process(target = run2)
# 4. 执行进程
p1.start()
p2.start()
p1.join()
p2.join()
print(lst) # 运行结果 []
- 进程键的数据共享的三种常用方式
- 管理器 manager
- 队列 queue
- 管道 pipe
- 管理器 manager
这里需要特别注意几个点 首先再创建进程之前需要开启管理器manager 使用manager.list() 或者manager.dict() 定义共享的列表以及字典 并且把这个参数传给函数中
lst = []
def run1(lst):
lst.append('run1')
def run2(lst):
lst.append('run2')
# 3. 创建进程
if __name__ == '__main__':
from multiprocessing import Manager
from multiprocessing import Process
with Manager() as manager:
lst = manager.list() # 这个列表是共享的列表
# dic = manager.dict() # 这个字典是共享的字典
# from multiprocessing import Process
# 3. 通过process创建进程
p1 = Process(target = run1,args=(lst,))
p2 = Process(target = run2,args=(lst,))
# 4. 执行进程
p1.start()
p2.start()
p1.join()
p2.join()
print(lst) # ['run1', 'run2']
- 队列 queue
注意 这里最巧妙的地方就是使用join方法 让两个进程相互交替进行 这样一个函数中使用put的部分就可以在第二个进程中打印 这样就实现了进程之间的切换
from multiprocessing import Process, Queue
def run1(q):
q.put('run1')
# time.sleep(1)
print('run1中获取:%s ' % q.get())
def run2(q):
print('run2中获取:%s ' % q.get())
q.put('run2')
if __name__ == '__main__':
q = Queue()
p1 = Process(target=run1, args=(q,))
p2 = Process(target=run2, args=(q,))
p1.start()
p2.start()
# 阻塞让两个函数交替进行
p1.join()
p2.join()
# 运行结果
'''
run2中获取:run1
run1中获取:run2
'''
- 管道pipe
管道pipe的使用方法和队列queue十分相似 也是使用join方法在两个函数之间执行进程 来实现数据在进程之间互通
管道pipe和队列queue有两个区别:- 队列的创造的函数对象是使用 q = Queue() 只用使用这个方法的put和get就可以了 但是pipe有几个进程对象就需要创建多少个pipe的对象 传参的部分就必须要使用相应的一个对象
- 队列queue使用的方法是 q.put() 和 q.get()
而pipe使用的方法是 p.send() 和 p.recv()
import time
from multiprocessing import Process, Pipe
def run1(obj1):
obj1.send('run1')
time.sleep(1)
print('run1中获取:%s ' % obj1.recv())
def run2(obj2):
print('run2中获取:%s ' % obj2.recv())
obj2.send('run2')
if __name__ == '__main__':
obj1, obj2 = Pipe()
p1 = Process(target=run1, args=(obj1,))
p2 = Process(target=run2, args=(obj2,))
p1.start()
p2.start()
p1.join()
p2.join()
# 运行结果
'''
run1
run2
'''
九 进程池
- 为什么要采用进程池
当需要创建的子进程数量不多 可以直接利用multiprocessing中的Process动态成多个进程 但如果是上百甚至上千个目标 手动的去创建进程的工作量巨大 而且频繁地创建和销毁进程 会消耗非常大的资源 所以就可以使用到multiprocessing模块提供pool方法 - 设置进程池
初始化pool 可以指定一个最大进程数 当有新的请求提交到pool中时 如果池还没有满 那么就会创建一个新的进程用来执行该请求 但是如果池中的进程数已经达到指定的最大值 那么请求就会等待 知道池中有进程结束 才会创建新的进程来执行
# 1. 导入进程池的包
import os
import time
from multiprocessing import Pool
def run1():
print(os.getpid())
# print('-'*100)
time.sleep(1)
# print('-'*100)
if __name__ == '__main__':
# 2.创建一个进程池
# Pool方法的第一个参数是 初始化 多少个进程
pool = Pool(3)
for i in range(10):
pool.apply_async(run1)
# 3. 必须关闭进程池
pool.close()
# 4. 必须等待所有的请求全部执行完成之后 在结束主进程
pool.join()
十 装饰器传参
装饰器也是可以传参的 之前我们讲到过 装饰器实际上是一个闭包的结构 能称之为装饰器必须要满足三个条件
- 外层函数必须要包含一个内层函数
- 外面的函数的返回值 必须是里面的内层函数
- 外面函数的参数(函数名)必须在内层函数被调用
这里提到一个使用装饰器传参的方法 如果是函数定义的装饰器 那么可以使用在函数外再套一个最外层函数 最外层函数的参数是需要传参的部分 (如果在outer里面进行传参会引起报错 如果在inner里面传参 那么应用的函数都需要有对应的参数 不然也会报错) 具体操作如下
import time
data = [10000000,20000000,30000000]
def go(s):
def outer(fun):
def inner():
print(s)
t_start = time.time()
fun()
t_end = time.time()
print(t_end - t_start)
return inner
return outer
# t_start = time.time()
@go('这样子可以传参')
def cacl():
for i in data:
num = 0
for j in range(i):
num += j
with open(f'{i}.txt','w') as fp:
fp.write(str(num))
cacl()
十一 用类封装一个传参装饰器并调用
使用类来封装一个装饰器实际上和直接用函数定义一个装饰器的效果相同 但是如果我不想在创立对象之后使用对象调用类中的方法(即 对象本身相当于一个类中的函数) 可以使用一个魔术方法__call__
首先第一步还是要有个初始值 初始值并不是需要装饰的函数 装饰器需要传的参数
第二步使用魔术方法__call__
再将闭包结构写下来
class Go:
def __init__(self,m):
self.m = m
def __call__(self, func):
def inner(name, age):
print(self.m)
func(name, age)
print('*------*')
return inner
@Go(123)
def fun(name,age):
print(name)
print(age)
fun('老王',60)
十二 多进程的案例补充
目标是计算 等差数列1~10000000 1~20000000 1~30000000 步长为1 并写到三个不同的文件当中 使用多进程进行操作 并计算这个程序执行的时间
import time
from multiprocessing import Process
def run1():
num = 0
for i in range(10000000):
num +=1
with open('a.txt','w') as fp:
fp.write(str(num))
def run2():
num = 0
for i in range(20000000):
num +=1
with open('b.txt','w') as fp:
fp.write(str(num))
def run3():
num = 0
for i in range(30000000):
num +=1
with open('c.txt','w') as fp:
fp.write(str(num))
if __name__ == '__main__':
t_start = time.time()
p1 = Process(target=run1)
p2 = Process(target=run2)
p3 = Process(target=run3)
p1.start()
p2.start()
p3.start()
p1.join()
p2.join()
p3.join()
t_end = time.time()
print(t_end - t_start)
十三 线程
-
线程的介绍
在python中 想要实现多任务除了使用进程 还可以使用线程来完成 线程是实现多任务的另一种方式 -
线程的概念
线程是进程中执行代码的一个分支 每个执行分支(线程)想要工作执行代码需要cpu进行调度 也就是线程是cpu的基本单位 每个进程至少有一个线程 而这个线程就是我们通常说的主线程
程序启动默认会有一个主线程 程序员自己创建线程为子线程 多线程可以完成更多任务 -
在python中 多线程是伪多线程(需要理解记住)
由于python解释器中存在GIL锁,所以python中的多线程是伪多线程 也就是cpu不能并行执行 只能并发执行GIL锁 : 全局解释锁 , 每次zhineng一个线程获得cpu的使用权 为了线程安全 也就是为了解决多线程之间的数据完整性和状态同步而加的锁 因为我们知道线程之间的数据是共享的
十四 多线程的使用
- 导入线程模块
from threading import Thread
- 线程类Thread参数说明
Thread([group[,target[,name[,args[,kwargs]]]]])- group : 线程组 目前只使用None (这个会在后面的内容中提及)
- target : 执行的目标任务名
- args : 以元组的方式给执行任务传参
- kwargs : 以字典方式给执行任务传参
- name : 线程名 一般不用设置
- 启动线程
启动线程使用start方法 - 多线程完成多任务的代码示例
这里有一个多线程的示例 运行的结果中可以看到多线程使用的痕迹: 在一部分时间内 cpu会运行fun1的部分 然后很短的时间又会运算一下fun2的部分 类似于反复横跳
from threading import Thread
def fun1():
for i in range(10000):
print(f'fun1-{i}')
def fun2():
for i in range(10000):
print(f'fun2-{i}')
if __name__ == '__main__':
t1 = Thread(target=fun1)
t2 = Thread(target=fun2)
t1.start()
t2.start()
t1.join()
t2.join()
- tips:
- 导入线程模块
from threading import Thread - 创建子进程并指定执行的任务
t1 = Thread(target = 任务名) - 启动线程执行任务
t1.start()
- 导入线程模块
十五 线程执行带有参数的任务
- 线程执行带有参数的任务介绍
前面我们使用线程执行的任务是没有参数的 加入我们使用线程执行的任务带有参数 如何给函数进行传参
Thread类 执行任务并给任务传参有两种方式
1. args 表示以元组的方式给执行任务传参
2. kwargs 表示以字典的方式执行任务传参
def run1(name):
time.sleep(1)
print(name)
def run1(name):
time.sleep(1)
print(name)
if __name__ == '__main__':
t1 = Thread(target=run1 ,args=('t1',))
t2 = Thread(target=run1, args=('t2',))
t1.start()
t2.start()
t1.join()
t2.join()
# 运行结果
'''
t1
t2
'''
十六 守护线程
- 什么是守护线程
设置守护线程的目的是主线程退出子线程销毁,不让主线程等待子线程去执行 - 设置守护主线程的方式
子线程对象.setDaemon(True)
import threading
import time
def run1():
print("start thread: %s" % threading.current_thread())
time.sleep(2)
print("end thread : %s "% threading.current_thread())
def run2():
print("start thread: %s" % threading.current_thread())
time.sleep(2)
print("end thread : %s " % threading.current_thread())
if __name__ == "__main__":
t1 = threading.Thread(target=run1, )
t2 = threading.Thread(target=run2, )
t1.setDaemon(True)
t2.setDaemon(True)
t1.start()
t2.start()
print("main thread: %s " % threading.current_thread())
# 运行结果
'''
start thread: <Thread(Thread-1, started daemon 8412)>
start thread: <Thread(Thread-2, started daemon 6788)>
main thread: <_MainThread(MainThread, started 19208)>
'''
十七 线程数据共享
- 线程之间的数据共享
首先 线程和进程不一样 进程不做处理 进程相互之间的数据是不共享的 但是线程不一样 线程的数据是共享的 接下来的示例可以证明
from threading import Thread
lst = []
def run1():
lst.append('run1')
def run2():
lst.append('run2')
if __name__ == '__main__':
t1 = Thread(target=run1)
t2 = Thread(target=run2)
t1.start()
t2.start()
t1.join()
t2.join()
print(lst) # ['run1', 'run2']
- 多线程数据共享会带来数据的错乱
a+=1 在计算机面前实际上是两个步骤
a1 = a + 1
a = a1
所以计算机在做运算的时候(特别是数字比较大的时候) 由于操作系统会控制在两个线程之间反复横跳的时间 很有可能会出现一个情况 : 在 (a1 = a + 1 ; a = a1) 这两个步骤之间 切换到另外一个进程中进行计算 那么数据就会出错
from threading import Thread
a = 0
def run1():
global a
for i in range(1000000):
a +=1
def run2():
global a
for i in range(1000000):
a += 1
if __name__ == '__main__':
t1 = Thread(target=run1)
t2 = Thread(target=run2)
t1.start()
t2.start()
t1.join()
t2.join()
print(a) #1502603
- 解决多线程的数据错乱(lock方法)
原理就是 将(a1 = a + 1 ; a = a1) 这个步骤锁在一起成为一个步骤 只有执行完整个步骤才能进行切换 这样不会导致数据错乱 (这里只是一个简单的逻辑 如果逻辑更多 也可以使用这种方法 将需要一起计算的部分上锁)
具体操作如下:
from threading import Thread,Lock
import time
lock = Lock()
a = 0
def run1():
global a
for i in range(10000000):
lock.acquire()
a +=1
lock.release()
def run2():
global a
for i in range(10000000):
lock.acquire()
a += 1
lock.release()
if __name__ == '__main__':
t_start = time.time()
t1 = Thread(target=run1)
t2 = Thread(target=run2)
t1.start()
t2.start()
t1.join()
t2.join()
t_end = time.time()
print(t_end-t_start)
print(a) #20000000