线程实际操作篇
并发编程之多线程
一 threading 模块
multiprocessing 模块 完全模仿了 threading 模块的接口 , 二者在使用层面. 有很大的相似性. 使用起来就是换了不同的模块而已
二 开启线程的俩种方式
方式一: 通过Thread这个模块直接实例化一个线程对象
from threading import Thread
import time, os
n = 100
def task():
print(f'{os.getpid()} is running')
print(f'{current_thread().name} is run') # Thread-1 is run
global n
n = 666
if __name__ == '__main__':
t = Thread(target=task)
t.start()
print('主线程', os.getpid())
print('主线程', current_thread().name) # 主线程 MainThread
t.join() # 虽然线程的开启到执行很快, 但是为了保证打印出的n是一个线程执行完后的结果# 保证线程一定执行完了
print('主', n) # 主 666
方式二: 通过自定义的类, 来继承 Thread这个类
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self) -> None:
print(f'{self.name}线程开启')
if __name__ == '__main__':
t1 = MyThread('线程1')
t1.start() # 线程1线程开启
print('主线程结束') # 主线程结束
开启线程的分支图
三. 在一个进程下开启多个线程与在一个进程下开启多个子进程的区别
3.1 谁开启的速度快
看看下面的例子
# -*- coding: utf-8 -*-
# @Author : JKey
# Timer : 2021/1/21 12:16
from threading import Thread
from multiprocessing import Process
import time
def work():
print('hello')
当是开启子线程时
if __name__ == '__main__':
t = Thread(target=work)
s = time.time()
t.start()
t.join()
print(time.time() - s)
"""
hello
0.0014333724975585938
"""
当是开启子进程时
if __name__ == '__main__':
p = Process(target=work)
s = time.time()
p.start()
p.join()
print(time.time() - s)
"""
hello
0.10881352424621582
"""
俩个时间相差了 80 来倍
3.2 看看他们的pid有什么变化
from threading import Thread
from multiprocessing import Process
import time, os
def check_pid():
print(f'子pid为{os.getpid()}')
time.sleep(0.1)
if __name__ == '__main__':
t = Thread(target=check_pid)
t.start()
print(f'主的{os.getpid()}')
""" 当是开启子线程的时候
子pid为10004
主的10004
"""
p = Process(target=check_pid)
p.start()
print(f'主的{os.getpid()}')
""" 当是开启子进程的时候
主的10004
子pid为772
"""
可以发现,子线程的pid和主线程的pid是一致的
而子进程的pid和主进程的pid是不一样的
3.3 同一进程内的线程共享该进程的数据
from threading import Thread
from multiprocessing import Process
import os
def work():
global n
n=0
if __name__ == '__main__':
n=100
# ------ 开启子进程 ------
p=Process(target=work)
p.start()
p.join()
print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
# ------ 开启子线程 ------
n=1
t=Thread(target=work)
t.start()
t.join()
print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
我们可以发现 子进程内改的仅仅是子进程内的n 的值, 而打印的主进程的n的值还是100
而子线程改的就是主线程的n 所以打印的就是0
四 练习
练习一: 多线程并发的socket通信
服务端
# -*- coding: utf-8 -*-
# @Author : JKey
# Timer : 2021/1/21 17:58
from socket import *
from threading import Thread
server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)
def run(conn, client_addr):
print(f"{client_addr[1]}连接成功")
while True:
try:
msg = conn.recv(1024)
if len(msg) == 0: break
print(f"收到{client_addr[1]}的信息,信息为:[{msg.decode('utf-8')}]")
conn.send(msg.upper())
except Exception:
break
if __name__ == '__main__':
while True:
conn, client_addr = server.accept()
t = Thread(target=run, args=(conn, client_addr))
t.start()
客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))
while True:
msg = input('请输入发送的内容>>>:').strip()
if len(msg) == 0: continue
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
练习二
三个任务,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
服务端
# -*- coding: utf-8 -*-
# @Author : JKey
# Timer : 2021/1/21 17:58
from socket import *
from threading import Thread
server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)
msg = b''
def server_recv(conn, client_addr):
print(f"{client_addr[1]}连接成功")
while True:
try:
global msg
# msg = b""
msg = conn.recv(1024)
if len(msg) == 0: break
print(f"收到{client_addr[1]}的信息,信息为:[{msg.decode('utf-8')}]")
except Exception:
break
def server_upper(conn):
# import time
while True:
global msg
msg = msg.upper()
# time.sleep(0.001)
# conn.send(msg)
def save():
while True:
global msg
with open('user.txt', 'a', encoding='utf-8') as f:
if len(msg) == 0:
continue
f.write(f"{msg.decode('utf-8')}\n")
print('保存信息成功')
msg = b""
if __name__ == '__main__':
while True:
conn, client_addr = server.accept()
t = Thread(target=server_recv, args=(conn, client_addr))
t2 = Thread(target=server_upper, args=(conn, ))
t3 = Thread(target=save)
t.start()
t2.start()
t3.start()
客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))
while True:
msg = input('请输入发送的内容>>>:').strip()
if len(msg) == 0: continue
client.send(msg.encode('utf-8'))
# data = client.recv(1024)
# print(data.decode('utf-8'))
线程相关的其他方法
Thread 实例对象的方法
isAlive() : 返回线程是否是活动的
getName() 返回线程名
setNmae() 设置线程名
threading 模块 提供的一些方法
threading.enumerate() 返回一个包含正在运行的线程的list. 正在运行指线程启动后,结束前,不包括 启动前和终止后的线程
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
代码示范
from threading import Thread
import time, os
import threading
def work():
time.sleep(3)
print(threading.current_thread().getName())
if __name__ == '__main__':
# 在主进程下开启线程
t = Thread(target=work)
t.start()
print(threading.current_thread().getName())
print(threading.current_thread())
print(threading.enumerate())
print(threading.active_count())
print('主线程')
""" 输出为
MainThread
<_MainThread(MainThread, started 6828)>
[<_MainThread(MainThread, started 6828)>, <Thread(Thread-1, started 1596)>]
2
主线程
Thread-1
"""
主进程等待子进程结束
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' % name)
if __name__ == '__main__':
t = Thread(target=sayhi, args=('jkey',))
t.start()
t.join()
print('主线程')
print(t.is_alive())
""" 输出为
jkey say hello
主线程
False
"""
守护线程
无论是进程还是线程,都循环: 守护 xxx 会等待 主xxx 运行完毕后被销毁.
需要强调的是:运行完毕并非终止运行
守护进程和守护线程的区别
- 对于主进程来说, 运行完毕指的是主进程代码运行完毕
- 对主线程来说, 运行完毕 指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕.
- 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然而主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束
- 主线程在其他非守护线程运行完毕后才算运行完毕()守护线程在此时就被回收.因为主线程的结束意味着进程的结束.进程整体的资源都将被回收,而进程必须保证非守护线程运行完毕后才能结束.
代码示范
from threading import Thread,current_thread
import time
def task(n):
print("%s is running" %current_thread().name)
time.sleep(n)
print("%s is end" %current_thread().name)
if __name__ == '__main__':
t1 = Thread(target=task,args=(3,))
t2 = Thread(target=task,args=(5,))
t3 = Thread(target=task,args=(100,))
t3.daemon = True
t1.start()
t2.start()
t3.start()
print("主") # 5秒后就结束了主线程. 即进程的资源就会关闭了. 不会等到守护线程也执行完毕
from threading import Thread
import time
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(3)
print("end456")
t1 = Thread(target=foo)
t2 = Thread(target=bar)
t1.daemon = True
t1.start()
t2.start()
print("main-------")
""" 输出为
123
456
main-------
end123
end456
"""
解析: 因为主线程在执行到 t1.start()的时候会很快就去执行子线程 foo,所有会立马打印123,t2子线程也一样,打印出456. 然后主线程要等到非守护子线程t2运行完毕才会结束生命周期,而t2运行完毕要3秒.这个期间已经够t1运行完毕了.所以打印出的是123,456,mian------,end123,end456. 这就更加足以证明守护线程守护的是主线程的生命周期.而非代码执行完毕.
死锁和递归锁
进程和线程都有死锁与递归锁
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self) -> None:
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(0.1) # 模拟处理任务 IO
mutexA.acquire()
print(f'{self.name} 抢到了 A锁')
mutexA.release()
mutexB.release()
if __name__ == '__main__':
t1 = MyThread('线程1')
t2 = MyThread('线程2')
t3 = MyThread('线程3')
t4 = MyThread('线程4')
t1.start()
t2.start()
t3.start()
t4.start()
print('主')
"""
线程1 抢到了 A锁
线程1 抢到了 B锁
线程1 抢到了 B锁
线程2 抢到了 A锁
主
"""
这个时候,会存在死锁的情况
分析:
线程1先去执行了f1 执行到抢A锁的时候 还没有人竞争, 线程1先拿到了 A锁 然后打印 抢到了A锁 这个时候, 别的线程可能在这个时候也起来了.但是这个时候它们都会去抢A锁, A锁在线程1那,它们必须等线程1这把锁释放出来.
然后线程1 又去抢B锁了, B 锁也是没人竞争的, 然后线程1打印了抢到了 B锁, 然后释放掉了B锁,又释放掉了A锁
这时候A锁可能就会被其他的线程抢到,案例中就被线程2抢到了.但是线程1不会停,他在释放完A锁后就去执行f2了,又去抢B锁去了,因为线程2抢到了A锁,其他线程必须等待A锁的释放才能执行,然后打印抢到了B锁接着往下睡了0.1s,这一秒钟线程2也是在执行的,他执行到要B锁了,但是现在的B锁在线程1那,而线程1还是睡,然后就等了这一秒,然后线程1睡醒了要去抢A锁了,但是A锁现在又在线程2手上.这时候就出现了死锁可,线程2要线程1手上的B锁,线程1要线程2手上的A锁,它们谁也不让,那就会出现阻塞的情况
解决方案,采用递归锁
在python中为了支持同一线程中多次请求同一资源,python 提供了可重入锁 RLock.
这个RLock内部维护着一个Lock和一个counter变量, counter 记录了 acquire 的次数, 从而使得资源可以被多次require.直到一个线程所有的acquire都被release,其他的线程才能获得资源, 上面的例子,如果使用RLock代替了Lock,就不会发送死锁的情况.
from threading import Thread, RLock
import time
mutexA = mutexB = RLock() # 其实是一把锁,但是有俩个名称
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self) -> None:
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(0.1) # 模拟处理任务 IO
mutexA.acquire()
print(f'{self.name} 抢到了 A锁')
mutexA.release()
mutexB.release()
if __name__ == '__main__':
t1 = MyThread('线程1')
t2 = MyThread('线程2')
t3 = MyThread('线程3')
t4 = MyThread('线程4')
t1.start()
t2.start()
t3.start()
t4.start()
print('主')
输出
"""
线程1 抢到了 A锁
线程1 抢到了 B锁
线程1 抢到了 B锁
主
线程1 抢到了 A锁
线程2 抢到了 A锁
线程2 抢到了 B锁
线程2 抢到了 B锁
线程2 抢到了 A锁
线程4 抢到了 A锁
线程4 抢到了 B锁
...
"""
分析
因为这个时候的锁可被多次acquire了,当线程1acquire了一次A锁,计数+1, 别的线程一看这个锁的计数不为0,就继续等待,这个时候线程1有acquire了一次B锁,计数又加1 变为 2 了,别人就更抢不到了.当又释放了B和A,计数又变为0了,这时候别人才可以抢这个锁.所以不会出现死锁的情况.
信号量
同进程一样
Semaphore 管理一个内置的计数器
每当调用acquire() 时内置计数器 -1;
调用release() 时内置计数器+1
计数器不能小于0, 当计数器为0时,acquire将阻塞线程直到其他线程调用release().
实例: 同时只有5个线程可以获得semaphore, 即 可以限制最大连接数为5
# -*- coding: utf-8 -*-
# @Author : JKey
# Timer : 2021/1/22 10:24
from threading import Thread, Semaphore, current_thread
import time
import random
def func():
sm.acquire() # 每一个线程都来抢钥匙,最多五个线程抢到,锁和钥匙是5对5的
print('%s 正在蹲坑' % current_thread().getName())
time.sleep(random.randint(1, 3))
print('%s 走人了 ' % current_thread().getName())
sm.release()
if __name__ == '__main__':
sm = Semaphore(5) # 设置一个信号量, 最大的限制为5
for i in range(1, 23):
t = Thread(target=func)
t.start()
Event 事件
python 线程的事件用于 主线程控制其他线程的执行,事件主要提供了四个方法, set, wait, clear,is_set
线程的一个关键特性是每个线程都是独立运行且状态不可预测,如果程序中的其他线程需要通过某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手,为了解决这些问题,我们需要使用 threading库中的Event对象, 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
"""
event.set() 设置event的状态值为 True, 所有阻塞池的线程激活进入就绪状态,等待操作系统调度
event.wait() 等待event的状态为True的时候,才往下执行,当不是True的时候,就一直原地等待
event.clear() 设置event的状态为False
event.is_set() 查看当前的event的状态 只有True和False俩种状态
"""
如图
例如,有多个 工作线程尝试连接mysql.我们想要在链接前保证mysql服务正常才让那些工作线程去链接mysql服务器, 如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作
from threading import Event, Thread, current_thread
e = Event()
def check_mysql():
"""查看检测mysql"""
print('正在检测mysql', e.is_set())
import time
time.sleep(1)
e.set()
def conn_mysql():
count = 0
while count < 3:
print(f'{current_thread().name} 正在第{count}尝试连接...')
e.wait(0.5)
if e.is_set():
print(f'{current_thread().name} 连接成功')
break
count += 1
else:
print(f'{current_thread().name} 连接超时')
if __name__ == '__main__':
t1 = Thread(target=check_mysql)
t2 = Thread(target=conn_mysql)
t1.start()
t2.start()
""" 输出结果
正在检测mysql False
Thread-7 正在第0尝试连接...
Thread-7 正在第1尝试连接...
Thread-7 连接成功
"""
例子2: 模拟红绿灯
from threading import Event, Thread, current_thread
import time, random
e = Event()
def task():
while True:
e.clear()
print('\033[1;31m红灯亮,请等待\033[0m')
count = 15
while count > 0:
print(f'\033[1;31m倒计时{count}\033[0m')
time.sleep(1)
count -= 1
e.set()
print('\033[1;36m绿灯亮了,请通行\033[0m')
count1 = 5
while count1 > 0:
print(f'\033[1;36m倒计时{count1}\033[0m')
time.sleep(1)
count1 -= 1
def task1():
while True:
if e.is_set():
print(f'{current_thread().name} 开始通行')
break
else:
print(f'{current_thread().name} 开始等红灯')
e.wait()
if __name__ == '__main__':
Thread(target=task).start()
while True:
time.sleep(random.randint(1, 2))
Thread(target=task1).start()
这里可以灵活的使用这个全局的事件的状态来控制其他的线程的进行.
小知识
打印输出不同的颜色
Python:print显示颜色
书写格式:
开头部分:\033[显示方式;前景色;背景色m + 结尾部分:\033[0m
解释:
- 开头部分的三个参数:显示方式,前景色,背景色是可选参数,可以只写其中的某一个;
- 由于表示三个参数不同含义的数值都是唯一的没有重复的,所以三个参数的书写先后顺序没有固定要求,系统都能识别;
- 建议按照默认的格式规范书写。
- 对于结尾部分,其实也可以省略,但是为了书写规范,建议\033[***开头,\033[0m结尾。
数值表示的参数含义:
显示方式: 0(默认\)、1(高亮)、22(非粗体)、4(下划线)、24(非下划线)、 5(闪烁)、25(非闪烁)、7(反显)、27(非反显)
前景色: 30(黑色)、31(红色)、32(绿色)、 33(黄色)、34(蓝色)、35(洋 红)、36(青色)、37(白色)
背景色: 40(黑色)、41(红色)、42(绿色)、 43(黄色)、44(蓝色)、45(洋 红)、46(青色)、47(白色)
条件 Condition 了解
使得线程等待,只有满足某条件时,才释放n个线程
案例,根据用户输入,运行对应的个数的线程
import threading
def run(n):
con.acquire()
con.wait()
print(f"run the thread {n}")
con.release()
if __name__ == '__main__':
con = threading.Condition()
for i in range(10):
t = threading.Thread(target=run, args=(i, ))
t.start()
while True:
inp = input('>>>>').strip() # 控制释放的线程的个数 次案例最多线程为10,超出无效
if inp == 'q': break
if not inp.isdigit() or int(inp) > 10:
print('请输入合法整数,最多10个线程')
continue
con.acquire()
con.notify(int(inp))
con.release()
案例2: 输入1 就去执行一个线程
import threading
def condition_func():
ret = False
inp = input(">>>>:").strip() # 输入1 就去执行一个线程
if inp:
if inp == '1':
ret = True
return ret
else:
return
def run(n):
con.acquire()
con.wait_for(condition_func)
print(f'run the thread: {n}')
con.release()
if __name__ == '__main__':
con = threading.Condition()
for i in range(1, 11):
t = threading.Thread(target=run, args=(i,))
t.start()
定时器
概念: 指定n秒后执行某个操作
例如
from threading import Timer
import os
print(os.cpu_count()) # 查看cpu的核有几个
def f1(name):
print(f"{name} is run")
t = Timer(3, f1, args=('3秒',))
t.start()
"""
8
3秒 is run
"""
验证码定时器
# 验证码定时器
from threading import Timer
import random, time
class Code:
def __init__(self):
self.make_cache()
def make_cache(self, interval=60):
self.cache = self.make_code()
print(self.cache)
self.t = Timer(interval, self.make_cache)
self.t.start()
def make_code(self, n=4):
res = ''
for i in range(n):
s1 = str(random.randint(0, 9))
s2 = chr(random.randint(65, 90))
s3 = chr(random.randint(97, 121))
res += random.choice([s1, s2, s3])
return res
def check(self):
while True:
inp = input(">>>: ").strip()
if inp.lower() == self.cache.lower():
print('验证成功', end='\n')
self.t.cancel()
break
if __name__ == '__main__':
obj = Code()
obj.check()
线程queue
queue队列: 使用 import queue , 用法与进程Queue一样
当必须在多个线程之间安全地交换信息时,队列在线程编程中特别有用。
可以被分为三大类介绍
1 队列
概念: 先进先出
class queue.Queue(maxsize=0) # 先进先出
import queue
q = queue.Queue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
"""
结果:先进先出
1
2
3
"""
2 堆栈
特点: 后进先出
class queue.LifoQueue(maxsize=0) # last in first out
# 堆栈
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
"""
输出结果:
3
2
1
"""
3 设置优先级
特点: 优先级越高,取出的就越快
class queue.PriorityQueue(maxsize=0) # 存储数据时,可设置优先级的队列
import queue
# 设置优先级
q = queue.PriorityQueue()
q.put([10, '111']) # 存放的值为一个可迭代的对象即可, 第一个元素放的是优先级的大小,为一个数字 越小优先级越高,越早被取出
q.put([88, '222'])
q.put([-11, '333'])
print(q.get())
print(q.get())
print(q.get())
"""
输出为:
[-11, '333']
[10, '111']
[88, '222']
"""
GIL全局解释器锁
1 介绍
"""
在CPython中,全局解释器锁(global interpreter lock,或GIL)是一个防止多个
一次执行Python字节码的本机线程。这把锁存在是必要的
因为CPython的内存管理机制不是线程安全的。(然而,自从有了GIL之后 其他功能已发展到依赖于它所实施的保证。)
内存管理机制 = 垃圾回收机制 gc
- 1. 引用计数
- 2. 标记清除
- 3. 分代回收
"""
# 结论: 在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势.
首先要明确的一点是,python中可以分为好几种解释器,有Cpython,Jpython,pypypython...
GIL是Cpython的特性,它是在实现Cpython解释器时所引入的概念. Jpython就没有这个GIL,但是因为现在主流的python解释器就是Cpython,所以就会导致很多人认为GIL是python语言的缺陷,所以这里要先明确表明一点:GIL并不是python的特性,python完全可以不依赖于GIL.
2 GIL介绍
GIL的本质其实就是一把互斥锁, 既然是互斥锁,所有互斥锁的本质都是一样的,都是将并发运行变成了串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全.
但是可以肯定的是保护不同的数据就应该使用不同的锁来对其进行保护.
在一个python文件的进程内, 不仅有test.py 的 主线程 或者 由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程, 总之, 所有线程都运行在这一个进程内.
大概的流程图为
我们可以将 解释器当作一个功能 而每个线程 , 也就是里面的代码就是, 这个功能的参数, 然后解释器再根据它的语法和功能执行实现不同的功能.
那我们现在验证一下,为什么说垃圾回收机制是不安全的.
举个例子. 线程1,线程2,线程3 它们执行的代码就是 a=1 print(a)
那有没有可能会出现这样一个情况? 我线程1 去 执行 a=1 ,就先开辟了一个内存空间, 将值1 放进去, 但是我现在还没有对其进行绑定,即我还没有绑定给a 这个名称, 这个时候 垃圾回收线程启动了, 它拿到这个值为1的名称空间一看.计数为0,它就直接给你回收了,那这时候,线程1再想绑定,我靠,值没了.这就懵逼了.
就是相当于 你 在吃饭 ,突然觉得有点渴了, 然后你就去买瓶水,那服务员一看,卧槽那没人了,就给你当垃圾回收了.你买完水一看, 卧槽我饭呢? 然后你们大眼瞪小眼,一脸懵逼的互看着对方.
所以,GIL就诞生了.它是给你加在了解释器那,谁抢到了谁就再去执行,这样就导致了一个进程内同一时间只有一个线程在运行.
3 GIL与Lock
但需要注意的是不是说你有了这个GIL锁之后,你的数据就不用加锁了,此锁非彼锁.
还记得我们前面说的吗? 保护不同的数据的安全,就应该加不同的锁。
GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图
提示: 按照 序号 一边看一边分析即可知道为何要加不同的锁.
分析:
#1.100个线程去抢GIL锁,即抢执行权限
#2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
#3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
#4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
互斥锁和join的区别
#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock
import os,time
def task():
global n
print('%s is running' %current_thread().getName())
temp=n
time.sleep(0.5)
n=temp-1
if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''
#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
#未加锁的代码并发运行
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
#加锁的代码串行运行
lock.acquire()
temp=n
time.sleep(0.5)
n=temp-1
lock.release()
if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''
#有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
temp=n
time.sleep(0.5)
n=temp-1
if __name__ == '__main__':
n=100
lock=Lock()
start_time=time.time()
for i in range(100):
t=Thread(target=task)
t.start()
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
'''
4 GIL与多线程
有了GIL的存在,同一时刻同一进程中只有一个线程被执行
听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势,也就是说python没用了,php才是最牛逼的语言?
别着急啊,还没讲完呢。
要解决这个问题,我们需要在几个点上达成一致:
#1. cpu到底是用来做计算的,还是用来做I/O的?
#2. 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能
#3. 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处
一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。
如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,
反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高
结论:
对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地
#分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
#单核情况下,分析结果:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
#多核情况下,分析结果:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
#结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
五 多线程性能测试
计算密集型:多进程效率高
# -*- coding: utf-8 -*-
# @Author : JKey
# Timer : 2021/1/22 17:50
# 多线程性能测试
# 计算密集型: 多进程效率高
from multiprocessing import Process
from threading import Thread
import os, time
def work():
res = 0
for i in range(100000000):
res *= i
if __name__ == '__main__':
l = []
print(os.cpu_count())
start_time = time.time()
for i in range(8):
# p = Process(target=work)
p = Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
stop_time = time.time()
print(f'run time is {stop_time - start_time} s')
"""
当是多进程时:
消耗: 8.848984241485596 s
当是多线程时:
消耗: 38.15397763252258 s
计算密集型 多进程 快于 多线程
"""
I/O密集型:多线程效率高
# -*- coding: utf-8 -*-
# @Author : JKey
# Timer : 2021/1/22 17:50
# 多线程性能测试
# 计算密集型: 多进程效率高
from multiprocessing import Process
from threading import Thread
import os, time
def work():
time.sleep(5)
if __name__ == '__main__':
l = []
print(os.cpu_count())
start_time = time.time()
for i in range(8):
# p = Process(target=work)
p = Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
stop_time = time.time()
print(f'run time is {stop_time - start_time} s')
"""
当是多进程时:
消耗: 5.230102777481079 s
当是多线程时:
消耗: 5.003544330596924 s
IO密集型时 多线程 快于 多进程
"""