【并发编程】第3回 线程和GIL全局解释器锁
目录
1.将TCP服务端制作成并发效果
1.1 方式一
- 将数据交互代码封装成函数循环,把端口号等数据放在if name == 'main':方法下
- 服务端
import socket
from multiprocessing import Process
# 数据交互的封装成函数
def talk(sock):
while True:
data = sock.recv(1024)
print(data.decode('utf8'))
sock.send(data.upper())
if __name__ == '__main__':
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, addr = server.accept()
# 开设进程去完成数据交互
p = Process(target=talk, args=(sock,))
p.start()
- 客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
client.send(b'hello baby')
data = client.recv(1024)
print(data.decode('utf8'))
1.2 方式二
- 将数据交互代码封装成函数循环,把服务端对象封装成函数
- 服务端
import socket
from multiprocessing import Process
# 方式2 服务端对象的函数
def get_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
return server
# 数据交互的封装成函数
def talk(sock):
while True:
data = sock.recv(1024)
print(data.decode('utf8'))
sock.send(data.upper())
if __name__ == '__main__':
# server = socket.socket()
# server.bind(('127.0.0.1', 8080))
# server.listen(5)
server = get_server()
while True:
sock, addr = server.accept()
# 开设进程去完成数据交互
p = Process(target=talk, args=(sock,))
p.start()
- 客户端不做改变
2.互斥锁
2.1 理解
- 多个程序同时操作一份数据的时候很容易产生数据错乱,为了避免数据错乱,我们需要互斥锁
2.2 本质
- 将并发变成串行,虽然牺牲了程序的执行效率但是保证了数据安全
2.3 代码展示
from multiprocessing import Process, Lock
import time
import json
import random
# 查票
def search(name):
with open(r'data.json', 'r', encoding='utf8') as f:
data = json.load(f)
print('%s在查票 当前余票为:%s' % (name, data.get('ticket_num')))
# 买票
def buy(name):
# 再次确认票
with open(r'data.json', 'r', encoding='utf8') as f:
data = json.load(f)
# 模拟网络延迟
time.sleep(random.randint(1, 3))
# 判断是否有票 有就买
if data.get('ticket_num') > 0:
data['ticket_num'] -= 1
with open(r'data.json', 'w', encoding='utf8') as f:
json.dump(data, f)
print('%s买票成功' % name)
else:
print('%s很倒霉 没有抢到票' % name)
def run(name, mutex):
search(name)
mutex.acquire() # 抢锁
buy(name)
mutex.release() # 释放锁
if __name__ == '__main__':
# 1.先产生锁
mutex = Lock()
for i in range(10):
p = Process(target=run, args=('用户%s'%i, mutex))
p.start()
2.4 强调
1.互斥锁只应该出现在多个程序操作数据的地方,其他位置尽量不要加
2.5 相关知识
- 行锁:只针对当前操作的行加锁。行级锁能减少数据库操作的冲突。加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。行级锁按照使用方式分为共享锁和排他锁。
- 表锁:只针对当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。
- 乐观锁
- 悲观锁
3.线程理论
3.1 理解
- 进程是资源单位:进程相当于是车间,进程负责给内部的线程提供相应的资源
- 线程是执行单位:线程相当于是车间的流水线,线程负责执行真正的功能
3.2 特征
- 一个进程至少含有一个线程
- 多进程与多线程的区别
多进程:需要申请内存空间,需要拷贝全部代码,资源消耗大
多线程:不需要申请内存空间,也不需要拷贝全部代码,资源消耗小 - 同一个进程下多个线程之间资源共享
4.创建线程的两种方式
- 开设线程不需要完整拷贝代码,所以无论什么系统都不会出现反复操作的情况,也不需要在启动脚本中执行,但是为了兼容性和统一性,习惯在启动脚本中编写
4.1 方式一 封装函数
from threading import Thread
import time
def task(name):
print(f'{name}正在运行')
time.sleep(3)
print(f'{name}运行结束')
t = Thread(target=task, args=('jason',))
t.start()
print('主线程')
if __name__ == '__main__':
t = Thread(target=task, args=('jason',))
t.start()
print('主线程')
4.2 方式二 继承类
from threading import Thread
import time
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f'{self.name}正在运行')
time.sleep(3)
print(f'{self.name}运行结束')
obj = MyThread('jason')
obj.start()
print('主线程')
5.多线程实现TCP服务端并发
- 比多进程更加简单方便,消耗的资源更少
import socket
from threading import Thread
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
def servers(sock):
while True:
try:
data = sock.recv(1024)
if len(data) == 0: break
data1 = data.decode('utf8')
data2 = data1.upper()
data3 = data2.encode('utf8')
sock.send(data3)
except ConnectionResetError as e:
print(e)
break
print('服务端接收消息>>>:%s' % data1)
while True:
sock, address = server.accept()
p = Thread(target=servers, args=(sock,))
p.start()
6.join方法
6.1 定义
- 主线程等到子线程运行结束之后再运行
# 主线程等到子线程运行结束之后再运行
from threading import Thread
import time
def task():
print('正在执行')
time.sleep(3)
print('运行结束')
t = Thread(target=task)
t.start()
t.join()
print('主线程')
7.同一个进程下线程间的数据共享
from threading import Thread
money = 1000
def func():
global money
money = 666
t = Thread(target=func)
t.start()
t.join() # 确保线程运行完毕 再查找money 结果更具有说服性
print(money)
8.线程对象相关方法
8.1 进程号
同一个进程下开设的多个线程拥有相同的进程号
8.2 线程名
from threading import Thread,current_thread
current_thread().name
主:MainThread 子:Thread-N
8.3 进程下的线程数
active_count()
9.守护线程
9.1 定义
- 守护线程伴随着被守护的线程的结束而结束
- 进程下所有的非守护线程结束,主线程(主进程)才能真正的结束
9.2 代码展示
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
print('主线程')
10.GIL全局解释器锁
10.1 储备知识
- python解释器也是由编程语言写出来的,Cpython-用c写出来的,Jpython-用Java写出来的,Pypython 用python写出来的,最常见的就是Cpython(默认)
10.2 官方文档对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.)
- GIL的研究是Cpython解释器的特点,不是python语言的特点
- GIL本质也是一把互斥锁
- GIL的存在使得同一个进程下的多个线程无法同时执行(关键)<理解:单进程下的多线程无法利用多核优势,效率低>
- GIL的存在主要是因为Cpython解释器中垃圾回收机制不是线程安全的
- 误解:python的多线程就是垃圾,利用不到多核优势,python的多线程确实无法使用多核优势,但是再IO密集型的任务下是有用的
- 误解:既然有GIL,那么以后我们写代码都不需要加互斥锁>>>: 不对,GIL只确保解释器层面数据不会错乱(垃圾回收机制),针对程序中自己的数据应该自己加锁处理(例:大门上锁,小门也上锁)
- 所有的解释型编程语言都没办法做到同一个进程下多个线程同时执行
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)