37 GIL 线程池 同步异步 阻塞非阻塞

GIL锁

 

GIL 全局解释器锁,是一个互斥锁. 是为了防止多个本地线程同一时间执行python代码,,Cpython的内存管理是非线程安全的

非线程安全 即 多个线程访问同一个资源,会 有问题

线程安全 即 多个线程访问同一个资源,不会有问题

该锁只存在Cpython中,这并不是Python这门语言的 除了Cpython之外 Jpython, pypy,解释器

之所以使用Cpython的原因??

C编译过的结果可以计算机直接识别

最主要的语言,C语言以后大量现成的,库(算法,通讯),Cpython可以无缝连接C语言的任何现成代码

内存管理

垃圾回收机制

python中不需要手动管理内存 ,C,OC

引用计数

a = 10 10地址次数计数为1

b = a 计数2

b = 1 计数1

a = 0 计数0

当垃圾回收启动后会将计数为0的数据清除掉,回收内存

分代回收

自动垃圾回收其实就是说,内部会有一个垃圾回收线程,会在某一时间运行起来,开始清理垃圾

这是可能会产生问题,例如线程1申请了内存,但是还没有使用CPU切换到了GC,GC将数据当成垃圾清理掉了

为了解决这个问题,Cpython就给解释器加上了互斥锁!

GIL锁作用:

开启子线程时,给子线程指定了一个target表示该子线程要处理的任务即要执行的代码。代码要执行则必须交由解释器,即多个线程之间就需要共享解释器,为了避免共享带来的数据竞争问题,于是就给解释器加上了互斥锁!

由于互斥锁的特性,程序串行,保证数据安全,降低执行效率,GIL将使得程序整体效率降低!

GIL锁的加锁与解锁时机

加锁: 只要有一个线程要使用解释器就立马枷锁

释放:

该线程任务结束

该线程遇到IO

该线程使用解释器过长 默认100纳秒

GIL的优点:

- 保证了CPython中的内存管理是线程安全的

GIL的缺点:

- 互斥锁的特性使得多线程无法并行
但我们并不能因此就否认Python这门语言,其原因如下:

1. GIL仅仅在CPython解释器中存在,在其他的解释器中没有,并不是Python这门语言的缺点

2. 在单核处理器下,多线程之间本来就无法真正的并行执行

3. 在多核处理下,运算效率的确是比单核处理器高,但是要知道现代应用程序多数都是基于网络的(qq,微信,爬虫,浏览器等等),CPU的运行效率是无法决定网络速度的,而网络的速度是远远比不上处理器的运算速度,则意味着每次处理器在执行运算前都需要等待网络IO,这样一来多核优势也就没有那么明显了

##### 举个例子:

任务1 从网络上下载一个网页,等待网络IO的时间为1分钟,解析网页数据花费,1秒钟

任务2 将用户输入数据并将其转换为大写,等待用户输入时间为1分钟,转换为大写花费,1秒钟

**单核CPU下:**1.开启第一个任务后进入等待。2.切换到第二个任务也进入了等待。一分钟后解析网页数据花费1秒解析完成切换到第二个任务,转换为大写花费1秒,那么总耗时为:1分+1秒+1秒 = 1分钟2秒

**多核CPU下:**1.CPU1处理第一个任务等待1分钟,解析花费1秒钟。1.CPU2处理第二个任务等待1分钟,转换大写花费1秒钟。由于两个任务是并行执行的所以总的执行时间为1分钟+1秒钟 = 1分钟1秒

可以发现,多核CPU对于总的执行时间提升只有1秒,但是这边的1秒实际上是夸张了,转换大写操作不可能需要1秒,时间非常短!

上面的两个任务都是需要大量IO时间的,这样的任务称之为IO密集型,与之对应的是计算密集型即IO操作较少大部分都是计算任务。

对于计算密集型任务,Python多线程的确比不上其他语言!为了解决这个弊端,Python推出了多进程技术,可以良好的利用多核处理器来完成计算密集任务。

计算密集型的效率测试

from multiprocessing import Process
from threading import Thread
import time

def task():
   for i  in range(10000000):
       i += 1

if __name__ == '__main__':
   start_time = time.time()
   # 多进程
   # p1 = Process(target=task)
   # p2 = Process(target=task)
   # p3 = Process(target=task)
   # p4 = Process(target=task)

   # 多线程
   p1 = Thread(target=task)
   p2 = Thread(target=task)
   p3 = Thread(target=task)
   p4 = Thread(target=task)

   p1.start()
   p2.start()
   p3.start()
   p4.start()

   p1.join()
   p2.join()
   p3.join()
   p4.join()
   
   print(time.time()-start_time)

IO密集型的效率测试

from multiprocessing import Process
from threading import Thread
import time
def task():
   with open("test.txt",encoding="utf-8") as f:
       f.read()
if __name__ == '__main__':
   start_time = time.time()
   # 多进程
   # p1 = Process(target=task)
   # p2 = Process(target=task)
   # p3 = Process(target=task)
   # p4 = Process(target=task)

   # 多线程
   p1 = Thread(target=task)
   p2 = Thread(target=task)
   p3 = Thread(target=task)
   p4 = Thread(target=task)

   p1.start()
   p2.start()
   p3.start()
   p4.start()

   p1.join()
   p2.join()
   p3.join()
   p4.join()

   print(time.time()-start_time)

自定义的线程锁与GIL的区别

GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等

对于程序中自己定义的数据则没有任何的保护效果,所以当程序中出现了共享自定义的数据时就要自己加锁

l例子:

from threading import Thread,Lock
import time

a = 0
def task():
global a
temp = a
time.sleep(0.01)
a = temp + 1

t1 = Thread(target=task)
t2 = Thread(target=task)
t1.start()
t2.start()
t1.join()
t2.join()
print(a)

 

过程分析:

1.线程1获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL

2.线程2获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL

3.线程1睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL

4.线程2睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL,最后a的值也就是1

之所以出现问题是因为两个线程在并发的执行同一段代码,解决方案就是加锁!

加锁和释放

拿到解释器要执行代码时立即加锁

遇到IO操作时释放

时间片用完 (最大设置为100)

进程池与线程池

什么是进程/线程池?

池表示一个容器,本质上就是一个存储进程或线程的列表,线程池 用来存储线程对象的对象

池子中存储线程还是进程?

如果是IO密集型任务使用线程池,如果是计算密集任务则使用进程池

python中ThreadPoolExecutor(线程池)与ProcessPoolExecutor(进程池)都是concurrent.futures模块下的,主线程(或进程)中可以获取某一个线程(进程)执行的状态或者某一个任务执行的状态及返回值。

通过submit返回的是一个future对象,它是一个未来可期的对象,通过它可以获悉线程的状态



import os,time

# 获取CPU核心数
print(os.cpu_count())

# 1.创建池子 可以指定池子里有多少线程 如果不指定默认为CPU个数 * 5
# 不会立即开启线程 会等到有任务提交后在开启线程

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
# 1.创建池子 可以指定池子里有多少线程 如果不指定默认为CPU个数 * 5
# 不会立即开启线程 会等到有任务提交后在开启线程

pool = ThreadPoolExecutor(10)
# 线程池最大值,机器所能承受的最大值 当然需要考虑你的机器有几个任务要做

from threading import enumerate,current_thread



print(enumerate())
def task(name,age):
print(name)
print(current_thread().name,'run')
time.sleep(2)

# 该函数提交任务到线程池中
pool.submit(task,'jerry',10)
#任务的参数 直接写到后面不需要定义参数名称 因为是可变位置参数
pool.submit(task,'qw',20)
pool.submit(task)
time.sleep(2)

print(enumerate())

"""
线程池,不仅帮我们管理了线程的开启和销毁,还帮我们管理任务的分配
特点: 线程池中的线程只要开启之后 即使任务结束也不会立即结束 因为后续可能会有新任务
避免了频繁开启和销毁线程造成的资源浪费
1.创建一个线程池
2.使用submit提交任务到池子中 ,线程池会自己为任务分配线程


"""

# 进程池的使用 同样可以设置最大进程数量,默认为CPU的个数

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time,os

# 创建进程池,指定最大进程数为3,此时不会创建进程,不指定数量时,默认为CPU和核数
pool = ProcessPoolExecutor(3)

def task():
time.sleep(1)
print(os.getpid(),"working..")

if __name__ == '__main__':
for i in range(10):
pool.submit(task) # 提交任务时立即创建进程

# 任务执行完成后也不会立即销毁进程
time.sleep(2)

for i in range(10):
pool.submit(task) #再有新任务是 直接使用之前已经创建好的进程来执行

线程池的使用:

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import current_thread,active_count
import time,os

# 创建进程池,指定最大线程数为3,此时不会创建线程,不指定数量时,默认为CPU和核数*5
pool = ThreadPoolExecutor(3)
print(active_count()) # 只有一个主线

def task():
   time.sleep(1)
   print(current_thread().name,"working..")

if __name__ == '__main__':
   for i in range(10):
       pool.submit(task) # 第一次提交任务时立即创建线程

   # 任务执行完成后也不会立即销毁
   time.sleep(2)

   for i in range(10):
       pool.submit(task) #再有新任务时 直接使用之前已经创建好的线程来执行

案例:TCP中的应用

首先要明确,TCP是IO密集型,应该使用线程池

线程池的shutdown

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import current_thread,enumerate

import time
pool = ThreadPoolExecutor(3)
def task():
print(current_thread().name)
print(current_thread().isDaemon())
time.sleep(1)

for i in range(5):
pool.submit(task)

st=time.time()

pool.shutdown()
# 等待所有任务全部完毕 销毁所有线程 后关闭线程池
print(time.time()-st)

print('over')

同步异步-阻塞非阻塞

同步异步-阻塞非阻塞,经常会被程序员提及,并且概念非常容易混淆!

阻塞非阻塞指的是程序的运行状态

阻塞:当程序执行过程中遇到了IO操作,在执行IO操作时,程序无法继续执行其他代码,称为阻塞!

非阻塞:程序在正常运行没有遇到IO操作,或者通过某种方式使程序即时遇到了也不会停在原地,还可以执行其他操作,以提高CPU的占用率

同步-异步 指的是提交任务的方式

同步指调用:发起任务后必须在原地等待任务执行完成,才能继续执行

异步指调用:发起任务后必须不用等待任务执行,可以立即开启执行其他操作

同步会有等待的效果但是这和阻塞是完全不同的,阻塞时程序会被剥夺CPU执行权,而同步调用则不会!

程序中的异步调用并获取结果方式1:

from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
import time

pool = ThreadPoolExecutor(3)
def task(i):
   time.sleep(0.01)
   print(current_thread().name,"working..")
   return i ** i

if __name__ == '__main__':
   objs = []
   for i in range(3):
       res_obj = pool.submit(task,i) # 异步方式提交任务# 会返回一个对象用于表示任务结果
       objs.append(res_obj)

# 该函数默认是阻塞的 会等待池子中所有任务执行结束后执行
pool.shutdown(wait=True)

# 从结果对象中取出执行结果
for res_obj in objs:
   print(res_obj.result())
print("over")

程序中的异步调用并获取结果方式2:

from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
import time

pool = ThreadPoolExecutor(3)
def task(i):
   time.sleep(0.01)
   print(current_thread().name,"working..")
   return i ** i

if __name__ == '__main__':
   objs = []
   for i in range(3):
       res_obj = pool.submit(task,i) # 会返回一个对象用于表示任务结果
       print(res_obj.result()) #result是同步的一旦调用就必须等待 任务执行完成拿到结果
print("over")

异步回调

什么是异步回调

异步回调指的是:在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数

为什么需要异步回调

之前在使用线程池或进程池提交任务时,如果想要处理任务的执行结果则必须调用result函数或是shutdown函数,而它们都是是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调

异步回调的使用

先来看一个案例:

在编写爬虫程序时,通常都是两个步骤:

1.从服务器下载一个网页文件

2.读取并且解析文件内容,提取有用的数据

按照以上流程可以编写一个简单的爬虫程序

要请求网页数据则需要使用到第三方的请求库requests可以通过pip或是pycharm来安装,在pycharm中点击settings->解释器->点击+号->搜索requests->安装

import requests,re,os,random,time
from concurrent.futures import ProcessPoolExecutor

def get_data(url):
   print("%s 正在请求%s" % (os.getpid(),url))
   time.sleep(random.randint(1,2))
   response = requests.get(url)
   print(os.getpid(),"请求成功 数据长度",len(response.content))
   #parser(response) # 3.直接调用解析方法 哪个进程请求完成就那个进程解析数据 强行使两个操作耦合到一起了
   return response

def parser(obj):
   data = obj.result()
   htm = data.content.decode("utf-8")
   ls = re.findall("href=.*?com",htm)
   print(os.getpid(),"解析成功",len(ls),"个链接")

if __name__ == '__main__':
   pool = ProcessPoolExecutor(3)
   urls = ["https://www.baidu.com",
           "https://www.sina.com",
           "https://www.python.org",
           "https://www.tmall.com",
           "https://www.mysql.com",
           "https://www.apple.com.cn"]
   # objs = []
   for url in urls:
       # res = pool.submit(get_data,url).result() # 1.同步的方式获取结果 将导致所有请求任务不能并发
       # parser(res)

       obj = pool.submit(get_data,url) #
       obj.add_done_callback(parser) # 4.使用异步回调,保证了数据可以被及时处理,并且请求和解析解开了耦合
       # objs.append(obj)
       
   # pool.shutdown() # 2.等待所有任务执行结束在统一的解析
   # for obj in objs:
   #     res = obj.result()
   #     parser(res)
   # 1.请求任务可以并发 但是结果不能被及时解析 必须等所有请求完成才能解析
   # 2.解析任务变成了串行,

总结:异步回调使用方法就是在提交任务后得到一个Futures对象,调用对象的add_done_callback来指定一个回调函数,

如果把任务比喻为烧水,没有回调时就只能守着水壶等待水开,有了回调相当于换了一个会响的水壶,烧水期间可用作其他的事情,等待水开了水壶会自动发出声音,这时候再回来处理。水壶自动发出声音就是回调。

注意:

  1. 使用进程池时,回调函数都是主进程中执行执行

  2. 使用线程池时,回调函数的执行线程是不确定的,哪个线程空闲就交给哪个线程

  3. 回调函数默认接收一个参数就是这个任务对象自己,再通过对象的result函数来获取任务的处理结果





 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

原文地址:https://www.cnblogs.com/komorebi/p/10982142.html