目录
认识GIL
多线程
线程间通信
# GIL:global interpreter lock # python中的一个线程对应于C语言中的一个线程 # python在最初的时候,就给程序加了一把GIL锁,GIL使得同一时刻只有一个线程运行在一个cpu上执行字节码,无法将多个线程映射到多个cpu上 # GIL无法利用多核优势:如果我们运行一个进程,这个进程里的线程就只能在一个cpu里运行,而不能在其他cpu上运行
# 相比C/C++/JAVA,python确实慢,在一些特殊场景下,Python比C++慢100~200倍 # python速度慢的原因: # 1、动态类型语言,边解释边执行,而且要随时检查数据类型 # 2、GIL导致 # GIL:全局解释器锁 # python中对象的管理,是使用引用计数器进行的,当引用数为0则释放对象。撤销引用分为两步操作,1、是引用数减1,2是判断引用数为0则释放对象 # 比如线程A和线程B同时引用对象num,则num的引用数为2 # 当线程A和线程B都想撤销对num的引用时,A先撤销,让引用数减1,B紧接着也撤销,引用数再次减1,这时引用数为0,B这边就提前释放对象。 # 但紧接着A要判断该对象的引用数并决定要不要释放时,对象已经不存在了。程序就要报错,或是祸乱其他数据了 # 规避GIL带来的限制 # 1、多线程threading机制依然是有用的,用于IO密集型计算 # 因为在IO期间,线程会释放GIL,实现CPU和IO的并行,因此多线程用于IO密集型计算,依然可以大幅度提升速度 # 但是多线程用于CPU密集型计算时,只会更加拖慢速度 # 2、使用multiprocessing的多进程机制实现并行计算,利用多核CPU优势
# 使用dis.dis获取字节码
import dis
def add(a):
a = a + 1
print(dis.dis(add))
运行结果如下 6 0 LOAD_FAST 0 (a) 2 LOAD_CONST 1 (1) 4 BINARY_ADD 6 STORE_FAST 0 (a) 8 LOAD_CONST 0 (None) 10 RETURN_VALUE None
# GIL是根据执行的字节码行数以及时间片、或是遇到io操作会主动释放,例如运行了100行字节码后,就释放GIL # 多线程在io操作频繁的时候,非常适用,因为GIL遇到io操作会主动释放
# 没有加锁时,多线程对同一个数据操作会出现问题
import threading
total = 0
def add():
global total
for i in range(1000000):
total += 1
def desc():
global total
for i in range(1000000):
total -= 1
thread1 = threading.Thread(target = add)
thread2 = threading.Thread(target = desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total) # 每次运行结果都不一样
# 操作系统能够调动的最小单元是线程,进程对系统资源消耗 # 对于io操作来说,多线程和多进程性能差别不大 # 注意: # 1、创建多个子线程时,程序里本身还有一个主线程 # 2、主线程结束,子线程仍会自动执行,直至结束 # 3、要设置主线程结束,子线程就立刻被kill掉,需要对子线程设置为守护线程setDaemon # 4、要设置子线程结束后,才能接着往下运行主线程程序,需要对子线程设置阻塞join() # 多线程的应用方式:1、通过Thread类实例化 2、通过集成Thread来实现多线程 1、通过Thread类实例化 # 用一个线程爬取列表1,再用另一个线程去爬取详情页,几乎同时进行,因为遇到io时,GIL会释放的 # 爬取列表1,等待返回时,就可以让另一个线程去爬取详情页
import time
import threading
def get_detail_html(url):
# 爬取文章详情页
print("get detail html started")
time.sleep(2)
print("get detail html end")
def get_detail_url(url):
# 爬取文章列表页
print("get detail url started")
time.sleep(3)
print("get detail url end")
if __name__ == "__main__":
thread1 = threading.Thread(target=get_detail_html,args=("",))
thread2 = threading.Thread(target=get_detail_url,args = ("",))
start_time = time.time()
# setDaemon是守护线程,当主线程退出时,将子线程也关闭
# 如果没有设置守护线程,那么当主线程退出时,子线程也将继续运行到结束
# thread1.setDaemon(True)
thread2.setDaemon(True)
thread1.start()
thread2.start()
# 如果需要子线程运行结束后,再让主线程往下运行,那就需要对子线程进行阻塞
thread1.join()
thread2.join()
# 当主线程退出时
print(f"last time:{time.time()-start_time}")
2、通过继承Thread来实现多线程 # 继承Thread类后,可重载__init__方法和run方法,但尽量不要继承start方法
class GetDetailHtml(threading.Thread):
def __init__(self,name):
super().__init__(name=name)
# 重载run方法
def run(self):
print("get detail html started")
time.sleep(2)
print("get detail html end")
class GetDetalUrl(threading.Thread):
def __init__(self,name):
super().__init__(name=name)
def run(self):
print("get detail url started")
time.sleep(3)
print("get detail url end")
if __name__ == "__main__":
thread1 = GetDetailHtml("get_detail_html")
thread2 = GetDetailUrl("get_detail_url")
start_time = time.time()
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"last_time{time.time()-start_time}")
# 线程间的通信,get_detail_url和get_detail_html需要合作,才能抓取到详情页 # 线程通信方式: # 1、共享变量(global,或创建一个py文件存储变量),但变量的改变容易出错 # 2、通过queue的方式,进行线程间同步
# 线程间通信方法1:共享变量
import variables
detail_url_list = []
def get_detail_html(detail_url_list):
# detail_url_list = variables.detail_url_list
# 爬取文章详情页
while True:
if len(variables.detail_url_list):
url = variables.detail_url_list.pop()
print("get detail html started")
time.sleep(2)
print("get detail html end")
def get_detail_url():
# detail_url_list = variables.detail_url_list
# 爬取文章列表页
print("get detail url started")
time.sleep(3)
while True:
for i in range(20):
varialble.detail_url_list.append(f"http://prohectsedu.com/{i}")
print("get detail url end")
if __name__ == "__main__":
thread_detail_url = threading.Thread(target=get_detail_url)
for i in range(10):
html_thread = threading.Thread(target=get_detail_html)
html_thread.start()
# 2、通过queue的方式,进行线程间同步 # queue本质是deque的对象,而deque在字节码的层级上就已经实现了线程安全 # qsize可判断queue的长度,empty判断是否为空,full判断是否满了,如果队列满了,put会自动阻塞 # put_nowait\get_nowait实际就是调用了put\get,只不过加了block参数
# 2、通过queue的方式,进行线程间同步
from queue import Queue
import time
import threading
def get_detail_html(queue):
# 爬取文章详情页
while True:
# queue中的get方法是阻塞的,如果queue为空,就会阻塞,直到queue不为空
url = queue.get()
print("get detail html started")
time.sleep(2)
print("get detail html end")
def get_detail_url(queue):
# 爬取文章列表页
while True:
print("get detail url started")
time.sleep(3)
for i in range(20):
queue.put(f"http://prohectsedu.com/{i}")
print("get detail url end")
if __name__ == "__main__":
detail_url_queue = Queue(maxsize = 1000)
thread_detail_url = threading.Thread(target=get_detail_url,args=(detail_url_queue))
for i in range(10):
html_thread = threading.Thread(target=get_detail_html,args=(detail_url_queue))
html_thread.start()
start_time = time.time()
detail_url_queue.task_done()
detail_url_queue.join()
# 进程之间不共享全局变量,但线程共享全局变量,因此多线程操作全局变量会容易出现数据错误,因此需要线程等待(join)或是互斥锁 # 但线程等待或是互斥锁,都会让多线程任务变成单任务,执行性能会降低 # 创建进程的资源开销要比创建线程的资源开销大,因为创建进程要复制资源给子进程,另外也会自动创建出一条子进程的主线程 # 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。就是操作系统将线程分配给某个CPU去执行,而CPU会将调度过来的进程去安排线程。 # 线程不能够独立执行,必须依存在进程中 # 多进程开发比单进程多线程开发稳定性要强。 # 进程优缺点: # 优点:可以用多核 # 缺点:资源开销大 # 线程优缺点: # 资源开销小 # 不能使用多核 # 跟计算密集型的相关操作使用多进程,使用多核提高计算能力。 # 多线程:文件写入、文件下载,i/o操作数据写入和读写。
# 单线程串行:IO和CPU计算是头尾衔接的 # 多线程并发:IO和CPU计算可以是并行的,但IO和IO不能并行,CPU和CPU不能并行_threading # 多CPU并行:IO\CPU都能并行_multiprocssing # 多机器并行:hadoop/hive/spark # python对并发编程的支持 # 多线程:threading,利用CPU和IO可以同时执行的原理,让CPU不会干巴巴等待IO完成 # 多进程:multiprocess,利用多核CPU的能力,真正的并行执行任务 # 异步IO:asyncio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行。 # 使用Lokc对资源加锁,防止冲突访问。 # 使用Queue实现不同线程/进程之间的数据通信,实现生产者-消费者模式 # 使用线程池Pool/进程池Pool,简化线程/进程的任务提交、等待结束、获取结果 # 使用subprocess启动外部程序的进程,并进行输入输出交互 # python并发编程有三种方式:多线程Thread、多进程Process、多协程Coroutine # CPU密集型计算、IO密集型计算 # CPU密集型:也叫做计算密集型,是指I/O在很短的时间就可以完成,CPU需要大量的计算和处理,特点是CPU占用率非常高:压缩解压缩、加密解密、正则表达式搜索 # IO密集型:系统运作大部分的状况是CPU在等IO(硬盘内存)的读写操作,CPU占用率比较低,例如文件处理程序、网络爬虫程序、读写数据库程序 # 一个线程中可以启动N个协程 # 多协程优缺点: # 优点:内存开销最少、启动协程数量最多 # 缺点:支持的库有限制(例如request,不支持协程)、代码实现复杂 # 多协程使用于:IO密集型计算、需要超多任务运行、但有现成库支持的场景
# 线程的生命周期:新建、就绪(start)、运行、阻塞、终止 # 新建线程需要分配资源,终止线程系统需要回收资源 # 重用线程,则可以减去新建和终止的开销——使用线程池 # 线程池好处: # 1、提升性能:减去了大量新建、终止的开销 # 2、使用场景:适合处理突发性大量请求或需要大量线程完成任务,但实际任务处理时间较短 # 3、防御功能:能有效避免系统因为创建线程过多,而导致系统负荷过大相应变慢等问题 # 4、代码又是:使用线程池的语法简洁方便 # ThreadPoolExecutor的使用方法 # 1、map函数,map的结果和入参是顺序对应的 # 2、future模式,更强大,如果用as_completed顺序是不定的