进程、线程补充知识
GIL(全局解释器锁)与普通互斥锁
GIL存在的验证
from threading import Thread, Lock
money = 100
def task():
global money
money -= 1
for i in range(100):
t = Thread(target=task)
t.start()
print(money) # 0
同时创建很多100个线程,并且在每个线程里执行减1的操作,最终结果是0,这里就验证了,对于同一进程的多线程的执行是有先后顺序的,即有锁存在
不加锁情况下对数据进行操作
money = 100
def task():
global money
temp = money
time.sleep(0.1) # IO操作换线程
money = temp - 1
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money) # 99
对不同数据加锁
mutex = Lock()
money = 100
def task():
global money
mutex.acquire()
temp = money
time.sleep(0.1) # IO操作换锁
money = temp - 1
mutex.release()
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money) # 0
多线程的作用与用途
我们考虑该问题时应该了解一些理论前提
- CPU
1. 单CPU
2. 多CPU - 任务类型
1. 计算密集型
2. IO密集型
不同情况下的情况
单cpu
多IO密集型任务
多进程浪费资源
进程执行中遇到IO操作会直接将cpu切换到下个任务,所以导致多进程也无法利用多核优势
多线程节省资源
线程执行过程中只需要在进程中切换执行的线程(切换+保存状态)
多计算密集型任务
多进程:耗时较长 (创建进程耗时+切换进程耗时)
多线程:耗时较短 (切换线程耗时)
多cpu
多IO密集型任务
多进程浪费资源
只能利用某些个cpu,其余的利用不上
多线程节省资源(切换+保存状态)
多计算密集型任务
多进程速度更快(多cpu的计算优势)
多线程速度较慢
结论: 多线程多进程的应用场景不同。尤其是多线程,在某些场景也比较有用
代码验证
import os
import time
from threading import Thread
from multiprocessing import Process
print(os.cpu_count())
def work():
res = 1
for i in range(1, 100000):
res *= i
if __name__ == '__main__':
stime = time.time()
t_list = []
for i in range(4):
t = Thread(target=work)
# t = Process(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(time.time() - stime)
def work():
time.sleep(0.1) # 模拟纯 IO操作
if __name__ == '__main__':
stime = time.time()
t_list = []
for i in range(100):
t = Process(target=work)
# t = Thread(target=work)
t.start()
for t in t_list:
t.join()
print(time.time() - stime)
结果
计算密集型:
多进程:5.426685571670532
多线程:13.538886785507202
IO密集型
多进程: 4.05186653137207
多线程: 0.016954660415649414
结论
IO密集型多线程更节省资源,节省时间
死锁现象
from threading import Thread, Lock
import time
mutex1 = Lock()
mutex2 = Lock()
class Mythread(Thread):
def run(self):
self.f1()
self.f2()
def f1(self):
mutex1.acquire()
print('f1抢到了锁1')
mutex2.acquire()
print('f2抢到了锁2')
mutex2.release()
mutex1.release()
def f2(self):
mutex2.acquire()
print('f2抢到了锁2')
time.sleep(0.1)
mutex1.acquire()
print('f1抢到了锁1')
mutex1.release()
mutex2.release()
for i in range(5):
t = Mythread()
t.start()
当存在多把锁的时候,任务内如果采用不同的抢锁放锁的顺序,机会出现死锁现象。所以锁不能轻易使用,并且应该尽可能的少用。
信号量
信号量在不同的知识体系中,所代表的的含义是不同
并发编程:多把互斥锁
django框架:达到某个条件自动触发特定功能
就像进入一个房间可以通过多扇门进入
如果将自定义互斥锁比喻为一扇门
那么信号量就是指通往房间的多扇门
其实本质意义就是同时提供多把锁,一把锁同时只能被使用一次,放锁以后下一个才可以接着抢锁。但是着了由于存在多吧锁,所以可以支持同时好几个任务抢锁。
import random
import time
from threading import Thread, Semaphore
sp = Semaphore(5) # 创建一个五个门的房子
def task(name):
sp.acquire()
print(f'{name} is peeing')
time.sleep(random.randint(1, 3))
print('\n')
sp.release()
for i in range(50):
t = Thread(target=task, args=(i,))
t.start()
event事件
一些子线程需等待另些子线程运行结束才能运行(类似发射信号)
即某些子线程是否运行取决于其他子线程
from threading import Thread, Event
import time
event = Event() # 创建event事件
def light():
print('red')
time.sleep(5)
print('green')
event.set() # 设置为event事件的参照
def car(i):
print(i, 'red, wait')
event.wait() # 等待light执行后才会执行后面的代码
print(i, 'green go')
t = Thread(target=light)
t.start()
for i in range(10):
t = Thread(target=car, args=(i,))
t.start()
进程池与线程池
知识补充与回顾
- 补充知识:服务端必备三个特性
1. 24小时不间断提供服务
2. 固定的IP和PORT
3. 支持高并发 - 知识回顾:TCP服务端实现并发
1. 多进程:来一个客户端就开一个进程(临时工)
2. 多线程:来一个客户端就开一个线程(临时工) - 问题:物理硬件存在极限,不支持无限创建进程或者线程
问题的解决方案
- 池的概念
其实池的概念就是指CPU同时支持创建的进程或者线程的最大个数 - 池的目的
能够保证物理硬件不被损坏的情况下,提升程序的运行效率 - 池的使用
(1) 进程池:
提前创建好固定数量的进程 后续反复使用这些进程(合同工)
(2)线程池:
提前创建好固定数量的线程 后续反复使用这些线程(合同工)
(3)如果任务超出了池子里面的最大进程或线程数 则原地等待
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time
import os
from threading import current_thread
pool1 = ProcessPoolExecutor(3) # 创建一个最大进程数为3的进程池
pool2 = ThreadPoolExecutor(30) # 创建一个最大线程数为30的线程池
def task1(n):
time.sleep(1)
print(n)
# print('%s is running' % i)
# print(current_thread().name)
print('进程池', os.getpid())
return '1执行结果%s' % n
def task2(n):
time.sleep(1)
print(n)
# print('%s is running' % i)
# print(current_thread().name)
print('线程池', current_thread().name)
return '2执行结果%s' % n
def run(*args, **kwargs):
print(args[0].result())
if __name__ == '__main__':
for i in range(20):
# pool.submit(task, i) # 往进程池里提交任务
# print(res.result()) # 同步提交
res1 = pool1.submit(task1, i).add_done_callback(run) # 想进程池内提交任务并返回结果
'''异步回调机制:不应该主动等待结果,让异步提交自动提醒'''
# res2 = pool2.submit(task2, i).add_done_callback(run)
# print(res.result())
if __name__ == '__main__':
for i in range(100):
# pool.submit(task, i) # 往进程池里提交任务
# print(res.result()) # 同步提交
# res1 = pool1.submit(task1, i).add_done_callback(run)
'''异步回调机制:不应该主动等待结果,让异步提交自动提醒'''
res2 = pool2.submit(task2, i).add_done_callback(run) # 想线程池内提交任务并返回结果
# print(res.result())
协程
单线程下实现并发
切换+保存状态
协程是程序员意淫出来的,操作系统并不认识它
携程就是自己检测IO,并对其进行切换操作
协程的实现
from gevent import spawn
from gevent import monkey;monkey.patch_all() # 检测一切IO操作
import time
def play():
print('play 1')
time.sleep(4)
print('play 2')
def eat():
print('eat 1')
time.sleep(3)
print('eat 2')
stime = time.time()
g1 = spawn(play) # 检测任务play
g2 = spawn(eat) # 检测任务eat
g1.join()
g2.join()
print(time.time()-stime)
协程实现TCP并发
# 服务端
import socket
from gevent import spawn
from gevent import monkey;monkey.patch_all()
def get_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, addr = server.accept()
spawn(communicate, sock)
def communicate(sock):
data = sock.recv(1024)
print(data.decode('utf8'))
sock.send(b'hello')
s1 = spawn(get_server)
s1.join()
# 客户端
import socket
from threading import Thread
def get_client():
client = socket.socket()
client.connect(('127.0.0.1', 8080))
count = 0
msg = 'say hello back'
count += 1
client.send(msg.encode('utf8'))
data = client.recv(1024)
print(data.decode('utf8'))
for i in range(3000):
t = Thread(target=get_client)
t.start()
进程线程理论总结
终极结论
python可以通过开设多进程 在多进程下开设多线程 在多线程使用协程,从而让程序执行的效率达到极致!!!
但是实际业务中很少需要如此之高的效率(一直占着CPU不放)。因为大部分程序都是IO密集型的
所以协程我们知道它的存在即可 几乎不会真正去自己编写