笔记讲解python多任务中的基础知识点,包括:进程、线程、协程、迭代器、生成器等
多任务
概念:操作系统 同时运行多个任务。
方式:并行 并发
并行
任务数 小于CPU核数。任务是真的一起执行
并发
任务数 大于CPU核数。操作系统快速切换任务,造成看起来一起发生的效果。
多线程
import threading
import time
def sing():
"""唱歌5秒"""
for i in range(5):
print("---唱歌中---")
time.sleep(1)
def dange():
"""跳舞5秒"""
for i in range(5):
print("---跳舞---")
time.sleep(1)
def main():
t1 = threading.Thread(target=sing) # 函数名,不带括号
t2 = threading.Thread(target=dange)
t1.start()
t2.start()
if __name__ == '__main__':
main()
程序中总有一个主线程,从上到下执行代码。当主线程遇到t.start()
时,才分出一个子线程。
多线程之间共享全局变量
给线程传递参数
def test(num):
print(num)
t = threading.Thread(target=test, args=(100, )) # 100是要传给test函数的参数,必须写成元组形式
t.start()
互斥锁
"""
由于 线程共享全局变量,所以不同线程 操作 同一个全局变量时,
为了解决资源竞争问题,所以要用到互斥锁。
"""
import threading
import time
g_num = 0
def test1(num):
global g_num
# ↓↓ 若mutex处于未锁状态,则此时上锁成功;若mutex处于锁定状态,则此时阻塞,直至mutex恢复解锁状态
mutex.acquire()
for i in range(num):
g_num += 1
mutex.release()
# ↑↑ 解锁
print("test1结束后g_num的值为%d" % g_num)
def test2(num):
global g_num
mutex.acquire()
for i in range(num):
g_num += 1
mutex.release()
print("test2结束后g_num的值为%d" % g_num)
# 创建一个互斥锁,默认是未上锁状态
mutex = threading.Lock()
def main():
t1 = threading.Thread(target=test1, args=(1000000,))
t2 = threading.Thread(target=test1, args=(1000000,))
t1.start()
t2.start()
time.sleep(3)
print("最终g_num的值为%d" % g_num)
if __name__ == '__main__':
main()
上锁部分的代码越少越好
锁内的代码耗时越长,其他用到锁的地方就需要等待得越长,所以将必要的操作锁起来就可以:
def test1(num):
global g_num
for i in range(num):
mutex.acquire()
g_num += 1
mutex.release()
# ↑↑ 解锁
print("test1结束后g_num的值为%d" % g_num)
进程
程序:xx.py
、QQ.exe
这种类似的是程序,它是静态的
进程:一个程序运行起来后,代码+用到的资源 称之为进程。
进程是操作系统 分配资源 的基本单位
代码示例(几乎和threading模块一样)
import multiprocessing
import time
def test1():
while True:
print('1......')
time.sleep(1)
def test2():
while True:
print('2......')
time.sleep(1)
def main():
p1 = multiprocessing.Process(target=test1)
p2 = multiprocessing.Process(target=test2)
p1.start()
p2.start()
if __name__ == '__main__':
main()
当代码运行到p1.start()
时,相当于又增加了一份代码,子进程在新的代码中运行;p2.start()
时又多一个子进程。这时程序中就有了1个主进程+2个子进程
代码+资源=进程
先有进程,再有线程
进程间通信
队列:Queue # 先进先出
import multiprocessing
q = multiprocessing.Queue(3) # 数据最多只能放3个
# ↓↓ q可以放置任意类型的数据
q.put(111)
q.put('fasd')
q.put([1, 23, '23'])
q.put(1) # 如果队列满了,则会一直等待
q.get()
q.get()
q.get()
q.empty() # q是否为空
q.full() # q是否满
q.get() # 如果队列为空,那么会一直等待
q.get_nowait() # 如果队列为空,那么会直接报错。一般和异常捕获结合使用
q.put_nowait()
队列示例:
"""模拟 同时进行 下载数据 和 解析数据"""
import multiprocessing
def download_from_web():
"""模拟下载数据"""
data = [1, 2, 3, 4]
print("已经下载完毕数据")
def analysis_data():
"""模拟分析数据"""
print("进行分析数据")
def main():
p1 = multiprocessing.Process(target=download_from_web)
p2 = multiprocessing.Process(target=analysis_data)
p1.start()
p2.start()
if __name__ == '__main__':
main()
由于进程之间数据不共享,所以,上面的代码要配合Queue来进行同时下载、解析。改为如下代码:
def download_from_web(q):
"""模拟下载数据"""
data = [1, 2, 3, 4]
# ↓↓ 向队列q中放数据
for i in data:
q.put(i)
print("已经下载完毕数据")
def analysis_data(q):
"""模拟分析数据"""
data_list = list()
# 从队列中获取数据
while True:
data = q.get()
data_list.append(data)
if q.empty():
break
print("进行分析数据:", data_list)
def main():
q = multiprocessing.Queue() # 如果不填数字,默认最大
p1 = multiprocessing.Process(target=download_from_web, args=(q,))
p2 = multiprocessing.Process(target=analysis_data, args=(q,)) # 通过传参将q传入
p1.start()
p2.start()
协程
迭代器
可迭代对象
from collections import Iterable
a = [1, 2, 3, 4, 5]
isinstance(a, Iterable) # 结果为True
# 只要一个对象,属于Iterable的子类,那么这个对象就是可迭代对象
# 自造可迭代对象
class Classmate:
def __iter__(self):
pass
c = Classmate()
print("c是否为可迭代对象?", isinstance(c, Iterable)) # 结果为True
所以,只要一个对象中有__iter__
方法,那么它就是可迭代对象。但此时,依旧不能用for循环来遍历它。
for 循环遍历的条件
-
这个对象是可迭代对象(拥有
__iter__
方法) -
它的
__iter__
方法返回一个对象的引用。这个对象必须满足:- 这个对象是可迭代对象(拥有
__iter__
方法) - 这个对象拥有
__next__
方法
- 这个对象是可迭代对象(拥有
代码示例:
"""如何使它能够被for循环迭代??"""
# 要求1:这个对象必须有__iter__方法
# 要求2:它的__iter__方法必须返回 一个对象的引用。
# 这个对象的要求是:1、有__iter__方法;2、有__next__方法。
class Classmate:
def __iter__(self):
return ClassIterator() # 返回一个对象的引用
# ↑↑ 这个对象ClassIterator() 它的内部包含__iter__和__next__方法
class ClassIterator:
def __iter__(self):
pass
def __next__(self):
pass
c = Classmate()
此时,可迭代对象c ,就可以被for循环遍历(理论上)。
对象c的__iter__
方法的返回值:ClassIterator()
,就是一个迭代器
当c被for循环时,就相当于调用ClassIterator()
这个对象的__next__
方法。__next__
返回什么,for循环就得到什么。
简化迭代器
上例中,ClassIterator()
对象只需要满足两个条件就可以作为迭代器,那么可以直接在原来的可迭代对象中新增__next__
方法,让其既是可迭代对象,又是迭代器:
import time
from collections import Iterable, Iterator
class Classmate:
def __init__(self):
self.names = list()
self.current_num = 0
def __iter__(self):
return self
def __next__(self):
if self.current_num < len(self.names):
ret = self.names[self.current_num]
self.current_num += 1
return ret
else:
raise StopIteration
classmate = Classmate()
classmate.add('小明')
classmate.add('小花')
classmate.add('小龙')
for name in classmate:
print(name)
time.sleep(1)
生成器
生成器是一种特殊的迭代器
生成器实现方式
-
列表生成式的方括号
[]
换成圆括号()
nums = [x for x in range(10)] print(nums) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] nums = (x for x in range(10)) print(nums) # <generator object <genexpr> at 0x00000271725FF648>
-
函数中有
yield
def fib(count): a, b = 0, 1 current_count = 0 while current_count < count: # print(a) yield a # 把要使用时的print换成yield,这个函数被调用时,就获得了一个生成器 a, b = b, a + b current_count += 1 obj = fib(10) for i in obj: print(i) print('------------------------------------------') # 如果紧接着用for循环,会发现什么都打印不出来 # for j in obj: # print(j) # 如果用next调用,会发现直接报错。(因为生成器中的值都已经被第一个for循环打印出来了) print(next(obj)) # 如果 注释掉上面的for循环,这里就可以打印。且每次打印的值都是下一个的值。 print(next(obj)) print(next(obj)) 如果向多次使用这个生成器,则 再创建一个生成器对象即可。 obj2 = fib(20) print(next(obj2))
**分析:**for循环内,函数执行到
yield a
时 会暂停,把a直接打印出来,当for循环再次遍历obj时,函数会接着上次yield a
的地方继续执行。
使用生成器完成多任务——协程
import time
def task_1():
while True:
print("---任务1执行中---")
time.sleep(0.2)
yield # 加上yield,让这个任务函数变成生成器
def task_2():
while True:
print("---任务2执行中---")
time.sleep(0.2)
yield
def main():
t1 = task_1()
t2 = task_2()
while True:
next(t1) # 利用生成器的方式切换任务,这就是 协程
next(t2)
if __name__ == '__main__':
main()
协程方式使用的模块——greenlet、gevent
简单总结
- 进程是资源分配的单位
- 线程是操作系统调度的单位
- 进程切换需要的资源很大,效率很低
- 线程切换需要的资源一般,效率一般。(不考虑GIL)
- 协程切换任务资源很小,效率高
- 多进程、多线程根据CPU核数不一样,可能是并行的,但协程是在一个线程中,所以是并发