- 多任务 : 操作系统可以同时运行多个任务
- 并发 : 任务数 > cpu核数, 通过操作系统任务调度算法, 实现用多个任务在同一时间段执行(事实上只有cpu核数个在执行)
- 并行 : 多核cpu情况下,多个任务的一些任务往往是在同一时间点执行的
在实际的场景中往往既有并发又有并行的多任务。
一、多线程
线程 :一个进程内部的一条代码执行流程(线程)
代码默认在默认线程上执行
1. threading模块
python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
- 函数实现多线程
import threading
import time
def say_sorry():
print("I am sorry")
time.sleep(1)
if __name__ == '__main__':
for i in range(5):
t = threading.Thread(target=say_sorry)
t.start()
- 程序会等待所有的子线程结束后才结束
- 线程数量
threading.enumerate()
2. 多线程代码封装
- 类实现多线程
import threading
import time
class MyThread(threading.Thread):
def run(self):
time.sleep(1)
print(self.name)
if __name__ == '__main__':
for i in range(5):
t = MyThread()
t.start()
执行结果
Thread-3
Thread-1
Thread-2
Thread-4
Thread-5
- 线程执行顺序
从上面可以看出多线程程序的执行顺序是不确定的
3. 多线程-共享全局变量
在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
缺点 : 线程是对全局变量随意修改可能造成多线程之间对全局变量的混乱(即非线程安全)
- 线程不安全案例
import threading
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print(g_num)
if __name__ == '__main__':
t1 = threading.Thread(target=work1, name="1T", args=(10000000,))
t2 = threading.Thread(target=work1, name="2T", args=(10000000,))
t1.start()
t2.start()
- 同步
线程同步:一个操作完成后再进行下一个操作
同步用互斥锁完成
程序中的同步相当于生活中的异步, 异步相当于生活中的同步
# 创建锁
lock = threading.Lock()
# 锁定
lock.acquire()
# 释放
lock.release()
上面线程不案例的案例可以这样解决
lock = threading.Lock()
lock.acquire()
global g_num
lock.release
- 锁总结
- 锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
- 锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
- 死锁
A锁等待B锁先释放, B锁等待A锁先释放,将造成死锁
- 避免死锁的方法
- 程序设计时尽量避免
- 添加超时时间
4. 案例:udp多线程聊天室
- 示例
import threading
from socket import *
def send_msg(udp_socket):
while True:
udp_socket.sendto(input("请输入内容:").encode("utf-8"), ("127.0.0.1", 8888))
def recv_msg(udp_socket):
while True:
recv_msg = udp_socket.recvfrom(1024)
print("服务器返回消息:"+recv_msg[0].decode("utf-8"))
def main():
# 1.创建socket
udp_socket = socket(AF_INET, SOCK_DGRAM)
udp_socket.bind(("", 9999))
# 发送接收数据
recv_thread = threading.Thread(target=recv_msg, args=(udp_socket,))
recv_thread.start()
send_msg(udp_socket)
if __name__ == '__main__':
main()
二、多进程
1. 进程介绍
程序 : 例如xxx.py这是程序,是一个静态的
**进程 **: 一个程序运行起来后,代码+用到的资源称之为进程,它是操作系统分配资源的基本单元。
- 就绪态 : 运行的条件都已经慢去,正在等在cpu执行
- 执行态 : cpu正在执行其功能
- 等待态 : 等待某些条件满足,例如一个程序sleep了,此时就处于等待态
2. 创建进程-multiprocessing
- 函数创建子进程
import time
from multiprocessing import Process
def run_proc(name, age, **kwargs):
time.sleep(1)
print(1)
if __name__ == '__main__':
for i in range(2):
p = Process(target=run_proc, args=("test", 18), kwargs={"m": 20})
p.start()
3. Process语法结构如下:
Process([group[,target[,name[,args[,kwargs]]]]])
- target:如果传递了函数的引用,可以任务这个子进程就执行这里的代码
- args:给target指定的函数传递的参数,以元组的方式传递
- kwargs:给target指定的函数传递命名参数
- name:给进程设定一个名字,可以不设定
- group:指定进程组,大多数情况下用不到
Process创建的实例对象的常用方法:
- start():启动子进程实例(创建子进程)
- is_alive():判断进程子进程是否还在活着
- join([timeout]):是否等待子进程执行结束,或等待多少秒
- terminate():不管任务是否完成,立即终止子进程
Process创建的实例对象的常用属性:
- name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
- pid:当前进程的pid(进程号)
4. 进程间不共享全局变量
- 示例
import time
from multiprocessing import Process
nums = [11, 22]
def run_proc():
nums.append(1)
time.sleep(3)
print(nums)
if __name__ == '__main__':
p = Process(target=run_proc)
p.start()
p.join()
p2 = Process(target=run_proc)
p2.start()
结果为
[11, 22, 1]
[11, 22, 1]
三、进程、线程对比
进程 :能够完成多任务,比如在一台电脑上能够同时运行多个QQ
线程 :能够完成多任务,比如一个QQ中的多个聊天窗口
- 定义的不同
- 进程 :系统进行资源分配和调度的一个独立单位.
- 线程 :是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和
栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
- 区别
- 一个程序至少有一个进程,一个进程至少有一个线程.
- 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
线线程不能够独立执行,必须依存在进程中
可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人
- 优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
四、进程间通信
1. Queue
可以使用multiprocessing模块的Queue实现多进程之间的数据传递
Queue本身是一个消息列队程序
- Queue的方法
-
q=Queue(5)
# 5为最大接收消息数量 -
Queue.qsize()
: 返回当前队列包含的消息数量; -
Queue.empty()
: 如果队列为空,返回True,反之False; -
Queue.full()
: 如果队列满了,返回True,反之False; Queue.get([block[,timeout]])
: 获取队列中的一条消息,然后将其从列队中移除
- block默认值为True,无timeout时, 消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止
- 如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;
- block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;
-
Queue.get_nowait()
: 相当Queue.get(False); -
Queue.put(item,[block[,timeout]])
: 将item消息写入队列,block默认值为True; -
Queue.put_nowait(item)
: 相当Queue.put(item,False);
- Queue示例
from multiprocessing import Queue
q = Queue(2)
if not q.full():
q.put_nowait("info")
if not q.empty():
q.get_nowait()
2. Queue在进程间的通信
- 示例
from multiprocessing import Queue, Process
def write(q):
while True:
if not q.full():
q.put_nowait("info")
def read(q):
while True:
if not q.empty():
print(q.get_nowait())
def main():
# 1.父进程创建Queue并传给子进程
q = Queue(3) # type:Queue
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 2.写入的子进程写入数据
pw.start()
pw.join()
# 3.读取的子进程读取数据
pr.start()
pr.join()
# 4.结束
print("over")
if __name__ == '__main__':
main()
五、进程池Pool
multiprocessing中的Pool方法一次性动态成生多个进程
- Pool(5)
- 初始化5个进程
- 新请求提交到Pool时,若Pool没满,创建新进程执行该请求
- 若Pool已达最大值,请求等待,直到池中有进程结束,才会用之前的进程来执行新的任务
- multiprocessing.Pool常用函数解析
-
apply_async(func[,args[,kwds]])
: 使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表; -
close()
: 关闭Pool,使其不再接受新的任务; -
terminate()
: 不管任务是否完成,立即终止; -
join()
: 主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
- 示例
from multiprocessing import Pool
def work(msg):
pass
po = Pool(3)
for i in range(3):
po.apply_async(work, (i,))
po.close() # 关闭进程池,关闭后po不再接收新的请求
po.join() # 等待po中所有子进程执行完成,必须放在close语句之后
- 进程池中的Queue
使用Pool创建进程,使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue()
from multiprocessing import Manager
Manager().Queue(3)
案例:copy功能
- 示例
import os
from multiprocessing import Process, Queue
def copy_file(file_path, new_dir_path, q):
if os.path.isdir(file_path):
os.mkdir(new_dir_path)
else:
content = None
try:
with open(file_path, "rb") as fr:
content = fr.read()
except Exception as e:
print(e)
try:
with open(new_dir_path, "wb") as fw:
fw.write(content)
except Exception as e:
print(e)
if not q.full():
q.put_nowait(file_path)
file_path_list = []
dir_path_list = []
def get_file_path(path):
if os.path.isfile(path):
file_path_list.append(path)
else:
dir_path_list.append(path)
path_list = os.listdir(path)
for new_path in path_list:
get_file_path(path + "/" + new_path)
return file_path_list, dir_path_list
def main():
# 消息对列用于显示进度
q = Queue(1000)
# 1.获取copy的文件夹
dir_path = input("请输入要copy的文件夹绝对路径:")
dir_path_back = dir_path + "[backup]"
# 2.递归取出每个空文件夹及文件路径
get_file_path(dir_path)
# 3.分别copy文件及空文件夹 到 新的文件夹
for empty_file_path in dir_path_list:
new_dir_path = dir_path_back + empty_file_path[dir_path_back.find("["):]
copy_file(empty_file_path, new_dir_path, q)
for file_path in file_path_list:
# /home/test[back] /home/test/t.txt --> /home/test[back]/t.txt
new_dir_path = dir_path_back + file_path[dir_path_back.find("["):]
# copy_file(file_path, new_dir_path, q)
p = Process(target=copy_file, args=(file_path, new_dir_path, q))
p.start()
# 4.显示进度
count = 0
sum = len(file_path_list) + len(dir_path_list)
while True:
if not q.empty():
q.get_nowait()
count += 1
print("进度为%.2f%%" % (count/sum*100))
if count/sum == 1:
break
if __name__ == '__main__':
main()
六、迭代器
1. 可迭代对象
- 可迭代对象
可迭代对象(Iterable) :可以通过for…in…这类语句迭代读取一条数据供我们使用的对象
- 如何判断一个对象是否可以迭代
isinstance()
判断一个对象是否是Iterable对象
# 可迭代对象有:[], (), {}, "abc"
isinstance({}, Iterable) # 判断一个对象是否是可迭代对象
- 可迭代对象的本质
- 迭代器:每迭代一次,for…in…都返回下一条数据,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,我们把这个“人”称为迭代器(Iterator)。
- 可迭代对象的本质 :可以向我们提供一个这样的中间“人”即迭代器帮我们对其进行迭代遍历
- 可迭代对象的实现 :通过
__iter__
方法向我们提供一个迭代器,那么也就是说,一个具备__iter__
方法的对象,就是一个可迭代对象。
- 可迭代对象的实现
from collections import Iterable
class MyList(object):
def __init__(self):
self.container = []
def add(self, item):
self.container.append(item)
def __iter__(self):
pass
my_list = MyList()
print(isinstance(my_list, Iterable)) # True
2. 迭代器
一个实现了
__iter__
方法和__next__
方法的对象,就是迭代器。
迭代器 : 帮助记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。
实际上 ,在使用next()函数的时候,调用的就是迭代器对象的 __next__
方法(Python3中是对象的 __next__
方法,Python2中是对象的next()方法)
构造一个迭代器 , 就要实现它的__next__
方法。python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现 __iter__
方法,,而 __iter__
方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的 __iter__
方法返回自身即可。
- iter()
iter()函数可获取可迭代对象的迭代器
from collections import Iterator
print(isinstance([], Iterator)) # False
print(isinstance(iter([]), Iterator)) # True
print(isinstance(iter("abc"), Iterator)) # True
- next()
对获取到的迭代器不断使用next()获取下一条数据
list_iter = iter([1, 2])
next(list_iter)
# 当next取不到数据时,抛出StopIteration异常
3. for … in … 本质
iter() --获取–> 可迭代对象Iterable的迭代器 --> next()获取值赋给item --> 异常StopIteration结束循环
4. 应用
迭代器通过next()函数的调用来返回下一个数据值 --> 不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间
- 示例
from collections import Iterable
class FibIterator(object):
def __init__(self, n):
self.n = n
self.current = 0
self.num1 = 0
self.num2 = 1
def __iter__(self):
return self
def __next__(self):
if self.current < self.n:
self.num1, self.num2 = self.num2, self.num1+self.num2
self.current += 1
return self.num1
else:
raise StopIteration
fib = FibIterator(10)
for num in fib:
print(num, end=" ")
七、生成器
1. 生成器介绍
生成器是一类特殊的迭代器
利用迭代器可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成.
但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据
为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)
2. 创建生成器
- [] --> () : 把一个列表生成式的[]改成()
G = (x*2 for x in range(5))
对于生成器G, 可以按照迭代器的使用方法来使用,即可以通过next()函数、for循环、list()等方法使用。
next(G)
for x in G:
print(x)
- 方法二:yield
只要在def中有yield关键字的就称为生成器
def fib(n):
current = 0
num1, num2 = 0, 1
while current < n:
num = num1
num1, num2 = num2, num1 + num2
current += 1
yield num # yield使fib函数成为一个生成器而不再是一个函数
return "done"
F = fib(5)
print(next(F))
for f in F:
print(f)
获取生成器中return的值
返回值包含在StopIteration的value中
F = fib(5)
while True:
try:
x = next(F)
except StopIteration as e:
print(e.value)
break
3. 使用send方法
- send唤醒生成器
...
temp = yield num
...
next(F) # next(F) 等价于 F.send(None)
F.send("python") # 此时的temp就为"python"
# 正确的解释是next和send函数在执行完yield后暂停执行(断点),即将生成器函数挂起。并将yield后的值返回
# 使用send时在断点处传入一个“附加数据”,temp用来接收这个附加数据
八、协程
1. 协程介绍
- 协程,又称微线程,纤程
- 它是一个自带CPU上下文的执行单元
- 例如: 可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行并且切换次数及时机由开发者确定
- 协程和线程差异
线程切换从系统层面远不止保存和恢复CPU上下文这么简单。操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。所以线程的切换非常耗性能。
但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
2. 协程的实现
- 协程的简单实现
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
用yield实现协程
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁
协程生产消费模型生产者生产消息后,直接通过
yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高
- gevent
其原理是当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
pip3 install gevent
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
gevent.sleep(1)
g1 = gevent.spawn(f, 2)
g2 = gevent.spawn(f, 2)
g1.join()
g2.join()
# 运行结果为
<Greenlet at 0x7fc4eca68448: f(5)> 0
<Greenlet at 0x7fc4eca68648: f(5)> 0
<Greenlet at 0x7fc4eca68448: f(5)> 1
<Greenlet at 0x7fc4eca68648: f(5)> 1
3. 给程序打补丁
- 示例
将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
import random
import time
from gevent import monkey
import gevent
monkey.patch_all() # 猴子补丁
def work(work_name):
for i in range(2):
time.sleep(random.random())
print(work_name)
gevent.joinall([
gevent.spawn(work, "work1"),
gevent.spawn(work, "work2")
])
4. 进程、线程、协程总结
- 进程是资源分配的单位
- 线程是操作系统调度的单位
- 进程切换需要的资源很最大,效率很低
- 线程切换需要的资源一般,效率一般
- 协程切换任务资源很小,效率高
- 多进程、多线程根据cpu核数不一样可能是并行的也可能是并发的。协程的本质就是使用当前进程, 在不同的函数代码中切换执行,可以理解为并行。协程是一个用户层面的概念,不同协程的模型实现可能是单线程也可能是多线程。