day37,全局解释器锁,异步同步,与线程池进程池

GIL:Global Interpreter Lock

全局解释器锁

官方解释: ''' 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.)

'''

在cpython中,全局解释器锁(gil)是一个互斥体,用于防止

同时执行python字节码的本机线程。这个锁主要是必要的

因为cpython的内存管理不是线程安全的。(然而,自从GIL

存在,其他特性也越来越依赖于它所执行的保证。)

 

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

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

Cpython中,有一个互斥锁,防止线程同一时间执行python代码

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

之所以使用Cpython的原因??

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

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

python 胶水 就是这也能干 那也能干

 

GC与GIL

垃圾回收机制

 

python中不需要手动管理内存,用的是引用计数

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

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

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

 

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

为什么Cpython要这么设计???

Cpython诞生于1991年 而多核处理器诞生2004年

当时不需要考虑多核效率问题

 

现在为什么不拿掉这个锁,因为这期间,很多已经完成的代码都依赖于这个锁,如果直接拿到,这些代码全得该,成本太大了

 

GIL锁的加锁与解锁时机

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

释放:

该线程任务结束

该线程遇到IO

 

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

GIL给我们造成的影响

多线程不能并行

案例:

有一个下载任务 要从网络中下载一个文件 大小1G

和转换 任务 使用input 转为大写输出

上述任务并行执行,耗时也不会有太大的提升,反而开启多进程会浪费更多资源

这种任务称之为IO密集型,大量的时间都花在IO等待

这类问题使用多线程,

计算密集型任务

 

图像处理,语音处理,大数据分析,

解决方案: *****

区分任务类型

如果是IO密集使用多线程

 

如果是计算密集使用多进程

GIL锁与自定义锁的关系

都是互斥锁

为什么有了GIL还需要自己加锁

GIL是加在解释器上的,只能锁住,解释器内部的资源,但是无法锁住我们自己开启资源

例如: 我们自己开启了一个json文件,多个线程要同时访问, 如果第一个线程写入数据写到一半,执行超时了,另一个线程过来接着写,就会产生问题,

自己开启共享资源还得自己所锁

 

像是申请内存 保存数据等等就不需要我们程序员考虑了 GIL已经搞定了

线程池与进程池 *****

线程池

池就是容器,

线程池就是装线程的容器

1.自动管理线程的开启和销毁

2.自动分配任务给空闲的线程

3.可以线程开启线程的数量 保证系统稳定

信号量也可以设置最大数量.信号量中是限制同时并发多少,但是线程已经全都建完了

如何使用:

1.创建池子

2.submit 提交任务

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

 

关闭后就不能提交新任务了

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

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

pool = ThreadPoolExecutor(2) # 线程池最大值,应该机器所能承受的最大值
#
print(enumerate())   # 检测当前存活的线程 [<_MainThread(MainThread, started 11148)>]

def task(i):
    print("%s is running" % i)
    time.sleep(3)

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

def task1(name,age):
    print(name)
    print(current_thread().name, "run")   # ThreadPoolExecutor-0_0 run
    print(age)

pool.submit(task1,"jerry",30)

time.sleep(5)

print(enumerate())

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

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

pool = ProcessPoolExecutor()
def task2(name):
    print(os.getpid())
    print(name)

if __name__ == '__main__':
    pool.submit(task2,"wangyong")
    pool.shutdown()   # 等待所有任务全部完毕 销毁所有线程 后关闭线程池
    # pool.submit(task2,"jerry") # 线程已经被销毁,所以会报错

3. 同步异步----阻塞非阻塞

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

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

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

 

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

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

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

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

而同步调用则不会!

 

4. 异步回调

4.1 什么是异步回调

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

 

4.2 为什么需要异步回调

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

 

4.3 异步回调的案例分析

线程池异步回调

"""
    爬虫是干啥的
    从网络中下载某个页面数据
    在从页面中提取有价值的数据

"""
"""
为嘛使用异步回调
1.生产者和消费者解开了耦合  
2.消费者可以及时处理生产完成的数据 
"""
import requests
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread

pool = ThreadPoolExecutor(2)

# 获取数据
def get_data(url):
    response = requests.get(url)
    print("%s 下载完成" % current_thread().name)
    return url,response.text

# 解析数据
def parser(obj):
    url,data = obj.result()
    print("%s 长度%s" % (url,len(data)))
    print("%s解析完成!" % current_thread().name)

urls = [
    "http://www.baidu.com",
    "http://www.qq.com",
    "http://www.python.org",
    "http://www.sina.com",
    "http://www.oldboyedu.com",
]

for u in urls:
    obj = pool.submit(get_data, u)
    obj.add_done_callback(parser)

print("提交完成")

进程池异步回调

import os
import requests
from concurrent.futures import ProcessPoolExecutor

pool = ProcessPoolExecutor()

def get_data(url):
    response = requests.get(url)
    print("%s下载完成" % os.getpid())
    return url,response.text

def parser(obj):
    url, data = obj.result()
    print("%s 的长度%s" %(url, len(data)))
    print("%s解析完成" % os.getpid())

urls = [
    "http://www.baidu.com",
    "http://www.qq.com",
    "http://www.python.org",
    "http://www.sina.com",
    "http://www.oldboyedu.com",
]

if __name__ == '__main__':
    for u in urls:
        obj = pool.submit(get_data, u)
        obj.add_done_callback(parser)

    print("提交完成")
    print("父进程pid:", os.getpid())

 

昨日回顾

JoinableQueue

可以被join的队列

join是等待任务结束

队列怎么叫结束?

调用task_done一次则表示有一个数据被处理完成了 当task_done次数等于put的次数就意味着任务处理完成了

这也是join的执行时机

该队列可以明确告知数据的使用方,所有数据都已经处理完成

在生产者消费者模型中,解决了消费者不知道何时算是任务结束的问题

具体过程:主进程先等待所有的生产者进程生成完毕,再等队列中的数据被全部处理,这就意味着,任务全部结束

 

多线程

使用多线程 多进程的目的 是一致 ,都是为了并发执行任务,从而提高效率

什么是线程:

线程是操作系统运算调度的最小单位 (CPU最小执行单位),线程被包含在进程中,一个线程就是一个固定的执行流程 (控制流)

线程的特点:

进程是不能被执行,进程是一个资源单位,其中包含了程序运行所需要的所有资源,

线程才是真正的执行单位,光有进程程序是无法运行的,必须先创建进程将资源加载到进程中,在启动线程来执行任务

一个进程至少包含一个线程,称之为主线程,主线程是由操作系统来开启,

一个进程可以包含多个线程,来提高程序的效率

线程与进程的区别:

线程创建的开销远小于进程

统一进程中的所有线程共享进程内的资源

线程之间没有父子关系,都是平等的,PID相同

如何选择:

要根据具体的任务类型,IO密集 计算密集

 

线程的使用方法与进程一模一样

开启线程的位置可以是任何位置

守护线程

守护线程会在所有非守护线程结束时一起结束 ,当然守护可以提前结束

线程安全问题

并发操作同一个资源 可能导致数据错乱的问题

解决方案: 加互斥锁

 

互斥锁 mutex

递归锁 Rlock 同一线程可以多次执行acquire()

信号量 semaphore

死锁问题:

不止一个锁,分别被不同线程持有,相互等待对方释放,就会导致锁死问题

 

今日内容

GIL锁 *****

Global Interpreter Lock 全局解释器锁

官方解释:'''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.)

'''

 

 

在 Cpython中,这个全局解释器锁 或者 称为GIL,是一个互斥锁. 是为了防止多个本地线程同一时间执行python字节码,

这个锁是非常重要的因为Cpython的内存管理是非线程安全的, ,然而这个GIL有存在的必要性, 因为有很多已经存在的代码,需要依赖这个锁

 

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

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

 

CPython中有一个互斥锁,防止线程同一时间执行python代码

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

之所以使用Cpython的原因??

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

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

python 胶水 就是这也能干 那也能干

 

内存管理 了解

垃圾回收机制

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

引用计数

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

b = a 计数2

b = 1 计数1

a = 0 计数0

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

分代回收

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

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

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

 

为什么Cpython要这么设计???

Cpython诞生于1991年 而多核处理器诞生2004年

当时不需要考虑多核效率问题

现在为什么不拿掉这个锁,因为这期间,很多已经完成的代码都依赖于这个锁,如果直接拿到,这些代码全得该,成本太大了

 

 

 

GIL锁的加锁与解锁时机

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

释放:

该线程任务结束

该线程遇到IO

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

 

 

GIL给我们造成的影响

多线程不能并行

案例:

有一个下载任务 要从网络中下载一个文件 大小1G

和转换 任务 使用input 转为大写输出

上述任务并行执行,耗时也不会有太大的提升,反而开启多进程会浪费更多资源

这种任务称之为IO密集型,大量的时间都花在IO等待

这类问题使用多线程,

 

计算密集型任务

图像处理,语音处理,大数据分析,

解决方案: *****

区分任务类型

如果是IO密集使用多线程

如果是计算密集使用多进程

 

GIL锁与自定义锁的关系

都是互斥锁

为什么有了GIL还需要自己加锁

GIL是加在解释器上的,只能锁住,解释器内部的资源,但是无法锁住我们自己开启资源

例如: 我们自己开启了一个json文件,多个线程要同时访问, 如果第一个线程写入数据写到一半,执行超时了,另一个线程过来接着写,就会产生问题,

自己开启共享资源还得自己所锁

像是申请内存 保存数据等等就不需要我们程序员考虑了 GIL已经搞定了

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

线程池与进程池 *****

线程池

池就是容器,

线程池就是装线程的容器

好处:

1.自动管理线程的开启和销毁

2.自动分配任务给空闲的线程

3.可以线程开启线程的数量 保证系统稳定

信号量中是限制同时并发多少,但是线程已经全都建完了

如何使用:

1.创建池子

2.submit 提交任务

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

关闭后就不能提交新任务了

 

同步 异步

在并发中 经常提及的几个概念

阻塞 - 非阻塞 程序的状态

程序的运行状态 非阻塞 可能就绪 或者 运行

 

并发 - 并行 多任务处理方式

多个任务看起来像是同时运行 ,本质是切换+保存状态

并行真正的同时进行中, 必须具备多核处理器

 

同步 异步 任务提交(执行方式)方式

 

同步 同步==阻塞 × 卡住 == 阻塞 ×

指的是发起任务后,必须在原地等待,直到任务完成拿到结果 ,称之为同步

默认情况就是同步的

异步 异步 == 非阻塞 ×

发起任务,不需要等待结果,可以继续执行其他代码,异步

异步任务必须依赖,并发或并行, 在python中 通过多线程或多进程

异步的效率明显高于同步

 

 

异步回调

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

异步任务的问题:

如果这个任务执行完成后会产生返回值,任务发起方该何时去获取结果

解决方案:

异步回调

异步回调指的就是一个函数,该函数会在任务后自动被调用,并且会传入Future对象 ,

通过Future对象的result()获取执行结果 ,

有了回调函数 就可以在任务完成时 及时处理它

案例:

 

通常异步任务都会绑定一个回调函数,用来处理任务结果

 

在进程池中回调函数是在父进程中执行,原因是 任务是由父进程发起的,所以结果也应该交给父进程

在线程池中回调函数就在子线程中执行,原因是 线程之间数据本来是共享的

 

如果你的任务结果需要交给父进程来处理,那建议回调函数,回调函数会自动将数据返回给父进程,不需要自己处理IPC

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

上一篇:python37 1.GIL--全局解释器锁 2.GIL带来的问题 3.为什么需要GIL 4.GIL的加锁解锁时机 5.关于GIL的性能的讨论 6.线程常用方法 7.GIL锁与自定义锁的区别 8.进


下一篇:线程,线程安全与python的GIL锁