python详解(10)--GIL与高并发与其他

GIL(Global Interpreter Lock):全局解释器锁

GIL的由来
GIL并非python语言的问题,而是C语言写的python解释器的问题,为了解决多线程之间的状态和全局变量问题(互斥锁会有死锁的可能性),在整个进程中添加的全局锁,使得python的多线程在Cpython中没有并行的可能性,同一时间执行的线程只能有一个,因此完全无法发挥多核CPU的能力。

GIL的工作原理和缺陷
python语言的线程调度是依赖操作系统本身的调度算法的(即C语言的调度),python会计算当前线程已执行的微代码数量(opcode)达到一定阈值后强制释放GIL,触发操作系统的一次线程调度,而其中的问题在于,线程GIL的释放和获取几乎是没有时间间隔的,因此在多核心多线程的任务中,在其他核心上的线程被唤醒后主线程已经又一次获取到GIL,只能浪费CPU的时间,造成总体性能比起单核CPU仍要下降。(改进措施有先持有GIL再进行上下文切换,在IO等待时释放GIL等,但使得操作系统线程调度占用更多资源)

GIL的影响及改进措施
在多核CPU下,python的多线程只有在IO密集型计算产生正面效果(因为多线程在遇到IO操作时会自动释放GIL锁进行调度),在至少有一个CPU密集型线程存在时,多线程的效率就会因为GIL大幅下降。
改进措施有:①使用多进程替代线程(每一个进程都有其独立的GIL锁);②使用其他的解释器;③使用其他语言的代码模块嵌入python中;④针对于GIL的改进,将切换颗粒度从opcode改为时间片计数,避免最近一次释放GIL锁的线程再次被立即调度,新增线程优先级功能等;⑤使用多进程+协程,极大提高效率。

GIL与互斥锁的区别
GIL保证的是同一时刻只有一个线程在执行,而互斥锁保证的是某一线程完成某一任务后(任务的完整性)才会解锁。

高并发与多线程

首先,高并发 ≠ 多线程

多线程:是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。

高并发:是一种系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问或者socket端口集中性收到大量请求(例如:12306的抢票情况;天猫双十一活动)。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。如果高并发处理不好,不仅仅降低了用户的体验度(请求响应时间过长),同时可能导致系统宕机,严重的甚至导致OOM异常,系统停止工作等。如果要想系统能够适应高并发状态,则需要从各个方面进行系统优化,包括,硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化……而多线程只是其中解决方法之一。

实现高并发需要考虑
①系统的架构设计,如何在架构层面减少不必要的处理(网络请求,数据库操作等)
②网络拓扑优化减少网络请求时间、如何设计拓扑结构,分布式如何实现?
③系统代码级别的代码优化,使用什么设计模式来进行工作?哪些类需要使用单例,哪些需要尽量减少new操作?
④提高代码层面的运行效率、如何选取合适的数据结构进行数据存取?如何设计合适的算法?
⑤任务执行方式级别的同异步操作,在哪里使用同步,哪里使用异步?
⑥数据库优化减少查询修改时间。数据库的选取?数据库引擎的选取?数据库表结构的设计?数据库索引、触发器等设计?是否使用读写分离?还是需要考虑使用数据仓库?
⑦缓存数据库的使用,如何选择缓存数据库?是Redis还是Memcache? 如何设计缓存机制?
⑧数据通信问题,如何选择通信方式?是使用TCP还是UDP,是使用长连接还是短连接?NIO还是BIO?netty、mina还是原生socket?
⑨操作系统选取,是使用winserver还是Linux?或者Unix?
⑩硬件配置?是8G内存还是32G,网卡10G还是1G?

:①高并发是一个复杂的问题,其影响因素有很多,多线程在这里只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式。多线程在解决高并发问题中所起到的作用就是使计算机的资源在每一时刻都能达到最大的利用率,不至于浪费计算机资源使其闲置。

②在项目代码层面只能针对单个用户的需求进行优化(即只能服务于单用户,通过减少单用户的响应时间优化全局,如celery异步发送邮件等),而更高一层的多用户的并发访问的都是同样的代码,这个层面的优化是由nginx(有些框架自带支持异步处理的服务器,如tornado)等web服务器来实现的。

一些其他问题及后续python相关的问题总结

以上即为个人学习过的python所有内容,其中大部分资料查阅自网上技术博客,小部分查阅自官方文档,由于内容较多,难免出现遗漏错,还请看到的大佬指出,西瓜这里拜谢!

以下内容属于零碎知识点,后续python相关的知识点及问题也会更新在此篇博客下。

URL编码
URL中有一些保留符号(! * ’ ( ) ; : @ & = + $ , / ? # [ ])用于在URL中起特殊作用,如果某个参数中有这些符号,则需要先对这些符号进行编码,否则会更改URL语义;此外,URL只支持ASCII字符集,因此会将在URL中的其他符号和语言进行编码后传输(浏览器有自己的编码方式,一般操作为在前端用JS对URL统一编码),有些浏览器在显示URL时会自动作一次解码,但其实传输的是编码后的值。(如中文,会按多个字节进行编码)
在python中可以使用urllib.parse模块对url进行编码和解码,urllib.parse.quote(a)编码,rullib.parse.unquote(a)解码。

logging模块
完成日志记录功能,其共分为5个等级,如下所示,某等级的日志只记录>=该等级的信息,warning为默认级别,日志的级别和内容是需要开发人员明确指定的,可以指定日志输出在终端或记录在文件中,通过修改日志的级别来改变不同生产环境中记录信息的详细程度。

DEBUG:详细的信息,通常只出现在诊断问题上;
INFO:确认一切按预期运行;
WARNING:一个迹象表明,一些意想不到的事情发生了,或表明一些问题在不久的将来(例如:磁盘空间低),这个软件还能按预期工作;
ERROR:更严重的问题,软件没能执行一些功能;
CRITICAL:一个严重的错误,这表明程序本身可能无法继续运行。

日志调用方法:使用logging.basicConfig(level=logging.WARNING, filename='', filemode='wa', format='xxx')其中filename与filemode与文件操作中类似,format的格式为
%(levelno)s: 打印日志级别的数值
%(levelname)s: 打印日志级别名称
%(pathname)s: 打印当前执行程序的路径,其实就是sys.argv[0]
%(filename)s: 打印当前执行程序名
%(funcName)s: 打印日志的当前函数
%(lineno)d: 打印日志的当前行号
%(asctime)s: 打印日志的时间
%(thread)d: 打印线程ID
%(threadName)s: 打印线程名称
%(process)d: 打印进程ID
%(message)s: 打印日志信息;
在工作中常用格式为format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s';使用logging.debug/info('ddd')来输出日志内容。

日志的作用:①程序调试②记录程序运行信息,故障分析与问题定位③日志足够详细时可用来分析用户行为。

StringIO与BytesIO
是在内存中读写的操作,属于io模块下的对象,其与文件的操作方法相同,但需要先导入io模块。
与名称类似的,StringIO操作字符串,BytesIO操作经过utf-8编码的字节流,f=StringIO(‘ddd’),相当于创建类的实例,也是创建了类似于打开文件时的字节流。

从内存地址获取变量值
python中,可以使用address = id(obj)获取变量内存地址,可以使用ctypes.cast(address, ctypes.py_object).value获取指定内存地址中的变量值,其中ctypes是内置模块。

路径相关
注意在开发中,所有的路径都从当前文件(即主文件)路径(与导入模块/调用函数无关)开始算起。

上一篇:线程中的锁的相关问题


下一篇:Python-----GIL锁