昨日回顾
1 生产者消费者
-在生产者和消费者之间,通过队列,增加缓冲,避免了生产者和消费者之间交互
-Queue,redis,rabbitmq,kafka
-解耦合,队列是微服务的基础
2 线程理论,开启
-进程是资源分配的最小单位,线程是执行的最小单位(cpu调度的最小单位),每个进程中最少一个线程
-两种方式(跟进程完全类似)
3 join方法
-等待子线程执行结束,线程对象.join()
4 线程数据共享
-不同线程,变量是可以共用的,查看和修改(数据错乱)
5 线程对象其他方法
-name:人为设置,有默认
-ident:线程号
-active_count():现在还存活多少线程
-is_alive():此线程是否还存活
6 线程互斥锁
-不同线程要修改同一个数据,要加锁
-让并行变成串行,牺牲了效率,保证了数据安全
-悲观锁,乐观锁,分布式锁
7 GIL
-全局解释器锁:在cpython解释器内部有一把大锁,线程要执行,必须获取到这把锁
-为什么要有它? python的垃圾回收机制是线程不安全的,所有所有线程要抢到GIL才能执行
-cpython的多线程不是真正的多线程,同一时刻,只有一个线程在执行,不能利用多核优势
-----以下只针对于cpython解释器
-在单核情况下:
-开多线程还是开多进程?不管干什么都是开线程
-在多核情况下:
-如果是计算密集型,需要开进程,能被多个cpu调度执行
-如果是io密集型,需要开线程,cpu遇到io会切换到其他线程执行
今日内容
1 验证GIL锁的存在方式
from threading import Thread
from multiprocessing import Process
def task():
while True:
pass
if __name__ == '__main__':
for i in range(6):
# t=Thread(target=task) # 因为有GIL锁,同一时刻,只有一条线程执行,所以cpu不会满
t=Process(target=task) # 由于是多进程,进程中的线程会被cpu调度执行,6个cpu全在工作,就会跑满
t.start()
2 GIL与普通互斥锁的区别
1 GIL锁是不能保证数据的安全,普通互斥锁来保证数据安全
from threading import Thread, Lock
import time
mutex = Lock()
money = 100
def task():
global money
mutex.acquire()
temp = money
time.sleep(1)
money = temp - 1
mutex.release()
if __name__ == '__main__':
ll=[]
for i in range(10):
t = Thread(target=task)
t.start()
# t.join() # 这样会变成串行,不能这么做
ll.append(t)
for t in ll:
t.join()
print(money)
3 io密集型和计算密集型
-----以下只针对于cpython解释器
-在单核情况下:
-开多线程还是开多进程?不管干什么都是开线程
-在多核情况下:
-如果是计算密集型,需要开进程,能被多个cpu调度执行
-如果是io密集型,需要开线程,cpu遇到io会切换到其他线程执行
from threading import Thread
from multiprocessing import Process
import time
# 计算密集型
def task():
count = 0
for i in range(100000000):
count += i
if __name__ == '__main__':
ctime = time.time()
ll = []
for i in range(10):
t = Thread(target=task) # 开线程:42.68658709526062
# t = Process(target=task) # 开进程:9.04949426651001
t.start()
ll.append(t)
for t in ll:
t.join()
print(time.time()-ctime)
# io密集型
def task():
time.sleep(2)
if __name__ == '__main__':
ctime = time.time()
ll = []
for i in range(400):
t = Thread(target=task) # 开线程:2.0559656620025635
# t = Process(target=task) # 开进程:9.506720781326294
t.start()
ll.append(t)
for t in ll:
t.join()
print(time.time()-ctime)
4 死锁现象(哲学家就餐问题)
# 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
# 单例模式:https://www.cnblogs.com/liuqingzheng/p/10038958.html
# 死锁现象,张三拿到了A锁,等B锁,李四拿到了B锁,等A锁
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
def eat_apple(name):
mutexA.acquire()
print('%s 获取到了a锁' % name)
mutexB.acquire()
print('%s 获取到了b锁' % name)
print('开始吃苹果,并且吃完了')
mutexB.release()
print('%s 释放了b锁' % name)
mutexA.release()
print('%s 释放了a锁' % name)
def eat_egg(name):
mutexB.acquire()
print('%s 获取到了b锁' % name)
time.sleep(2)
mutexA.acquire()
print('%s 获取到了a锁' % name)
print('开始吃鸡蛋,并且吃完了')
mutexA.release()
print('%s 释放了a锁' % name)
mutexB.release()
print('%s 释放了b锁' % name)
if __name__ == '__main__':
ll = ['egon', 'alex', 'tie_dan']
for name in ll:
t1 = Thread(target=eat_apple, args=(name,))
t2 = Thread(target=eat_egg, args=(name,))
t1.start()
t2.start()
# 递归锁(可重入),同一个人可以多次acquire,每acquire一次,内部计数器加1,每relaese一次,内部计数器减一
# 只有计数器不为0,其他人都不获得这把锁
from threading import Thread, Lock,RLock
import time
# 同一把锁
# mutexA = Lock()
# mutexB = mutexA
# 使用可重入锁解决(同一把锁)
# mutexA = RLock()
# mutexB = mutexA
mutexA = mutexB =RLock()
def eat_apple(name):
mutexA.acquire()
print('%s 获取到了a锁' % name)
mutexB.acquire()
print('%s 获取到了b锁' % name)
print('开始吃苹果,并且吃完了')
mutexB.release()
print('%s 释放了b锁' % name)
mutexA.release()
print('%s 释放了a锁' % name)
def eat_egg(name):
mutexB.acquire()
print('%s 获取到了b锁' % name)
time.sleep(2)
mutexA.acquire()
print('%s 获取到了a锁' % name)
print('开始吃鸡蛋,并且吃完了')
mutexA.release()
print('%s 释放了a锁' % name)
mutexB.release()
print('%s 释放了b锁' % name)
if __name__ == '__main__':
ll = ['egon', 'alex', 'tie_dan']
for name in ll:
t1 = Thread(target=eat_apple, args=(name,))
t2 = Thread(target=eat_egg, args=(name,))
t1.start()
t2.start()
5 Semaphore信号量
# Semaphore:信号量可以理解为多把锁,同时允许多个线程来更改数据
from threading import Thread,Semaphore
import time
import random
sm=Semaphore(3) # 数字表示可以同时有多少个线程操作
def task(name):
sm.acquire()
print('%s 正在厕所位'%name)
time.sleep(random.randint(1,5))
sm.release()
6 Event事件
# 一些线程需要等到其他线程执行完成之后才能执行,类似于发射信号
# 比如一个线程等待另一个线程执行结束再继续执行
# 一些线程需要等到其他线程执行完成之后才能执行,类似于发射信号
# 比如一个线程等待另一个线程执行结束再继续执行
from threading import Thread, Event
import time
event = Event()
def girl(name):
print('%s 现在不单身,正在谈恋爱'%name)
time.sleep(10)
print('%s 分手了,给屌丝男发了信号'%name)
event.set()
def boy(name):
print('%s 在等着女孩分手'%name)
event.wait() # 只要没来信号,就卡在者
print('女孩分手了,机会来了,冲啊')
if __name__ == '__main__':
lyf = Thread(target=girl, args=('刘亦菲',))
lyf.start()
for i in range(10):
b = Thread(target=boy, args=('屌丝男%s号' % i,))
b.start()
# 作业:起两个线程,第一个线程读文件的前半部分,读完发一个信号,另一个进程读后半部分,并打印
from threading import Thread, Event
import time
import os
event = Event()
# 获取文件总大小
size = os.path.getsize('a.txt')
def read_first():
with open('a.txt', 'r', encoding='utf-8') as f:
n = size // 2 # 取文件一半,整除
data = f.read(n)
print(data)
print('我一半读完了,发了个信号')
event.set()
def read_last():
event.wait() # 等着发信号
with open('a.txt', 'r', encoding='utf-8') as f:
n = size // 2 # 取文件一半,整除
# 光标从文件开头开始,移动了n个字节,移动到文件一半
f.seek(n, 0)
data = f.read()
print(data)
if __name__ == '__main__':
t1=Thread(target=read_first)
t1.start()
t2=Thread(target=read_last)
t2.start()
7 线程queue
# 进程queue和线程不是一个
# from multiprocessing import Queue
# 线程queue
from queue import Queue,LifoQueue,PriorityQueue
# 线程间通信,因为共享变量会出现数据不安全问题,用线程queue通信,不需要加锁,内部自带
# queue是线程安全的
'''
三种线程Queue
-Queue:队列,先进先出
-PriorityQueue:优先级队列,谁小谁先出
-LifoQueue:栈,后进先出
'''
如何使用
q=Queue(5)
q.put("lqz")
q.put("egon")
q.put("3号")
q.put("4号")
q.put("5号")
# q.put("银dan")
# q.put_nowait("银dan")
# 取值
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
# 卡住
# print(q.get())
# q.get_nowait()
# 是否满,是否空
print(q.full())
print(q.empty())
LifoQueue
q=LifoQueue(5)
q.put("lqz")
q.put("egon")
q.put("3号")
q.put("4号")
q.put("5号")
#
# q.put("ddddan")
print(q.get())
PriorityQueue:数字越小,级别越高
q=PriorityQueue(3)
q.put((-10,'1号'))
q.put((100,'2号'))
q.put((101,'3号'))
# q.put((1010,'铁dddan')) # 不能再放了
print(q.get())
print(q.get())
print(q.get())
8 线程池
1 为什么会出现池?不管是开进程还是开线程,不能无限制开,通过池,假设池子里就有10个,不管再怎么开,永远是这10个
2 如何使用
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(2)
pool.submit(get_pages, url).add_done_callback(call_back)
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from threading import Thread
import time
import random
pool = ThreadPoolExecutor(5) # 数字是池的大小
# pool = ProcessPoolExecutor(5) # 数字是池的大小
def task(name):
print('%s任务开始' % name)
time.sleep(random.randint(1, 4))
print('任务结束')
return '%s 返回了'%name
def call_back(f):
# print(type(f))
print(f.result())
if __name__ == '__main__':
# ll=[]
# for i in range(10): # 起了100个线程
# # t=Thread(target=task)
# # t.start()
# res = pool.submit(task, '屌丝男%s号' % i) # 不需要再写在args中了
# # res是Future对象
# # from concurrent.futures._base import Future
# # print(type(res))
# # print(res.result()) # 像join,只要执行result,就会等着结果回来,就变成串行了
# ll.append(res)
#
# for res in ll:
# print(res.result())
# 终极使用
for i in range(10): # 起了100个线程
# 向线程池中提交一个任务,等任务执行完成,自动回到到call_back函数执行
pool.submit(task,'屌丝男%s号' % i).add_done_callback(call_back)
from concurrent.futures import ThreadPoolExecutor
import requests # 爬虫会学到的模块
pool = ThreadPoolExecutor(2)
def get_pages(url):
# https://www.baidu.com
res = requests.get(url) # 向这个地址发送请求
name = url.rsplit('/')[-1] + '.html'
print(name) # www.baidu.com.html
# res.content拿到页面的二进制
return {'name': name, 'text': res.content}
def call_back(f):
dic = f.result()
with open(dic['name'], 'wb') as f:
f.write(dic['text'])
if __name__ == '__main__':
ll = [这里面放网站链接]
for url in ll:
pool.submit(get_pages, url).add_done_callback(call_back)
9 进程池
1 如何使用
from concurrent.futures import ProcessPoolExecutor
pool = ProcessPoolExecutor(2)
pool.submit(get_pages, url).add_done_callback(call_back)
============================其余补充===================================
进程 与 线程 的数据 比较:
不同的 进程,变量 不能共用, 仅可以查看主进程中的变量,但是不能修改
线程的数据(即 主线程中的变量)共享: 不同线程,变量是可以共用的,查看和修改(数据错乱)
多线程 与 多进程 的区别:
1、开启多个进程时:
父进程有一个pid, 每一个子进程各有一个pid
即
创建一个 父进程,
创建若干个 子进程
2、开启多个线程时:
所有的线程(主线程 、子线程) 只有一个pid
即
创建一个新进程,
盛放 所有的所有的线程
对于cpython解释器:
多线程:程序只能在 单 个cpu上, 并发 执行
多进程:程序可在 多 个cpu上, 并行 执行
----应用: 在各种情况下,究竟是开 多进程 or 多线程
对于 单 核计算机:
由于在一个cpu上都只能进行并发执行,并且线程的开销比进程更小,
所以程序无论是干什么,都是开多线程
开多线程还是开多进程?不管干什么都是开线程
对于 多 核计算机:
对于 计算密集型程序: 需要开 多进程, 能被放在多个cpu上调度执行
对于 I/O密集型程序: 由于 存在着频繁的I/O阻塞, 为了系统资源的最大化,只需要开 多线程, 放在一个cpu上进行并发执行即可。
如果使用多进程方式, 系统资源消耗更大, 运行所需时间反而会更长,因为 cpu频繁调度进程时,会耗费大量的时间,用来调度进程的资源
解释性的语言, 线程是 不安全的
因为是边解释边执行的,不知道当前数据是否是之后用到的
所以使用了GIL锁,用于将当前的程序中的所有数据都固定住,不被垃圾回收机制回收掉
编译型的语言, 线程 是安全的
因为一次性编译之后,知道哪些数据是需要用到的,对于用不到的则进行回收
乐观锁 与 悲观锁:
悲观锁:能保证第一个修改成功
从一开始,就认为本次 有其他人对数据进行修改,所以一开始就进行加锁
乐观锁:不能保证修改成功
从一开始,就认为本次 没有数据进行修改,所以一开始就不进行加锁,
但当真正要改的时候,如果发现数据被改动了,则取消本次修改
课堂例题中:
上面一个 for i in range(10):
...
是完完全全的串行
下面一个 for t in l1: 变成了并行
此时没有sleep时,
并行之后就需要,用到锁
死锁问题: 互相等待对方手中的资源
多线程时:
由于多个线程只能在一个cpu上执行,
一个cpu不是执行它,就是执行另一个,
所以一旦,双方都需要对方手中的资源,则就会陷入互相等待的局面
递归锁: 把并发的程序, 变成了 串行的程序
Event事件
event.wait() # 当前需要等待事件发生,然后再进行下一步
evert.set() # 当前 事件发生,可以进行之后的操作
三种线程Queue
-Queue:队列,先进先出
-PriorityQueue:优先级队列,谁小谁先出
# q=PriorityQueue(3)
优先级队列特点: 放入值的时候,需要指定优先级
# q.put((-10,'金蛋'))
-LifoQueue:栈,后进先出队列,后进先出
queue注意点:
# 进程 中的队列 queue (进程queue和线程不是一个)
from multiprocessing import Queue
# 线程 queue 是安全的
from queue import Queue,LifoQueue,PriorityQueue
# 线程之间通信,因为共享变量会出现数据不安全问题
# 用线程queue,不需要加锁,内部自带; 线程queue是安全的
进程 和 线程中都有, Queue
但是 他们的Queue不是同一个东西, 不能混着用
使用并发的 tcp方式 的socket通信
以下代码需要写在if __name__ == '__main__':的里面,否则每当开启一个进程的时候,
__main__ 上面的所有代码,就会被搬运在一个新文件中执行一次,而为服务端指定的ip:端口 是固定的,所以就会出现当前的ip:端口 被占用的报错
使用万能异常的时候,不光要保证即使有错,还能继续执行
还要能看到程序究竟出现了什么错误,因此以后应按照下面这种方式进行书写,即出现了异常 也要打印出异常
except Exception as e:
print(e)
break
客户端如果发送一个内容固定的字符串时,也可以使用这种方式,即 直接将字符串内容指定为 utf-8编码的byte类型
client.send(b'hello world')
线程池 、进程池 要点:
1.
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
process_pool = ProcessPoolExecutor( pool_size ) # 进程池
thread_pool = ThreadPoolExecutor( pool_size ) # 线程池
2. 普通方式开启进程/线程,无法获得task中的返回值
{
t=Thread(target=task, args=(f'屌丝男 {i} 号', ))
t.start()
仅仅是效果 等效于
res=pool.submit(task, f'屌丝男 {i} 号' )
这里的res内容: <Future at 0x26367ad1d00 state=running>
<Future at 0x26367ae9d00 state=pending>
3. 多进程/多线程 中的任务 带返回值时: 通过 call_back(f) 方法,获取每个进程的结果
{
向线程池中提交一个任务,等任务执行完成,自动回到call_back函数执行
pool.submit(task,f'屌丝男 {i}号').add_done_callback(call_back) # 启动进程 、把进程中任务的结果返回
call_back函数自行定义:
def call_back(f):
# print(type(f)) # 观察其 类型
print(f.result())
}
5.
{
第一种:
res=pool.submit(task, f'屌丝男 {i} 号' )
print(res.result()) # 像join,只要执行result,就会等着结果回来,就变成串行了
表面上类似于下面这种形式,不过上面这种 就变成 串行的了,而下面的则仍然是并发
第二种:
pool.submit(task, f'屌丝男 {i} 号' ).add_done_callback(call_back)
仍然是并发运行各个程序(不是串行!!! 和原来无返回值一样),
并且在运行完之后, task返回的结果 就 传给了call_back, 在call_back中可以对返回的结果进行处理
}
==============================================
GIL锁(即 解释器的锁, 一次只能让一个线程获得运行的权限,在解释器上运行)
加自己的mutex锁,使得数据一次完整的操作,只能是一个线程进行
正常情况,都先释放后获得的锁
可重入: 一个人可以多次获得同一把锁
sm 用于 限制同时 用厕所的人的个数
使用共享变量,会出现数据错乱的问题 所以推荐使用 队列
windows中 开多进程时,一定要把所有的代码都写到 ...main... 中
将所有的 进程/线程对象t,都装进一个列表中,目的:
为了这些线程都执行完, 最终查看 外部变量的结果
# 开辟相同数量的 进程 和 线程,进程的花销比线程要大
8-26-GLI锁与普通互斥锁、死锁问题、递归锁、信号量、Event事件、并发的tcp通信、进程池线程池
最新推荐文章于 2023-07-20 09:05:26 发布