文章目录
文档链接
multiprocessing - 基于进程的并行性 - 文档
python多进程任务拆分之apply_async()和map_async()
在使用 multiprocessing模块 时应遵守某些准则和习惯用法
取决于平台,multiprocessing
支持三种启动流程的方法。
多线程_多进程
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每一个进程都有独立的代码和数据空间,进程间的切换会有较大的开销 | 线程可以看出是轻量级的进程,多个线程共享内存,线程切换的开销小 |
所处环境 | 在操作系统中,同时运行的多个任务 | 在程序中多个顺序流同时执行 |
分配内存 | 系统在运行的时候为每一个进程分配不同的内存区域 | 线程所使用的资源是他所属进程的资源 |
包含关系 | 一个进程内可以拥有多个线程 | 线程是进程的一部分,所有线程有时候称为是轻量级的进程 |
理解并行和并发
-
一个程序至少有一个进程,一个进程至少有一个线程
-
并行:多个CPU核心,不同的程序就分配给不同的CPU来运行。可以让多个程序同时执行。
cpu1 -------------
cpu2 -------------
cpu3 -------------
cpu4 -------------
- 并发:单个CPU核心,在一个时间切片里一次只能运行一个程序,如果需要运行多个程序,则串行执行。
cpu1 1---- 3----
cpu1 2 ---- 4----
1. 多线程
传统的方式
模拟同时写代码和画图
def coding():
for x in range(3):
print('正在写代码%s' % x)
time.sleep(1)
def drawing():
for x in range(3):
print('正在画图%s' % x)
time.sleep(1)
def main():
coding()# 阻塞的 写完代码才可以画图 同步的
drawing()
if __name__ == "__main__":
main()
模拟同时唱歌和跳舞
from time import sleep
def sing():
for i in range(3):
print('正在唱歌...%d'%i)
dance() # 也是阻塞的 唱歌打印完 才会跳舞
sleep(1)
def dance():
for i in range(3):
print('正在跳舞...%d'%i)
sleep(1)
if __name__=='__main__':
sing()
# dance()
1.1 _thread
模块(了解即可用的少)
#导入模块
# _thread实现线程
import _thread
import time
def fun1():
print('开始运行fun1')
time.sleep(4)
print('运行fun1结束')
def fun2():
print('开始运行fun2')
time.sleep(2)
print('运行fun2结束')
if __name__ == '__main__':
print('开始运行')
#创建线程
_thread.start_new_thread(fun1,())
_thread.start_new_thread(fun2,())
time.sleep(7)
1.1.1 _thread
创建线程并传递参数
import _thread
import time
def fun1(thread_name,delay):
print('开始运行fun1,线程的名:',thread_name)
time.sleep(delay)
print('运行fun1结束')
def fun2(thread_name,delay):
print('开始运行fun2,线程的名:',thread_name)
time.sleep(delay)
print('运行fun2结束')
if __name__ == '__main__':
print('开始运行')
#创建线程
_thread.start_new_thread(fun1,('thread-1',3))
_thread.start_new_thread(fun2,('thread-2',3))
time.sleep(7)
1.2 threading
模块(常用需掌握)
例子 1
import threading
import time
# 多线程的方式
def coding():
for x in range(3):
print('正在写代码%s' % x)
time.sleep(1)
def drawing():
for x in range(3):
print('正在画图%s' % x)
time.sleep(1)
def main():
t1 = threading.Thread(target=coding)
t2 = threading.Thread(target=drawing)
t1.start()
t2.start()
if __name__ == "__main__":
main()
运行结果
代码和 画图是同时出现的。
有兴趣的 可以用多线程 写一个 同时唱歌和跳舞的。
其实是差不多的对吧。 只是将函数 作为参数传给 Thread
例子 2
import threading
import time
def fun1(thread_name,delay):
print('线程{}开始执行fun1'.format(thread_name))
time.sleep(delay)
print('线程{}运行fun1结束'.format(thread_name))
def fun2(thread_name,delay):
print('线程{}开始执行fun2'.format(thread_name))
time.sleep(delay)
print('线程{}运行fun2结束'.format(thread_name))
if __name__ == '__main__':
print('开始运行')
#创建线程
t1=threading.Thread(target=fun1,args=('thread-1',2))
t2=threading.Thread(target=fun2,args=('thread-2',3))
#启动线程
t1.start()
t2.start()
t1.join()
t2.join()
print('代码最后一行\n\n\n',threading.enumerate(),'\n\n\n') # 打印多线程序列化
# 等待线程终止。这会阻塞调用线程,
# 调用join方法 :主进程等待调用join的子进程结束 才会结束
# 直到 join() 方法被调用终止——正常或通过未处理的异常终止——或者直到出现可选的超时。
# 说这么多,其实就一句话。
# 有join 阻塞。 最后一句 print 会在 最后打印。
# 没有join 阻塞。
# t1.start 之后 就会打印。
# 不会等到 t1 ,t2 两个线程执行完 再 打印
join
的结果
不加 join
的结果
1.2.1 以类(class
)的方式构建多线程程序
class 类的方式
import threading
import time
# 多线程的方式 类 的方式
class CodingTread(threading.Thread):
def run(self):
for x in range(3):
print('正在写代码%s' % threading.current_thread()) # 显示当前在运行的线程 的对象
time.sleep(1)
class DrawingTread(threading.Thread):
def run(self):
for x in range(3):
print('正在画图%s' % threading.current_thread())
time.sleep(1)
def main():
t1 = CodingTread()
t2 = DrawingTread()
t1.start()
t2.start()
# print(threading.enumerate()) # 打印多线程序列化
if __name__ == "__main__":
main()
1.2.2 继承threading.Thread
实现创建线程
# 重写 run方法 继承
import threading
import time
def fun1(delay):
print('线程{}执行fun1'.format(threading.current_thread().getName()))
time.sleep(delay)
print('线程{}执行fun1结束'.format(threading.current_thread().getName()))
def fun2(delay):
print('线程{}执行fun2'.format(threading.current_thread().getName()))
time.sleep(delay)
print('线程{}执行fun2结束'.format(threading.current_thread().getName()))
#创建一个类MyThread 继承threading.Thread
class MyThread(threading.Thread):
#重新构造方法
def __init__(self,func,name,args):
super().__init__(target=func,name=name,args=args)
# 重写run方法
def run(self):
self._target(*self._args)
if __name__ == '__main__':
print('开始运行')
t1=MyThread(fun1,'thread-1',(2,))
t2=MyThread(fun2,'thread-2',(4,))
t1.start()
t2.start()
t1.join()
t2.join()
1.3 线程共享全局变量
# 在同一个线程内共享全局变量
import time
from threading import Thread
# 定义一个全局变量
num = 10
def test1():
global num
for i in range(3):
num += 1
print('执行test1函数num的值:%d\n' % num)
def test2():
print('执行test2函数num的值:%d \n' % num)
if __name__ == '__main__':
# 创建线程
t1 = Thread(target=test1)
t2 = Thread(target=test2)
t1.start()
t2.start()
t1.join()
t2.join()
可以看到两个函数 打印 的num 是一样的
运行结果
1.3.1 线程共享全局变量存在的问题(加锁)
例子1
无锁
# 线程不安全
# 修改值
# 修改之后值应该是 100000和200000
import time
from threading import Thread
num = 0
def test1():
global num
time.sleep(2)
for i in range(100000):
num += 1
print('执行test1函数num的值:', num)
def test2():
global num
time.sleep(2)
for i in range(100000):
num += 1
print('执行test2函数num的值:', num)
if __name__ == '__main__':
t1 = Thread(target=test1)
t2 = Thread(target=test2)
t1.start()
t2.start()
t1.join()
t2.join()
运行结果
并没有获得预期的 值。
因为交叉修改 num 导致数据混乱。
例子2
如何加锁
import threading
# 共享全局变量的问题
# 多线程都是在同一个进程中运行的,执行顺序是无序的,有可能会造成数据错误。
VALUE = 0
gLock = threading.Lock() # 加锁 定义
def add_value():
global VALUE # 需要修改 全局变量,就要 关键字定义一下
gLock.acquire() # 锁定 VALUE 修改值的地方才加锁
for x in range(10000000):
VALUE += 1
gLock.release() # 释放一下 数据就不会 出错了
print('value:%d' % VALUE)
def main():
for x in range(2):
t1 = threading.Thread(target=add_value())
# t2 = threading.Thread(target=add_value())
t1.start()
# t2.start()
if __name__ == "__main__":
main()
运行结果
得到预期的值
1.3.2 互斥锁的使用 1
在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
谁先抢先 锁上。 那么 其他的 锁 想上锁。 就只能等到这边 对该资源上的锁 释放之后。
那边才可以上锁。所以相当于就被阻塞了。
import time
from threading import Thread, Lock
num = 0
# 创建一个互斥锁
lock = Lock()
def test1():
global num
lock.acquire() #上锁
# 这里上锁之后 就要 等到循环 完之后
# 才会 释放锁。
# 意味着 test2 需要等到 循环完才能 进行上锁,这个时候 test2 的操作就被阻塞了。
for i in range(100000):
num += 1
lock.release() # 释放锁
print('执行test1函数num的值:%d\n' % num)
def test2():
global num
lock.acquire() # 上锁
for i in range(100000):
num += 1
lock.release() # 释放锁
print('执行test2函数num的值:%d \n' % num)
if __name__ == '__main__':
t1 = Thread(target=test1)
t2 = Thread(target=test2)
t1.start()
t2.start()
t1.join()
t2.join()
运行结果
1.3.3 互斥锁的使用 2
一般来说线程不安全是 怕 变量被修改
。
也就是说 其实我们只需要在 变量被修改的时候上锁
。
保证该变量 当前只有 一个线程在 修改他
。
谁先抢先 锁上。 那么 其他的 锁 想上锁。 就只能等到这边 对该资源上的锁 释放之后。
那边才可以上锁。所以相当于就被阻塞了。
# 锁优化后的使用
# 比(上一个代码) 的上锁效率高
import time
from threading import Thread,Lock
num=0
#创建一个互斥锁
lock=Lock()
def test1():
global num
for i in range(100000):
lock.acquire() # 上锁
num+=1
lock.release() #释放锁
print('执行test1函数num的值:',num)
def test2():
global num
for i in range(100000):
lock.acquire() # 上锁
num+=1
lock.release() # 释放锁
print('执行test2函数num的值:',num)
if __name__ == '__main__':
t1=Thread(target=test1)
t2=Thread(target=test2)
t1.start()
t2.start()
t1.join()
t2.join()
运行结果:
最终的结果 是 200000 是正确的
但是 test1 相当于一个中间 结果 。 没有价值了。
1.3.4 死锁
在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。
举个例子:一个线程获取了第一个锁,然后在获取第二个锁的 时候发生阻塞,那么这个线程就可能阻塞其他线程的执行,从而导致整个程序假死。
在线程共享多个资源的时候,
如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁
例子
下方代码执行之后 得到 如下 结果
Thread-6 执行-MyTread 1 ClassOut if
Thread-7 执行-MyTread 2 ClassOut if
后续的In 的内容没有打印。
因为被阻塞了。
from threading import Thread, Lock
import time
# 创建互斥锁
lock_a = Lock()
lock_b = Lock()
class MyThread1(Thread):
def run(self):
if lock_a.acquire(): # 如果 锁 a 锁定成功 则等待和打印
print(self.name, '执行-MyTread 1 ClassOut if')
time.sleep(1)
if lock_b.acquire(): # 如果 锁 b 锁定成功 则等待和打印
print(self.name, '执行-MyTread 1 ClassIn if')
# in 里面的话就永远不会被打印。
# 因为被阻塞了 class 2 在使用这把锁 lock_b
# 这条 lock_b.acquire 就一直在阻塞中等待 lock_b 的释放。
# 但是 class 2 中 又在等我们 我们 class 1 中的 lock_a 释放之后 它 好上锁。
# 因为 class 2 中的 if lock_a.acquire() 也是被阻塞了的。
# 需要class 1 释放 锁 a 。 之后 class 2 才可以使用 lock_a 上锁。
# 这样好了。 你在等我 我在等你 释放。就死锁了。
time.sleep(1)
lock_b.release() # 释放 锁 b
print('lock_a 释放') # print 语句并没有被执行 说明了 锁a 没有释放
lock_a.release() # 释放 锁 a
class MyThread2(Thread):
def run(self):
if lock_b.acquire(): # 如果 锁 b 锁定成功 则等待和打印
print(self.name, '执行-MyTread 2 ClassOut if')
time.sleep(1)
if lock_a.acquire(): # 如果 锁 a 锁定成功 则等待和打印
print(self.name, '执行-MyTread 2 ClassIn if')
time.sleep(1)
lock_a.release() # 释放 锁 a
print('lock_b 释放') # print 语句并没有被执行 说明了 锁b 没有释放
lock_b.release() # 释放 锁 b
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
那么我们可以测试下 在执行完这段代码之后再 次执行 lock_a.acquire
或者 lock_b.acquire
就会看到程序挂起
非常直观的可以看到 是 *
说明程序一直在等待
然后我们手动在程序 中 释放一下 锁 ,执行 lock_a.release()
然后我们发现 程序 In 执行了。 因为们的lock_a
被我们 手动释放了。
所以会 报一个 释放 未锁定的锁的错误。
这就是整个死锁的流程。
所以我们应该避免 一个锁 锁定之后 还没有 释放。 就又执行下次 锁定
。 导致 阻塞。
更应避免双方之间 互相 阻塞。 导致 死锁。
1.3.5 线程同步的应用
# 线程同步
# 协同步调
from threading import Thread,Lock
import time
# 创建3把互斥锁
lock1=Lock()
lock2=Lock()
lock3=Lock()
# 对lock2和lock3上锁
lock2.acquire()
lock3.acquire()
class Task1(Thread):
def run(self):
while True:
if lock1.acquire():
print('...task1...')
time.sleep(1)
#释放lock2的锁
lock2.release()
class Task2(Thread):
def run(self):
while True:
if lock2.acquire():
print('...task2...')
time.sleep(1)
lock3.release()
class Task3(Thread):
def run(self):
while True:
if lock3.acquire():
print('...task3...')
time.sleep(1)
lock1.release()
if __name__ == '__main__':
t1=Task1()
t2=Task2()
t3=Task3()
t1.start()
t2.start()
t3.start()
轮流释放下个要执行的任务的锁。
在释放之前下一个任务是无法执行的
因为在等待上锁。
相等于你要上厕所,厕所有人。 你要等他出来。厕所里 只有这一个坑的情况。。
1.4 局部变量
所以我们现在有了三个方案来处理 线程不安全导致的数据共享
问题。也就是脏数据
的问题
- 一个是加锁
(互斥锁)
- 一个是
Local
局部变量 - 一个是
创造实例
。将变量的值 挂在实例的属性上。
我们知道多线程环境下,每一个线程
均可以使用所属进程
的全局变量
。
如果一个线程对全局变量进行了修改
,将会影响到其他所有的线程对全局变量的计算操作
,从而出现数据混乱
,即为脏数据
。为了避免多个线程同时对变量进行修改
,引入了线程同步机制
,通过互斥锁来控制对全局变量的访问
。所以有时候线程使用局部变量比全局变量好
,因为局部变量只有线程自身可以访问
,同一个进程下的其他线程不可访问
。
但是局部变量也是有问题
,就是在函数调用的时候,传递起来很麻烦
。
每个函数一层一层调用都需要传递 参数
因此 Python 还提供了ThreadLocal
变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据
,这些私有数据对其他线程
也是不可见的。
1.4.1 局部变量测试(参数传递非常麻烦)
# 调用需要一层一层的去 调用 很麻烦
# 因为变量都在本地 所以无法 直接访问。
# 只能 通过参数传递的方式 访问。
# 你要访问 test 中的 参数 b
# 怎么做?
# 其实就是 放到一个中转站上。 然后大家 都从中转站上去 拿数据就好了。
def _split(name):
return name.split('m')
def print_(b):
print(b)
def foo1(b):
print_(b)
def foo2(b):
print_(b)
def test(name):
b=_split(name)
foo1(b)
foo2(b)
test("onempis")
[‘one’, ‘pis’]
[‘one’, ‘pis’]
1.4.2 使用 ThreadLocal
对象
import threading
#创建ThreadLocal对象
local=threading.local()
def process_thread(name):
#将传入的name值绑定到local的name上
local.name=name
process_student()
def process_student():
student_name=local.name
print('线程名:%s 学生姓名:%s'%(threading.current_thread().getName(),student_name))
t1=threading.Thread(target=process_thread,args=('张三',),name='Thread-A')
t2=threading.Thread(target=process_thread,args=('李四',),name='Thread-B')
# 全局变量但是 有自己的 私有属性,不共享相同的值,但是共享相同的变量
# 回顾下 之前写的 类的 类属性 和 实例属性。和这个场景 是否有点相似呢?
# 直接通过 实例访问 修改 类属性。 我们修改之后的值 只属于自己。
# 没修改的时候是 全局共享的。
# 当然这个属性名字 是 所有实例之间 都可访问的。
# 你没有修改那么就是 类定义时候的值。
t1.start()
t2.start()
t1.join()
t2.join()
# ----------------------------
# 当然 这里 的例子 实际上就是 一个 嵌套函数
# 这样写也没啥毛病
import threading
class Local_:
name=None
local = Local_()# 我们自己写的这个 Local_ 没有处理线程安全。之列的一些 问题
# 毕竟你只写了个 pass
# 所以在下面student_name 那里 赋值的时候 我进行 休眠
# 就会导致 一些问题。 线程不安全。 导致 name 都是一个值。 这里只是为了 帮助大家 理解 概念
# 但是 threading 的 local 是 经过处理的 所以是 线程安全的
# 有兴趣的可以去看看 _threading_local.py 这是源码文件名字。
def process_thread(name):
local.name = name
def process_student():
# 将传入的name值绑定到local的name上
from time import sleep
sleep(2)
student_name = local.name
print('线程名:%s 学生姓名:%s' %
(threading.current_thread().getName(), student_name))
process_student()
t1 = threading.Thread(target=process_thread, args=('张三', ), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('李四', ), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
# --------------
# 当然不加锁我们也可以实现 线程安全的
# 但是 这个例子中的处理方法。
# 我们每次使用的时候 都创建了 一个 Local_ 实例 然后再 销毁这个实例。
# 具体情况 还要看你的使用场景。
# 这样也不失为一种处理方式。
# 相当于中转站中 每次 开辟一个 自己的空间
# 有点 超市里面去存包。每次那个密码 放东西。
# 走的时候 取东西。然后这个空间释放出来给其他需要的人使用。
import threading
class Local_:
name = None
def process_thread(name):
local = Local_()
local.name = name
def process_student():
# 将传入的name值绑定到local的name上
from time import sleep
sleep(2)
student_name = local.name
print('线程名:%s 学生姓名:%s' %
(threading.current_thread().getName(), student_name))
process_student()
del local
t1 = threading.Thread(target=process_thread, args=('张三', ), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('李四', ), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
1.5 多线程生产者和消费者模式 Lock
版
本质上 就是
两个线程 对一个全局变量进行操作。
一个对 Money
进行添加。
一个对 Money
进行减去
相当于 一个赚钱 一个花钱。 而 Money 就是银行账户。
import threading
import random
import time
# Lock 版本 生产者 和消费者模式(多线程)
# 可以让代码可读性更高
gMoney = 1000 # 初始1000元
gLock = threading.Lock() # 制造一把
gTotalTimes = 10 # 总的生产次数
gTimes = 0 # 已经生产的次数
class Producter(threading.Thread): # 生产者类
def run(self):
global gMoney
global gTimes
while True: # 死循环 保证他一直 生产金钱
money = random.randint(100, 1000)
gLock.acquire() # 加锁操作
gMoney += money
print('%s生产了%d元钱,剩余%d' %
(threading.current_thread(), money, gMoney))
if gTimes >= 10:
gLock.release()
break
gTimes += 1
gLock.release() # 解锁释放
time.sleep(0.5) # 沉睡0.5秒
class Consumer(threading.Thread): # 消费者类
def run(self):
global gMoney
while True:
money = random.randint(100, 1000)
gLock.acquire() # 加锁
if gMoney >= money:
gMoney -= money
print('%s消费者消费了%d元钱,剩余%d元钱' %
(threading.current_thread(), money, gMoney))
else:
if gTimes >= gTotalTimes: # 如果已经生产的次数 大于 我们 规定的 生产次数 执行以下代码
gLock.release() # 解锁 释放
break # 结束
print('%s消费者准备消费%d元钱,剩余%d元钱,余额不足' %
(threading.current_thread(), money, gMoney))
gLock.release() # 解锁
time.sleep(0.5) # 沉睡0.5秒
def main(): # 主程序入口
for x in range(5):
t = Producter(name='生产者线程%d' % x)
t.start()
for x in range(3):
t = Consumer(name='消费者线程%d' % x)
t.start()
if __name__ == "__main__":
main()
1.5.1 生产者消费者模式多线程 condition
版
信号量,发送通知。
condition
和一般的锁比较 其他都是一样的 。 这个类 可以发送通知。相当于你在外面等厕所,然后里面的人对你说,我快上好了。你准备一下。
此类(Condition
)实现条件变量对象
。条件变量允许一个或多个线程等待
,直到另一个线程通知它们。
如果 lock 是否给出了参数 None ,必须是 Lock 或 RLock 对象,它用作基础锁。否则,一个新的 RLock 对象被创建并用作基础锁。
两个线程 对一个全局变量进行操作。
一个对 Money
进行添加。
一个对 Money
进行减去
相当于 一个赚钱 一个花钱。 而 Money 就是银行账户。 这里的代码 唯一不一样的就是
释放锁 之前 通知了 群发消息。 让其他等待的线程知道 我要释放了。
import threading
import random
import time
# 通知
# 还有阻塞 的方式 减少加锁
gMoney = 1000 # 初始1000元
gCondition = threading.Condition() # 制造一把
gTotalTimes = 100 # 总的生产次数
gTimes = 0 # 已经生产的次数
class Producter(threading.Thread): # 生产者类
def run(self):
global gMoney
global gTimes
while True: # 死循环 保证他一直 生产金钱
money = random.randint(100, 1000)
gCondition.acquire() # 加锁操作
gMoney += money
print('%s生产了%d元钱,剩余%d' %
(threading.current_thread(), money, gMoney))
if gTimes >= gTotalTimes:
gCondition.release()
break
gTimes += 1
gCondition.notify_all() # 通知所有等待的线程, 处于等待状态的被唤醒,获取锁
gCondition.release() # 解锁释放
time.sleep(0.5) # 沉睡0.5秒
class Consumer(threading.Thread): # 消费者类
def run(self):
global gMoney
while True:
money = random.randint(100, 1000)
gCondition.acquire() # 加锁活动锁
while gMoney < money: # 这里 if 改为while 就可以循环 挂起等待 gMoney>money
if gTimes >= gTotalTimes: # 如果运行次数大于设计次数
gCondition.release() # 解锁,释放
return # 返回整个函数,终止掉此线程
print('%s消费者准备消费%d元钱,剩余%d元钱,金额不足.' %
(threading.current_thread(), money, gMoney))
gCondition.wait() # 等待
gMoney -= money
print('%s消费者消费了%d元钱,剩余%d元钱' %
(threading.current_thread(), money, gMoney))
gCondition.release() # 解锁
time.sleep(0.5) # 沉睡0.5秒
def main(): # 主程序入口
for x in range(1):
t = Producter(name='生产者线程%d' % x) # 设置线程名字
t.start()
for x in range(3):
t = Consumer(name='消费者线程%d' % x)
t.start()
if __name__ == "__main__":
main()
1.5.2 queue 线程安全队列
线程不安全是因为,线程是无序的,会造成数据错误,和之前加锁才会数据正确,是一样的
queue的线程是 安全的
队列就是先进先出
栈 先进后出, 后进先出,和弹夹 ,以及我们打字一样。 后打的字删除的时候 就是先删除的。
queue 常用函数
- 初始化
Queue(maxsize)
qsize()
返回队列的大小empty()
判断队列是否为空full()
判断队列是否满了get()
从队列中获取最先进去的数据,block
参数:block 阻塞默认是True
,如果队列没有值,就会一直阻塞在这里
put()
将一个数据放到队列当中block
参数: block 阻塞 默认是True
,如果队列满了,就会一直堵塞在这里,直到 队列为不满状态
简单测试
from queue import Queue
q = Queue(4)
for x in range(4):
q.put(x, block=True)
# 放入元素
# block堵塞默认是True ,如果队列满了,就会一直堵塞在这里,直到 不满
print(f"队列元素个数 qsize : {q.qsize()}") # 获取队列总共有多少个元素
print(f"是否为空 empyt :{q.empty()}") # 判断队列是否为空
print(f"是否装满了 full : {q.full()}") # 判断队列是否满了
print(
f"从队列中获取一个元素 : {q.get(block=True)}") # block堵塞默认是True ,如果队列没有值,就会一直堵塞在这里
q.put(10) # 这个地方执行完
print(q.qsize()) # 对列长度为 4 满了
q.put(10, block=True, timeout=5) # 这个地方 队列已经满了。 但是 阻塞 只阻塞 5 秒
# 超过 5 秒 就不阻塞了
# 不阻塞了。 队列还是满的 就报错了 。
运行结果
报错 Full
就是满了的意思。
下面代码 很简单
就是两个函数
一个函数 往队列中put
元素
一个函数从队列中 get
元素
使用多线程
from queue import Queue
import time
import threading
def set_value(q):
index = 0
while True:
q.put(index)
index += 1
time.sleep(3)
def get_value(q):
while True:
print(q.get()) # block默认是True 没有值 会 堵塞等待有值再获取
def main():
q = Queue(4)
t1 = threading.Thread(target=set_value, args=[q])
t2 = threading.Thread(target=get_value, args=[q])
t1.start()
t2.start()
if __name__ == "__main__":
main()
1.5.3 生产者消费者模式(queue
队列版)
本质还是一样的 只是变成了 对 队列的操作。
生产的东西放到 队列中了。
# 配合queue 安全队列的使用
from threading import Thread
from queue import Queue
import time
class Producter(Thread):
def run(self):
global queue
count=0
while True:
#判断队列的大小
if queue.qsize()<1000:
for i in range(100):
count+=1
msg='生产第'+str(count)+'个产品'
queue.put(msg)
print(msg)
time.sleep(0.5)
class Consumer(Thread):
def run(self):
global queue
while True:
if queue.qsize()>100:
for i in range(10):
msg=self.name+'消费'+queue.get()
print(msg)
time.sleep(1)
if __name__ == '__main__':
queue=Queue()
p=Producter()
c=Consumer()
p.start()
time.sleep(1)
c.start()
用多线程写一个爬虫
多线程 + 队列 实现 生产者和消费者模式的 爬虫
import requests
from lxml import etree
import os
import re
from queue import Queue
import queue
import threading
import string
import time
# 多线程的方式
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64)',
'Referer': 'http://www.doutula.com/'
}
PAT = string.punctuation
SAVEPATH = "".join([".", os.sep, "img"])
page_url_template = 'http://www.doutula.com/photo/list/?page={}'
get = requests.get
class Producter(threading.Thread): # 生产者模式
# 第一个*代表的是任意的未知参数, 后面第二个*的代表任意的关键字参数
def __init__(self, page_queue, img_queue, *args, **kwargs):
super(Producter, self).__init__(*args, **kwargs)
self.page_queue = page_queue
self.img_queue = img_queue
def run(self):
while True:
try:
url = self.page_queue.get(timeout=30)
html = self.gen_html_obj(url)
self.handle_html(html)
except queue.Empty:
print(f"page_queue->队列为空 page线程{threading.current_thread()}结束")
break
def gen_html_obj(self, url):
try:
r = get(url, headers=HEADERS, timeout=30)
r.raise_for_status() # 如果状态不是200 引发 异常
r.encoding = "utf-8" # 设置编码
html = etree.HTML(r.text)
return html
except requests.exceptions.BaseHTTPError as exc:
print(exc)
def handle_img_node(self, img):
"""
从图片节点提取属性
构造文件名字,以及获取图片url
"""
img_url = img.get('data-original')
# 用get方法获取data-original属性 的值 ,也就是下载链接
alt = img.get('alt')
alt = re.sub(fr'[{PAT}]', '', alt) # 把特殊字符给替换掉
suffix = os.path.splitext(img_url)[1] # 提取后缀
filename = alt + suffix # 组合图片名字
return img_url, filename
def handle_html(self, html):
# 获取 下面class 不等于 gif 的图片
if html is not None:
imgs = html.xpath(
"//div[@class='page-content text-center']//img[@class!='gif']")
for img in imgs: # data-original
result = self.handle_img_node(img)
self.img_queue.put(result) # 添加到队列里面
# result -> img_url, filename
class Consumer(threading.Thread):
def __init__(self, page_queue, img_queue, *args, **kwargs):
super(Consumer, self).__init__(*args, **kwargs)
self.page_queue = page_queue
self.img_queue = img_queue
def run(self):
# run 方法是重写的用来覆盖 我们继承的 threding.Tread的fun
# run 会自动运行
while True:
try:
img_url, filename = self.img_queue.get(timeout=60)
# 元组的话可以用这种方式解包
except queue.Empty:
print(f"img_queue -> 队列为空 img线程{threading.current_thread()}结束")
break
self.save_(img_url, filename)
def save_(self, img_url, filename):
res = get(img_url, headers=HEADERS).content
save_name = "".join([SAVEPATH, os.sep, filename])
try:
with open(save_name, "wb") as f:
f.write(res)
except FileNotFoundError:
os.mkdir(SAVEPATH)
with open(save_name, "wb") as f:
f.write(res)
finally:
print(filename, " —> 下载成功")
# print(img_url)
def main():
work_max = 20
page = 3 # 下载页数
img_work_max = 20 # 下载图片的线程数
work_max = min(work_max, page)
page_queue = Queue(work_max) # 页面队列 个 创建队列大小
img_queue = Queue(108*work_max) # 图片队列 个 创建队列大小
for x in range(1, page+1):
url = 'http://www.doutula.com/photo/list/?page=%d' % x
page_queue.put(url, timeout=30)
for x in range(work_max):
locals()[f"p{x}"] = Producter(page_queue, img_queue)
locals()[f"p{x}"].start()
for x in range(img_work_max):
locals()[f"t{x}"] = Consumer(page_queue, img_queue) # , daemon=True
locals()[f"t{x}"].start()
for x in range(img_work_max):
locals()[f"t{x}"].join()
if __name__ == "__main__":
start = time.time()
main()
print("线程销毁完毕 -> 程序结束")
print("耗时 -> ", time.time()-start, " 秒")
2 多进程
Python
提供了非常好用的多进程包multiprocessing
,只需要定义一个函数,Python会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行
的转换。multiprocessing
支持子进程、通信和共享数据
。语法格式如下:
语法
Process([group [, target [, name [, args [, kwargs]]]]])
# 创建子进程
# 导入模块
from multiprocessing import Process
def run_test():
print('...test.....')
if __name__=='__main__':
print('主进程执行')
# 创建子进程 target接收执行的任务
p=Process(target=run_test)
# 调用子进程
p.start()
2.1 创建子进程
# 创建子进程 并且传入参数
# 导入模块
from multiprocessing import Process
# 定义任务的函数
def run_test(name, age, **kwargs):
print('子进程正在运行 name的值:%s ,age的值:%d' % (name, age))
print('字典kwargs:', kwargs)
if __name__ == '__main__':
print('主进程开始执行')
# 创建子进程
# kwargs 可以传 也可以不传递
# p=Process(target=run_test,args=('test',),kwargs={'key':12})
p = Process(target=run_test, args=('test', 23), kwargs={'key': 12})
# 调用子进程
p.start()
运行结果
2.1 join 方法
和多线程的 效果是一样的。
阻塞。 等待子进程结束。
# join方法的使用
#导入模块
from multiprocessing import Process
from time import sleep
def worker(interval):
print('work start')
sleep(interval)
print('work end')
if __name__=='__main__':
print('主进程正在执行')
#创建子进程
p1 = Process(target=worker, args=(3, ))
p2 = Process(target=worker, args=(3, ))
#调用子进程
p1.start()
p2.start()
#希望下面的输出语句,再子进程执行完才输出
# sleep(4)
#调用join方法 :主进程等待调用join的子进程结束 才会结束
p1.join()
p2.join()
print("主进程执行完")
print('主进程执行完')
如果没有 join 可能 最后两句 print 就会 提前打印出来。 因为没有阻塞。
运行结果
2.1.1join
方法的 timeout
参数
# join方法中timeout的使用
#导入模块
from multiprocessing import Process
from time import sleep
def worker(interval):
print('work start')
sleep(interval)
print('work end')
if __name__=='__main__':
print('主进程正在执行')
#创建子进程
p=Process(target=worker,args=(5,))
#调用子进程
p.start()
#希望下面的输出语句,再子进程执行完才输出
# sleep(4)
#调用join方法 :主进程等待调用join的子进程结束
p.join(3) # 相当于 阻塞 3秒
print('主进程执行完')
因为只阻塞了 3秒。
而我们的任务中 睡眠了 5秒。
所以 主进程执行完 这句话还是 先打印了。
运行结果
2.2 process
属性
pid
name
is_alive
# process 属性的使用
# 导入模块
import multiprocessing
import time
# 定义执行任务的函数
def colck(interval):
for i in range(3):
print('当前时间:{}'.format(time.ctime()))
time.sleep(interval)
if __name__=='__main__':
# 创建子进程
p=multiprocessing.Process(target=colck,args=(1,))
p.start()
p.join()
print('p.pid:',p.pid)
print('p.name:',p.name)
print('p.is_alive:',p.is_alive())# 是否存活
is_alive
在最后才打印 所以那个时候 我们的子进程 p
以及 销毁。 返回 False
运行结果
2.3 创建多个任务
# 创建多个任务
# 导入模块
from multiprocessing import Process
from time import sleep
def work1(interval):
print('执行work1')
sleep(interval)
print('end work1')
def work2(interval):
print('执行work2')
sleep(interval)
print('end work2')
def work3(interval):
print('执行work3')
sleep(interval)
print('end work3')
if __name__=='__main__':
print('执行主进程')
p1=Process(target=work1,args=(4,))
p2=Process(target=work2,args=(2,))
p3=Process(target=work3,args=(3,))
# 调用子进程
p1.start()
p2.start()
p3.start()
p1.join()
p2.join()
p3.join()
print('p1.name:',p1.name)
print('p2.name:',p2.name)
print('p3.name:',p3.name)
print('主进程执行完')
运行结果:
2.4 继承的方式创建进程
# 使用继承方式创建进程
# 导入模块
# 什么叫实例化属性
# 有实例化属性的时候需要进行初始化
# 我们需要重写__init__ 初始化 构造函数
from multiprocessing import Process
from time import sleep
import time
# 定义类
class ClockProcess(Process):
# 重写初始化方法 覆盖父类的同名方法
def __init__(self,interval):
Process.__init__(self)
self.interval=interval
# 重写run() 覆盖父类的同名方法
def run(self):
print('子进程开始执行的时间:{}'.format(time.ctime()))
sleep(self.interval)
print('子进程结束的时间:{}'.format(time.ctime()))
if __name__ == '__main__':
# 创建子进程
p=ClockProcess(3)
# 调用子进程
p.start()
p.join()
print('主进程执行完')
运行结果
2.5 进程池非阻塞状态的使用
# 进程池非阻塞状态的使用
# 导入模块
import multiprocessing
import time
# 进程执行的任务函数
def func(msg):
print('start:', msg)
time.sleep(3)
print('end:', msg)
if __name__ == '__main__':
# 创建初始化3的进程池
pool = multiprocessing.Pool(3)
# 添加任务
for i in range(1, 6):
msg = '任务%d' % i
pool.apply(func, (msg, ))
# pool.apply_async(func, (msg, ))
# 此 apply_async 是异步的 多进程创建的 非阻塞
# 还有一个apply 是单进程的
# 如果进程池不再接收新的请求 调用close
pool.close()
# 等待子进程结束
pool.join()
print("程序结束")
apply()
调用 func 带着参数 args 和关键字参数 kwds . 它会一直阻塞,直到结果准备就绪。考虑到这一块, apply_async() 更适合并行执行工作。此外, func 只在池中的一个工作人员中执行。
相当于 这样
# 伪代码
p1.start()
p1.join()
p2.start()
p2.join()
我们来对比一下
apply_async
运行结果
apply
运行结果
可以看见 apply
是阻塞的。 相当于柜台排队,只有前面一个人 办完业务,然后你才可以上去。
但是 apply_async
有点像是 前面一个人在办。 然后 业务员 处理的时候, 有 其他时间,叫你上去,那个人在边上等结果,然后直接办你的业务。
2.6 阻塞 apply 使用
# 导入模块
# 进程池阻塞状态的使用
import multiprocessing
import time
# 进程执行的任务函数
def func(msg):
print('start:',msg)
time.sleep(3)
print('end:',msg)
if __name__ == '__main__':
# 创建初始化3的进程池
pool=multiprocessing.Pool(3)
# 添加任务
for i in range(1,6):
msg='任务%d'%i
pool.apply(func,(msg,))
# 单进程的就是阻塞的
# 如果进程池不再接收新的请求 调用close
pool.close()
# 等待子进程结束
pool.join()
运行结果
2.7 关于多个进程之间的数据共享。
- 多个进程之间数据
是否共享
?- 结论 此方法
不共享 num
的值(即全局变量,每个进程单独维护自己的内存) - 可以看出
每个子进程之间单独维护 一份自己的数据。
- 结论 此方法
全局变量仅仅只是被读取。
- 要
用队列
就可以共享
数据了- 目前常用的是
Queue
- 目前常用的是
# 导入模块
from multiprocessing import Process
num=10
def work1():
global num
num+=5
print('子进程1运行后:num的值',num)
def work2():
global num
num+=10
print('子进程2运行后:num的值',num)
if __name__ == '__main__':
print('主进程开始运行')
# 创建子进程
p1=Process(target=work1)
p2=Process(target=work2)
# 启动子进程
p1.start()
p2.start()
# 主进程等待子进程结束
p1.join()
p2.join()
print('全局变量num:',num)
运行结果
可以看出 每个子进程之间单独维护 一份自己的数据。
全局变量仅仅只是被读取。
实验二 发现 容器对象 也是 一样的并不是 引用, 而是 一个 新的 对象 , 看 id 打印的 内存地址就知道了。 (解决方案看 进程通信)
import multiprocessing as mp
def foo(pArg, res):
for i in range(pArg):
res.append(i)
print(f"res{pArg-2}", id(res))
return res
res1 = []
res2 = []
res3 = []
if __name__ == '__main__':
p1 = mp.Process(target=foo, args=(3, res1))
p2 = mp.Process(target=foo, args=(4, res2))
p3 = mp.Process(target=foo, args=(5, res3))
p1.start()
p2.start()
p3.start()
p1.join()
p2.join()
p3.join()
print("over")
print("res1 main", res1, id(res1))
print("res2 main", res2, id(res2))
print("res3 main", res3, id(res3))
2.8 使用队列进行通信
关于队列 的用法
本文前面 多线程部分写过了。
可以拉上去看一下。
这个函数本质上也是一样的。
一个函数 put
元素
一个函数 get
元素
只是用上了多进程(前面队列
那一节 用的多线程
)
# 导入模块
# 多进程之间通信
# 使用Queue
from multiprocessing import Process,Queue
from time import sleep
#定义写入的方法
def write(q):
a=['a','b','c','d']
for i in a:
print('开始写入的值:%s'%i)
q.put(i)
sleep(1)
def reader(q):
for i in range(q.qsize()):
print('读取到的值:%s'%q.get())
sleep(1)
if __name__ == '__main__':
# 创建队列
q=Queue()
# 创建进程
pw=Process(target=write,args=(q,))
pr=Process(target=reader,args=(q,))
pw.start()
pw.join()
pr.start()
pr.join()
2.8.1 多进程通信(Manager
)进程池通信
使用multiprocessing
自带的队列类 Manager().Queue()
来进行 进程池 Pool
之间的通信。
# 导入模块
# 多进程之间通信2
# 进程池之间的通信 不使用Queue
# 而是使用Manager
from multiprocessing import Process,Pool,Manager
from time import sleep
# 定义写入的方法
def write(q):
a=['a','b','c','d']
for i in a:
print('开始写入的值:%s'%i)
q.put(i)
sleep(1)
def reader(q):
for i in range(q.qsize()):
print('读取到的值:%s'%q.get())
sleep(1)
if __name__ == '__main__':
# 创建队列
q=Manager().Queue()
# 创建进程
pool=Pool(3)
pool.apply(write,(q,))
pool.apply(reader,(q,))
pool.close()
pool.join()
import multiprocessing as mp
def foo(pArg, res):
for i in range(pArg):
res.append(i)
print(f"res{pArg-2}", id(res))
if __name__ == '__main__':
Manager = mp.Manager
with Manager() as m:
res1 = m.list([])
res2 = m.list([])
res3 = m.list([])
p1 = mp.Process(target=foo, args=(3, res1))
p2 = mp.Process(target=foo, args=(4, res2))
p3 = mp.Process(target=foo, args=(5, res3))
p1.start()
p2.start()
p3.start()
p1.join()
p2.join()
p3.join()
print("over")
print("res1 main", res1[:], id(res1))
print("res2 main", res2[:], id(res2))
print("res3 main", res3[:], id(res3))