线程的应用
1 > GIL与普通互斥锁的区别
1.1 > 先验证GIL的存在
例如我们先创建100 个线程,让每个线程都去修改主线程中的数据。由于GIL锁的存在,子线程会依此对主线程中的数据进行修改,操作如下:
from threading import Thread
money = 100
def task():
global money
money -= 1
for i in range(100): # 创建100个线程
t = Thread(target=task)
t.start()
print(money) # 0
1.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 rang(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
# 为了确保结构正确 应该等待所有的线程运行完毕再打印money
2 > 验证多线程作用
有两种情况,单个cpu和多个cpu
2.1 > 单个cpu
多个IO密集型任务
多进程: 浪费资源 无法利用多个CPU
多线程: 节省资源 切换+保存状态
多个计算密集型任务
多进程:耗时更长 创建进程的消耗+切换消耗
多线程:耗时较短 切换消耗
2.2 > 多个cpu
多个IO密集型任务
多进程:浪费资源 多个CPU无用武之地
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:利用多核 速度更快
多线程:速度较慢
结论就是:多进程和多线程都有具体的应用场景 尤其是多线程并不是没有用。
2.3 > 代码验证
''' 模拟创建多进程处理密集型计算任务 '''
from multiprocessing import Process
import time
def work():
res = 1
for i in range(1, 100000):
res *= i
if __name__ == '__main__':
start_time = time.time()
p_list = []
for i in range(8):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
''' 模拟创建多线程处理密集型计算任务 '''
from threading import Thread
import time
def work():
res = 1
for i in range(1, 100000):
res *= i
if __name__ == '__main__':
start_time = time.time()
t_list = []
for i in range(8):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print('总耗时:%s' % (time.time() - start_time))
由此可以得出,当处理计算密集型任务时。多进程耗时时间:5.4863…, 多线程耗时时间:18.315… 所以可以知道时多进程更好。
''' 模拟创建多线程处理IO密集型任务 '''
from threading import Thread
import time
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))
''' 模拟创建多进程处理IO密集型任务 '''
from threading import Thread
import time
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))
由此可以得出,当处理计算密集型任务时。多进程耗时时间:2.4953…, 多线程耗时时间:0.0149… 所以可以知道此时多4线程更好。
3 > 死锁现象
锁的知识我们都知道了,但是就算掌握了如何枪锁,如何放锁,也会产生死锁的现象。代码如下:
from threading import Thread, Lock
import time
# 产生两把锁A,B
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()
所以说对于互斥锁我们仅仅需要理解它是如何执行的,在工作中让我们自己写这些的情况都是很少很少的,我们都是会去用一些别人别人写好的不会死锁模块。
4 > 信号量(了解)
信号量在不同的知识体系中,展示出来的功能是不一样的。在并发编程中信号量是多把互斥锁,在django框架中信号量的意思是达到某个条件自动触发特定功能。我们可以将信号量在并发编程中形象的比喻成公共厕所(有多个坑位),将自定义互斥锁比喻成是单个厕所(一个坑位)。操作代码如下:
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()
5 > event事件(了解)
之前我们学过用join方法让主进程/主线程,控制子进程/子线程,然而event事件是可以让子线程的运行可以由其他子线程决定。代码如下:
from threading import Thread, Event
import time
event = Event() # 类似于造了一个红绿灯
def light():
print('红灯亮着的 所有人都不能动')
time.sleep(10)
print('3 2 1 GO !!!')
event.set()
def car(name):
print('%s 正在等红灯 ' % name)
event.wait()
print('%s加油门 飙车了' % name)
t = Thread(target=light) # 创建主线程
t.start()
for i in range(3):
t = Thread(target=car, args=('熊猫 PRO%s' % i,)) # 创建子线程
t.start()
6 > 进程池与线程池(重点)
池: 保证计算机硬件安全的情况下提升程序的运行效率
进程池的意思就是: 提前创建好固定数量的进程 后续反复使用这些进程(合同工)
线程池的意思就是: 提前创建好固定数量的线程 后续反复使用这些线程(合同工)
如果任务超出了池子里面的最大进程或线程数 则原地等待
进程池和线程池其实降低了程序的运行效率 但是保证了硬件的安全!!!
代码演示如下:
''' 线程池演示代码 '''
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
pool = ThreadPoolExecutor(5) # 线程池线程数默认是cpu个数的五倍,也可以自定义
'''
上面的代码执行之后就会立刻创建5个等待工作的线程
'''
def task(n):
time.sleep(3)
print(n)
return '任务的执行结果 :%s' % (n ** 2)
def func(*args, **kwargs):
print(args[0].result())
for i in range(10):
pool.submit(task, i).add_done_callback(func) # 朝线程池中提交任务(异步)
'''
异步回调机制
add_done_callback只要任务有结果了 就会自动调用括号内的函数处理
'''
''' 进程池演示代码 '''
from concurrent.futures import ProcessPoolExecutor
import time
import os
pool = ProcessPoolExecutor(4)
def task(m):
time.sleep(1)
print(m)
print(os.getpid())
return '程序运行结果%s' % (m ** 2)
def function(*args, **kwargs):
print(args[0].result())
if __name__ == '__main__':
for t in range(5):
pool.submit(task, t).add_done_callback(function)
'''
异步回调机制
add_done_callback只要任务有结果了 就会自动调用括号内的函数处理
'''
7 > 协程
协程其实是程序员自己相出来的一个名词,对于操作系统而言只认识进程和线程,协程其实就是自己通过代码来检测程序的IO操作并自己处理,让CPU感觉不到IO的存在从而大幅度的占用CPU。就类似于一个人同时干服务和接待客人的工作,在接待和服务之间来回切换。代码演示如下:
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(4)
print('%s eat 2' % name)
start_time = time.time()
g1 = spawn(play, 'bob')
g2 = spawn(eat, 'bob')
g1.join() # 等待检测任务执行完毕
g2.join() # 等待检测任务执行完毕
print('总耗时: ', time.time() - start_time)
8 > 基于协程实现TCP服务端并发
python可以通过开设多进程 在多进程下开设多线程 在多线程使用协程,从而让程序执行的效率达到极致!!!
但是实际业务中很少需要如此之高的效率(一直占着CPU不放),因为大部分程序都是IO密集型的,所以协程我们知道它的存在即可 几乎不会真正去自己编写。
'''服务端'''
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()
spawn(communication, sock)
g1 = spawn(get_server)
g1.join()
'''客户端'''
from threading import Thread, current_thread
import socket
def get_client():
client = socket.socket()
client.connect(('127.0.0.1', 8080))
count = 0
while True:
msg = '%s say hello %s'% (current_thread().name, count)
count += 1
client.send(msg.encode('utf8'))
data = client.recv(1024)
print(data.decode('utf8'))
for i in range(20):
t = Thread(target=get_client)
t.start()