今日内容回顾
互斥锁
当多个程序同时操作一份数据的时候很容易产生数据错乱
为了避免数据错乱,我们需要使用互斥锁来解决
-
什么是互斥锁
将能够让多个程序同时操作的数据加锁,改变成只能单个程序操作的数据,单个程序操作完后释放锁才能让下一个程序操作。
将并发变成了串行,虽然牺牲了程序的执行效率但是保证了数据安全。
-
如何使用
导入模块multiprocessing中的Lock类
代码简易实操:
# 导入模块 from multiprocessing import Process, Lock # 创建一个对象 mutex = Lock() mutex.acquire() # 加锁, "加锁内的数据" mutex.release() # 释放锁 """ 被加锁的数据,程序如果想要访问只有抢到锁才能访问。 抢到锁的程序访问完数据后会释放锁等待下一个程序抢锁访问。 。 可以简单的讲锁理解为,将加锁的数据关进了一个小黑屋 并给小黑屋安装了一个门禁卡,只有得到门禁卡的程序才能够进入小黑屋 程序出去之后,将门禁卡放到小黑屋门口等待下一位程序拿到门禁卡进入小黑屋。 """" -
强调
互斥锁只应该出现在多个程序操作数据的地方,其他位置尽量不要加
线程理论
-
进程
进程是资源单位,可以将进程当作一个工厂的车间,进程负责给内部线程提供相应的资源
-
线程
线程是执行的单位,相当于工厂车间内的流水线,线程负责执行真正的功能
-
一个进程至少含有一个线程
-
多进程与多线程的区别
多进程:每开一个进程就需要开辟一个内存空间,需要拷贝全部代码资源消耗太大
多线程:不需要申请内存空间,也不需要拷贝代码资源消耗小
-
同一个进程下多个线程之间的资源是共享的。
创建线程的俩种方式
线程跟进程创建的方式非常相似,同一个进程下可以创建很多个线程不需要拷贝代码及申请内存空间。
基于threading模块中Thread类中方法创建进程
代码实操:
# 导入threading模块
from threading import Thread
def task(name):
print(f'{name}正在运行')
time.sleep(3)
print(f'{name}运行结束')
"""
创建进程不需要完整拷贝代码,所以无论什么时候系统都不会出现创建进程中的反复操作的情况
也不需要在启动脚本中执行,但是为了兼容性和统一性,可以习惯性的在启动脚本中编写
"""
# 方式1 在不需要启动脚本中创建
t = Thread(target=task, args=('jason',))
t.start()
print('主线程')
# 方式2 在启动脚本中创建(不是必须非要在启动脚本中创建只是为了兼容性和统一建议在脚本中创建)
if __name__ == '__main__':
t = Thread(target=task, args=('jason',))
t.start()
print('主线程')
基于类的继承方式创建线程
代码实操:
# 创建一个类继承Thread
class MyThread(Thread):
# 重写类中__init__方法
def __init__(self, name):
super().__init__()
self.name = name
# 重写类中run方法
def run(self):
print(f'{self.name}正在运行')
time.sleep(3)
print(f'{self.name}运行结束')
# 实例化产生一个对象,将所需要的数据填写进去
obj = MyThread('jason')
obj.start()
print('主线程')
多线程实现TCP服务端并发效果
比多进程更加简单,消耗资源更少
代码实操
# 服务端
from threading import Thread
import socket
def get_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
return server
def task(sock):
while True:
data = sock.recv(1024)
print(data.decode('utf8'))
sock.send(data.upper())
if __name__ == '__main__':
server = get_server()
while True:
sock, addr = server.accept()
t = Thread(target=task, args=(sock,))
t.start()
# 客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
for i in range(100):
print(i)
client.send('hell'.encode('utf8'))
data = client.recv(1024)
print(data.decode('utf8'))
线程的join方法
跟进程相同,也是主线程等待子线程运行结束后运行。
代码实操
主线程等到子线程运行结束之后再运行
from threading import Thread
import time
def task():
print('正在执行')
time.sleep(3)
print('运行结束')
t = Thread(target=task)
t.start()
t.join()
print('主线程')
同一个进程下线程间数据是共享的
跟进程不同,线程在同一进程内多个线程间的数据是共享的。
代码实操
from threading import Thread
money = 100
def func():
global money
money = 666
t = Thread(target=func)
t.start()
t.join() # 确保线程运行完毕 再查找money 结果更具有说服性
print(money)
线程对象的相关方法
-
进程号
同一个进程下创建的多个线程其拥有的都是相同的进程号
使用multiprocessing中的current_process().pid 查看
或者 os.getpid查看
-
线程名、统计进程下活跃的线程数
# current_thread().name 查看线程名 # active_count() 统计进程下活跃的线程数 from threading import Thread, current_thread, active_count import time def task(): # current_thread().name 主线程名:MainThread 子线程名:Thread-N print('子进程号:', current_thread().name) time.sleep(0.2) if __name__ == '__main__': for i in range(10): t = Thread(target=task) t.start() print('活跃的线程数:', active_count()) # cpu运行速度较快,需要延迟点时间才能查看具体效果
守护线程
守护线程伴随着被守护的线程的结束而结束
from threading import Thread
import time
def task():
print('子线程运行task函数')
time.sleep(3)
print('子线程运行task结束')
t = Thread(target=task)
t.daemon = True
t.start()
# t.daemon = True 必须在start上面
print('主线程')
"""
进程下所有的非守护线程结束 主线程(主进程)才能真正结束!!!
"""
GIL全局解释器锁
储备知识
1.python解释器也是由编程语言写出来的
Cpython 用C写出来的
Jpython 用Java写出来的
Pypython 用python写出来的
ps:最常用的就是Cpython(默认)
# 官方文档对GIL的解释
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
"""
1.GIL的研究是Cpython解释器的特点 不是python语言的特点
2.GIL本质也是一把互斥锁
3.GIL的存在使得同一个进程下的多个线程无法同时执行(关键)
言外之意:单进程下的多线程无法利用多核优势 效率低!!!
4.GIL的存在主要是因为cpython解释器中垃圾回收机制不是线程安全的
"""
1.误解:python的多线程就是垃圾 利用不到多核优势
python的多线程确实无法使用多核优势 但是在IO密集型的任务下是有用的
2.误解:既然有GIL 那么以后我们写代码都不需要加互斥锁
不对 GIL只确保解释器层面数据不会错乱(垃圾回收机制)
针对程序中自己的数据应该自己加锁处理
3.所有的解释型编程语言都没办法做到同一个进程下多个线程同时执行
ps:我们平时在写代码的时候 不需要考虑GIL 只在学习和面试阶段才考虑!!!

浙公网安备 33010602011771号