16-进程、线程
并发有点像排队打水,并行类似于多个水龙头一起打水
什么是进程process
是系统进行资源分配的基本单位,是操作系统的基础。程序是指令、数据和其组织形式的集合,进程是程序的实体。
一个任务就是一个进程,比如打开记事本
优点:稳定,一个进程崩了不会影响另一个
缺点:创建进程,开销大
在windows下创建进程
通过Process对象创建进程:
from multiprocessing import Process
以下例子可以同时运行两个任务,他们是并行的,并且同时打印各自的进程ID和父进程ID;
在创建进程时可以传递参数,可以是元组或列表
from multiprocessing import Process
from time import sleep
import os
m = 0
def task(s, name):
global m
while True:
sleep(s)
m += 1
print(f"这是{name}!", os.getpid(), os.getppid(), f"m={m}")
def task2(s, name):
global m
while True:
sleep(s)
m += 1
print(f"这是{name}!", os.getpid(), os.getppid(), f"m={m}")
if __name__ == '__main__':
p = Process(target=task, name="任务1", args=(1, "任务1"))
p.start()
p2 = Process(target=task2, name="任务2", args=(2, "任务2"))
p2.start()
while True:
sleep(3)
m += 1
print(f"main--m={m}")
注意:这个例子里的全局变量m,在三个进程中的值都不一样。也就是说,即使是全局变量,在各个进程中也是独立的。这就可以用来在下载中实现多任务同时下载。
自定义进程
在函数中,无法直接调用进程的方法,所以可以自定义一个进程类
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self, name):
super(MyProcess, self).__init__()
self.name = name
# 重写run方法
def run(self):
n = 1
while True:
time.sleep(1)
print(f"进程名字:{self.name},n的值是{n}")
n += 1
if n == 5:
break
if __name__ == '__main__':
p = MyProcess("Alice")
p.start()
p2 = MyProcess(name="Bob")
p2.start()
进程池
当进程比较多时,可以用Pool方法创建进程池对象,并指定最大进程数。
分为两种:
非阻塞式:
- 此模式下,指定最大进程后,所有任务添加到队列,立刻返回,不等待其他进程结束。
- 每个进程中的回调函数等待任务完成后运行
- 超出最大进程数的任务需要等队列前面的任务完成后才会进行,并使用之前的进程。
- 因此,该模式的优点是:进程可以复用。
import os
from multiprocessing import Pool
import time
import random
def task1(task_name):
print("开始做任务:", task_name)
start = time.time()
time.sleep(random.random()*2)
end = time.time()
return f"{task_name}用时:{end - start},进程ID是:{os.getpid()}"
task_repo = []
def callback_task1(n):
task_repo.append(n)
if __name__ == '__main__':
pool = Pool(5)
tasks = ["吃饭", "洗衣服", "打游戏", "散步", "逛超市", "洗碗"]
for task in tasks:
pool.apply_async(task1, args=(task,), callback=callback_task1)
# 非阻塞模式、异步
pool.close() # 添加任务结束
pool.join() # 阻止主进程结束
for repo in task_repo:
print(repo)
print("已结束")
阻塞式
添加一个进程执行一个任务,等待当前进程的任务完成后,才会进入下一个进程运行下一个任务。
进程可以复用,在例子中的体现是:第六个任务,用了第一个进程
进程间通信Queue
from multiprocessing import Queue
q = Queue(5)
strs = "ABCDE"
for i in strs:
q.put(i)
print(q.qsize())
if q.full():
print("队列已满")
else:
q.put("F", timeout=3)
# 如果队列满了,put方法会在这里等待
# timeout参数表示等待的时间,默认是none
# 获取队列的值
while not q.empty():
print(f"当前取出的值是:{q.get()},队列还剩下{q.qsize()}个值")
用queue在两个进程间通信的例子
from multiprocessing import Process, Queue
import time
import traceback
def download_file(q):
images = ["1.jpg", "2.jpg", "3.jpg"]
for image in images:
print(f"正在下载{image}")
time.sleep(0.5)
q.put(image)
def get_file(q: Queue):
while True:
try:
file = q.get(timeout=3)
print(f"{file}保存成功")
except Exception:
print(traceback.format_exc())
print("保存完毕")
break
# 写个main函数的好处就是各个函数的作用域里可以用重名的变量了,否则q会有波浪线
def main():
q = Queue(5)
p1 = Process(target=download_file, args=(q,))
p2 = Process(target=get_file, args=(q,))
p1.start()
p2.start()
if __name__ == '__main__':
main()
线程
线程是进程中的一个实体,是程序执行流的最小单元。线程的消耗远小于进程,所以一般用多线程来实现并发处理。
线程的状态:创建-(就绪-运行-阻塞)-结束。阻塞后回到就绪状态。
两个线程执行下载和听音乐的例子:
import threading
import time
def download_file(n):
images = ["1.jpg", "2.jpg", "3.jpg"]
for image in images:
print(f"正在下载{image}")
time.sleep(n)
print(f"下载{image}成功。")
def listen_music():
musics = ["song1", "song2", "song3", "song4"]
for music in musics:
time.sleep(0.5)
print(f"正在听{music}")
if __name__ == '__main__':
# 创建线程对象
t = threading.Thread(target=download_file, name="下载", args=(1,))
t.start()
t2 = threading.Thread(target=listen_music, name="听音乐")
t2.start()
线程中可以共享全局变量
以下这个例子,可以看到两个线程都访问了全局变量money,最终结果是800。
import threading
money = 1000
def get_money():
global money
for i in range(100):
money -= 1
def get_money2():
global money
for i in range(100):
money -= 1
if __name__ == '__main__':
# 创建线程对象
t = threading.Thread(target=get_money, name="t1")
t.start()
t2 = threading.Thread(target=get_money2, name="t2")
t2.start()
t.join()
t2.join()
print(money)
线程锁GIL
为了保证数据安全性,在线程运行时,会加上线程锁。当前线程未完成不允许其他线程访问cpu。
这导致了Python不能实现真正的多线程,速度比较慢。
以下例子中:
import threading
num = 0
def task1():
global num
for i in range(1000000):
num += 1
print(f"task1当前n的值是{num}")
def task2():
global num
for i in range(1000000):
num -= 1
print(f"task2当前n的值是{num}")
if __name__ == '__main__':
th1 = threading.Thread(target=task1)
th2 = threading.Thread(target=task2)
th1.start()
th2.start()
th1.join()
th2.join()
print(f"最终num的值是{num}")
打印的值是
task1当前n的值是451444
task2当前n的值是0
最终num的值是0
线程和进程的适用场景
线程:耗时操作,比如爬虫、读写IO操作
进程:计算密集型
多线程同步
为了避免数据不安全的问题,我们也可以用lock对象手动加上线程锁
import threading
import time
lock = threading.Lock()
list1 = [0] * 10
def task1():
# 获取线程锁,如果已经上锁,则等待锁的释放
lock.acquire()
for i in range(len(list1)):
list1[i] = 1
time.sleep(0.5)
lock.release() # 释放锁
def task2():
lock.acquire()
for i in range(len(list1)):
print(f"当前list[{i}]的值是{list1[i]}")
time.sleep(0.5)
lock.release()
if __name__ == '__main__':
th1 = threading.Thread(target=task1)
th2 = threading.Thread(target=task2)
th2.start()
th1.start()
th1.join()
th2.join()
print(list1)
死锁
可以理解为两个人,一个拿着叉子等蛋糕,一个拿着蛋糕等叉子。就陷入了死锁。
以下这个例子会产生死锁,解决方式是加上timeout参数
from threading import Thread,Lock
import time
lock1 = Lock()
lock2 = Lock()
class MyThread(Thread):
def run(self):
if lock1.acquire():
print(f"{self.name}获取了lock1")
time.sleep(0.1)
if lock2.acquire(timeout=5):
# 这里如果去掉timeout参数,就会死锁
# 因为永远都等不到另外一把锁释放
print(f"{self.name}获得了lock2,同时有1 和 2")
lock2.release()
lock1.release()
class MyThread2(Thread):
def run(self):
if lock2.acquire():
print(f"{self.name}获取了lock2")
time.sleep(0.1)
if lock1.acquire():
print(f"{self.name}获得了lock1,同时有1 和 2")
lock1.release()
lock2.release()
if __name__ == '__main__':
t1 = MyThread()
t2 = MyThread2()
t1.start()
t2.start()
线程之间的通信:生产者和消费者
import threading
import queue
import random
import time
def produce(q: queue.Queue):
i = 0
while i < 10:
num = random.randrange(1, 100)
q.put(num)
print(f"生产者生产数据:{num}")
time.sleep(1)
i += 1
q.put(None)
# 完成任务
q.task_done()
def consume(q: queue.Queue):
while True:
item = q.get()
if item is None:
break
print(f"消费者获取到:{item}")
time.sleep(3)
q.task_done()
def main():
q = queue.Queue(10)
# 创建生产者
th = threading.Thread(target=produce, args=(q,))
th.start()
th1 = threading.Thread(target=consume, args=(q,))
th1.start()
if __name__ == '__main__':
main()
协程
进程中可能有多个线程,一个线程中可能有多个协程
耗时操作,比如和网络请求相关的操作,会用到协程
yield的参考:https://blog.csdn.net/mieleizhi0522/article/details/82142856
用yield实现协程交替运行
import time
def task1():
for i in range(3):
print(f"A{i}")
yield
time.sleep(0.2)
def task2():
for i in range(3):
print(f"B{i}")
yield
time.sleep(0.2)
if __name__ == '__main__':
g1 = task1()
g2 = task2()
while True:
try:
next(g1)
next(g2)
except Exception as e:
print(e)
break
用greenlet包实现
import time
from greenlet import greenlet
def task_a():
for i in range(5):
print(f"A{i}")
gb.switch()
time.sleep(0.1)
def task_b():
for i in range(5):
print(f"B{i}")
gc.switch()
time.sleep(0.1)
def task_c():
for i in range(5):
print(f"C{i}")
ga.switch()
time.sleep(0.1)
if __name__ == '__main__':
ga = greenlet(task_a)
gb = greenlet(task_b)
gc = greenlet(task_c)
ga.switch()
用gevent&猴子补丁实现
greenlet已经实现了切换,但是不够智能。所以还有一个可以自动切换的包—>gevent
import time
import gevent
from gevent import monkey
monkey.patch_all()
# 打了猴子补丁后就可以自动切换啦
def task_a():
for i in range(5):
print(f"A{i}")
time.sleep(0.1)
def task_b():
for i in range(5):
print(f"B{i}")
time.sleep(0.1)
def task_c():
for i in range(5):
print(f"C{i}")
time.sleep(0.1)
if __name__ == '__main__':
g1 = gevent.spawn(task_a)
g2 = gevent.spawn(task_b)
g3 = gevent.spawn(task_c)
g1.join()
g2.join()
g3.join()
读取网页的例子:
以下三个网站是同时读取的
import gevent
from gevent import monkey
import urllib.request
monkey.patch_all()
def desk_down(url):
res = urllib.request.urlopen(url)
content = res.read()
print(f"下载了{url}的数据,长度{len(content)}")
if __name__ == '__main__':
urls = ["https://www.163.com/", "https://www.qq.com/", "https://www.baidu.com/"]
g1 = gevent.spawn(desk_down, urls[0])
g2 = gevent.spawn(desk_down, urls[1])
g3 = gevent.spawn(desk_down, urls[2])
g1.join()
g2.join()
g3.join()