python线程(2)
GIL与普通互斥锁的区别
# 1.先验证GIL的存在
from threading import Thread, Lock
import time
money = 100
def task():
global money
money -= 1
for i in range(100): # 创建一百个线程
t = Thread(target=task)
t.start()
print(money)
# 2.再验证不同数据加不同锁
from threading import Thread, Lock
import time
money = 100
mutex = Lock()
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1)
money = tmp - 1
mutex.release()
"""
抢锁放锁也有简便写法(with上下文管理)
with mutex:
pass
"""
t_list = []
for i in range(100): # 创建一百个线程
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
# 为了确保结构正确 应该等待所有的线程运行完毕再打印money
print(money)
"""
GIL是一个纯理论知识 在实际工作中根本无需考虑它的存在
GIL作用面很窄 仅限于解释器级别
后期我们要想保证数据的安全应该自定义互斥锁(使用别人封装好的工具)
"""
#总结
GIL锁是解释器级别的锁,保证同一时刻进程中只有一个线程拿到GIL锁,拥有执行权限。而线程互斥锁是保证同一时刻只有一个线程能对数据进行操作,是数据级别的锁。
验证多线程作用
多线程的优势
1、线程在程序中是独立的,并发的执行流,划分尺度小于进程,所有多线程程序的并发性高
2、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,可以极大地提高进程程序的运行效率;
3、线程比进程具有更高的性能,由于同一个进程中的线程都有共性,多个线程共享同一个进程的虚拟空间,可以很容易实现通信。操作系统在创建进程中,必须为该进程分配独立内存空间,分配大量相关资源,但创建线程则简单得多
"""
两个大前提
CPU的个数
单个
多个
任务的类型
IO密集型
计算密集型
"""
# 单个CPU
多个IO密集型任务
多进程:浪费资源 无法利用多个CPU
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:耗时更长 创建进程的消耗+切换消耗
多线程:耗时较短 切换消耗
# 多个CPU
多个IO密集型任务
多进程:浪费资源 多个CPU无用武之地
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:利用多核 速度更快
多线程:速度较慢
结论:多进程和多线程都有具体的应用场景 尤其是多线程并不是没有用!!!
多进程计算密集型
# 代码示例:
from threading import Thread
from multiprocessing import Process
import os
import time
def run():
res = 1
for i in range(1, 10000):
res *= 1
if __name__ == '__main__':
print(os.cpu_count())
start_time = time.time()
p_list =[]
for i in range(8):
p = Process(target=run)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
# 输出结果:
8
总耗时:14.24073338508606
多线程计算密集型
# 代码示例:
from threading import Thread
from multiprocessing import Process
import os
import time
def run():
res = 1
for i in range(1, 10000):
res *= 1
if __name__ == '__main__':
print(os.cpu_count())
start_time = time.time()
t_list =[]
for i in range(8):
t = Thread(target=run)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print('总耗时:%s' % (time.time() - start_time))
# 输出结果:
8
总耗时:55.64063338508606
多进程IO密集型
# 代码示例:
def work():
time.sleep(2) # 模拟纯IO操作
if __name__ == '__main__':
start_time = time.time()
p_list = []
for i in range(100):
p = Process(target=work)
p.start()
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
# 输出结果:
总耗时:0.6527695655822754
多线程IO密集型
# 代码示例:
def work():
time.sleep(2) # 模拟纯IO操作
if __name__ == '__main__':
start_time = time.time()
t_list = []
for i in range(100):
t = Thread(target=work)
t.start()
for t in t_list:
t.join()
print('总耗时:%s' % (time.time() - start_time))
# 输出结果:
总耗时:0.015956878662109375
多线程总结:
# IO密集型
多线程总耗时:0.015956878662109375
多进程总耗时:0.6527695655822754
结论
多线程更好
# 计算密集型
多线程总耗时:14.24073338508606
多进程总耗时:55.64063338508606
结论
多进程更好
'注意的是 计算密集型的数要相对较大'
死锁现象
# 死锁的概念
两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,程序无法推进
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
class MyThread(Thread):
def run(self):
self.f1()
self.f2()
def f1(self):
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexB.acquire()
print(f'{self.name}抢到了B锁')
mutexB.release()
mutexA.release()
def f2(self):
mutexB.acquire()
print(f'{self.name}抢到了B锁')
time.sleep(2) # 死锁在这里,后面的代码不会继续执行
mutexA.acquire()
print(f'{self.name}抢到了A锁')
mutexA.release()
mutexB.release()
for i in range(20):
t = MyThread()
t.start()
# 执行结果:
Thread-1抢到了A锁
Thread-1抢到了B锁
Thread-1抢到了B锁
Thread-2抢到了A锁
"""锁不能轻易使用并且以后我们也不会在自己去处理锁都是用别人封装的工具"""
线程 A 得到某个资源 R1,同时去申请资源 R2,线程 B 得到了资源 R2,同时去申请资源 R1。这时就出现了死锁,线程 A 因为得不到资源 R2 而一直处于等待状态,线程 B 也因为得不到资源 R1 而一直处于等待状态。下面的代码演示了这种情况。
信号量
# 信号量和互斥锁
互斥锁同时只允许一个进程修改数据,而信号量则允许多个人同时修改数据
#信号量在不同的知识体系中 展示出来的功能是不一样的
eg:
在并发编程中信号量意思是多把互斥锁
在django框架中信号量意思是达到某个条件自动触发特定功能
"""
如果将自定义互斥锁比喻成是单个厕所(一个坑位)
那么信号量相当于是公共厕所(多个坑位)
互斥锁就相当于你家的卫生间,只有一个坑位,同时只能一个人上厕所;而信号量则相当于公共厕所,有多个坑位,可以多个人同时上厕所。比如现在有10个人需要上厕所,现在公共厕所只有3个坑位,这时候就先有3个人上厕所,等其中一个或者多个人出来之后,另外的人才能进去。
"""
# 代码示例:
from threading import Thread, Semaphore
import time
import random
sp = Semaphore(5) # 创建一个有五个坑位(带门的)的公共厕所
def task(name):
sp.acquire() # 抢锁
print('%s正在蹲坑' % name)
time.sleep(random.randint(1, 5))
sp.release() # 放锁
for i in range(1, 31):
t = Thread(target=task, args=('用户%s号' % i, ))
t.start()
'只要是跟锁相关的几乎都不会让我们自己去写 后期还是用模块'
event事件
"""
子线程的运行可以由其他子线程决定!!!
"""
from threading import Thread, Event
import time
event = Event() # 类似于造了一个红绿灯
def light():
print('红灯亮着的 所有人都不能动')
time.sleep(3)
print('绿灯亮了 油门踩到底 给我冲!!!')
event.set() # 绿灯亮了
def car(name):
print('%s正在等红灯' % name)
event.wait()
# 等灯绿 此时 event为False 直到event.set()将其值设置为True,才继续
print('%s加油门 飙车了' % name)
t = Thread(target=light)
t.start()
for i in range(20):
t = Thread(target=car, args=('熊猫PRO%s' % i,))
t.start()
# 这种效果其实也可以通过其他手段实现 比如队列(只不过没有event简便)
| event.isSet() |
返回event的状态值 |
| event.wait() |
如果event.isSet() == False将阻塞线程 |
| event.set() |
设置event的状态值为True,等待系统调度 |
| event.clear() |
恢复event的状态值为False |

进程池与线程池
#Python标准模块——concurrent.futures
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor:进程池,提供异步调用
'''问题:
计算机硬件是有物理极限的 我们不可能无限制的创建进程和线程
措施:
池:
保证计算机硬件安全的情况下提升程序的运行效率
进程池:
提前创建好固定数量的进程 后续反复使用这些进程(合同工)
线程池:
提前创建好固定数量的线程 后续反复使用这些线程(合同工)
如果任务超出了池子里面的最大进程或线程数 则原地等待
强调:
进程池和线程池其实降低了程序的运行效率 但是保证了硬件的安全!!!'''
基本方法
# submit(fn, *args, **kwargs):
异步提交任务
# map(func, *iterables, timeout=None, chunksize=1):
取代for循环submit的操作
# shutdown(wait=True):
相当于进程池的pool.close()+pool.join()操作
#wait=True,
等待池内所有任务执行完毕回收完资源后才继续
# wait=False,
立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
# submit和map必须在shutdown之前
# result(timeout=None):
取得结果
# add_done_callback(fn):
回调函数
#done():
判断某一个线程是否完成
#cancle():
取消某个任务
进程池代码示例
from concurrent.futures import ProcessPoolExecutor
import os, time, random
def task(n):
print('%s is runing' % os.getpid())
time.sleep(random.randint(1, 3))
return n ** 2
if __name__ == '__main__':
executor = ProcessPoolExecutor(max_workers=3)
futures = []
for i in range(11):
future = executor.submit(task, i)
futures.append(future)
executor.shutdown(True)
print('+++>')
for future in futures:
print(future.result())
pool = ProcessPoolExecutor(5)
# 进程池进程数默认是CPU个数 也可以自定义
'''上面的代码执行之后就会立刻创建五个等待工作的进程'''
pool.submit(task, i).add_done_callback(func)
"""add_done_callback只要任务有结果了 就会自动调用括号内的函数处理"""
协程
# 协程的概念
可以停止当前函数的执行并保存当前状态, 并在下次执行时进行恢复. 对于单线程的运行来说, 什么时候需要这种操作呢??
等待的时候. 比如等待文件打开, 等待锁, 等待网络返回等等. 这时程序运行着也没什么事做, 就可以先去做其他事情, 等这边好了再继续回来执行
"""
进程:资源单位
线程:执行单位
协程:单线程下实现并发
并发的概念:切换+保存状态
首先需要强调的是协程完全是程序员自己想象出来的名词!!!
对于操作系统而言只认识进程和线程
协程就是自己通过代码来检测程序的IO操作并自己处理 让CPU感觉不到IO的存在从而最大幅度的占用CPU
类似于一个人同时干接待和服务客人的活 在接待与服务之间来回切换!!!
"""
协程的优缺点
# 协程的优点
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu
# 协程的缺点
1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
#总结协程特点:
必须在只有一个单线程里实现并发
修改共享数据不需加锁
用户程序里自己保存多个控制流的上下文栈
附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
协程的基本使用
# 保存的功能 我们其实接触过 yield 但是无法做到检测IO切换
from gevent import monkey;monkey.patch_all()
# 固定编写 用于检测所有的IO操作
from gevent import spawn
import time
def play(name):
print('%s play 1' % name)
time.sleep(5)
print('%s play 2' % name)
def eat(name):
print('%s eat 1' % name)
time.sleep(3)
print('%s eat 2' % name)
start_time = time.time()
g1 = spawn(play, 'jason')
g2 = spawn(eat, 'jason')
g1.join() # 等待检测任务执行完毕
g2.join() # 等待检测任务执行完毕
print('总耗时:', time.time() - start_time)
# 正常串行肯定是8s+
# 5.00609827041626 代码控制切换
基于协程实现TCP服务端并发
from gevent import monkey;monkey.patch_all()
from gevent import spawn
import socket
def communication(sock):
while True:
data = sock.recv(1024) # IO操作
print(data.decode('utf8'))
sock.send(data.upper())
def get_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, addr = server.accept() # IO操作
spawn(communication, sock)
g1 = spawn(get_server)
g1.join()
"""
终极结论
python可以通过开设多进程 在多进程下开设多线程 在多线程使用协程
从而让程序执行的效率达到极致!!!
但是实际业务中很少需要如此之高的效率(一直占着CPU不放)
因为大部分程序都是IO密集型的
所以协程我们知道它的存在即可 几乎不会真正去自己编写
"""
