day 37

什么是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.)
'''

释义:

存在于cpython中是一个全局解释锁,也被称为GIL锁,也是一把互斥锁,是为了防止多个线程在同一个时间执行python字节码,

这个锁是非常重要的,因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,所以即使它影响了程序效率也无法将其直接去除。

在cpython中,GIL锁会仈=把线程的并行变成串行,导致运行效率变低。

另外解释器也不只有cpython还有pypy,jpython等等,GIL锁也只是存在于cpython中,所以这不是语言的问题,是解释器本身的问题,而cpython可以

调用c语言中一大堆库,python使用者来说是非常实用的。

GIL带来的问题

运行py文件的三个步骤:

1  从硬盘中加载python解释器到内存中

2  从硬盘中加载到py文件到内存中

3 解释器对py文件内容进行解析,再交给cpu执行

需要明确的是每当执行一个py文件就会开启一个python解释器

一一条线程在解释器上运行的时候GIL几乎没有任何影响,但是到解释器开始跑多线程时,GIL锁带来的问题就出现了,GIL是一种
互斥锁,多个进程之间需要共享解释器资源,为了避免共享带来的数据错乱的问题,于是就给解释器加上了互斥锁。

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

 

为什么需要GIL

GIL 与GC之间的渊源

在使用Python中进行编程时,程序员无需参与内存的管理工作,这是因为Python有自带的内存管理机制,简称GC。那么GC与GIL有什么关联?

要搞清楚这个问题,需先了解GC的工作原理,Python中内存管理使用的是引用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示该数据已经没有人使用,成了垃圾数据。

当内存占用达到某个阈值时,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行。

示范代码

from threading import  Thread
def task():
    a = 10
    print(a)

# 开启三个子线程执行task函数
Thread(target=task).start()
Thread(target=task).start()
Thread(target=task).start()

#打印结果

10
10
10

![image-20190102224044044](https://ws1.sinaimg.cn/large/006tNbRwly1fysmcbuzcxj30r00ix3zs.jpg)

从图中可以看出GC在于线程之间竞争解释器的执行权,cpu什么时候切换,切换到哪里去我们并不知道,假如线程正在定义

a = 10首先先申请内存空间将10放进去后,这时候切换到GC持有执行权,一看有个10没有被引用,那肯定就把它清理掉了,这样

还怎么定义变量a = 10,所以加互斥锁非常有必要了,


有了GIL锁以后多个线程不可能同时在同一个时间内使用解释器,保证了解释器的数据安全。

GIL的加锁与解锁时机

枷锁时机:在解释器被调用时立即加锁。

解锁时机:当线程遇到IO时或者线程执行时间到达设定值。

GIL的优点:保证了CPython中的内存管理是线程安全的

GIL的缺点:互斥锁的特性使得多线程无法并行

在单核的情况下,无论是IO密集还是计算莫密集GIL都不会产生任何影响

多核下对于IO密集任务,GIL会有细微的影响,基本可以忽略

Cpython中IO密集任务应该采用多线程,计算密集型应该采用多进程

另外:之所以广泛采用CPython解释器,就是因为大量的应用程序都是IO密集型的,还有另一个很重要的原因是CPython可以无缝对接各种C语言实现的库,这对于一些数学计算相关的应用程序而言非常的happy,直接就能使用各种现成的算法

IO密集测试

#多线程
def task():
    time.sleep(1)

if __name__ == "__main__":
    sta = time.time()
    ls = []
    for i in range(50):
        # p = Process(target=task)
        p = Thread(target=task)
        p.start()
        ls.append(p)
    for p in ls:
        p.join()
    print(time.time() - sta)
#打印结果
1.008044958114624
#多进程
def task():
    time.sleep(1)

if __name__ == "__main__":
    sta = time.time()
    ls = []
    for i in range(50):
        p = Process(target=task)
        # p = Thread(target=task)
        p.start()
        ls.append(p)
    for p in ls:
        p.join()
    print(time.time() - sta)
#打印结果
9.1587073802948

自定义锁与GIL的区别
GIL只是全局解释器的锁,对我们在程序自己定义的数据丝毫起不到保护的作用,所以我们还是需要自己定义锁来自行保护数据

如下例:

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()
只是有GIL锁的情况下,会先有一个线程拿到GIL锁,遇到IO后会释放锁,此时数据还没有被修改,当锁释放后
下一个进程也会读取数据,读取到的是没有被修改过的数据,进程只会修改自己读取到的数据,所以最终运行完的
结果为1
from threading import Thread,Lock
import time

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

    
t1 = Thread(target=task)
t2 = Thread(target=task)

t1.start()
t2.start()

t1.join()
t2.join()
print(a)
加上自己的锁以后,子线程会取得GIL锁后对需要进行修改的地方上锁没有完成修改不会释放锁,这样下一个线程拿到的数据就是修改过的数据
这样重复在子线程运行完毕以后就可以完成对数据的修改。

进程池与线程池

什么是进程/线程池?

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

当是IO密集型任务使用线程池,是计算密集型就使用进程池

为什么需要进程/线程池

1 创建线程池   可以自己指定线程数,不指定就默认cpu个数乘以5
不会立即加载,等到有任务提交后才会开启
不仅帮我们管理线程的开启和销毁,还帮助我们管理任务的分配,
避免了频繁开启和销毁线程的资源浪费,任务的参数直接写道后面,是可变位置参数

进程池的使用

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是IO密集型所以使用多线程

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

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

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

同步调用不等于阻塞虽然也有等待的效果,阻塞时程序会剥夺cpu的执行权,同步调用不会。

从字面上也可以看出是异步调用的效率更高。

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函数,而它们都是是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调

import time
from threading import Thread,current_thread
from concurrent.futures import ThreadPoolExecutor
import concurrent
pool = ThreadPoolExecutor(5)
def task():
    time.sleep(1)
    print("任务完成")
    return "我好帅"

def finished(res):             #异步回调,保证主线程线程转成串行
    print(res.result())
    print("确实帅")

print("start")

res = pool.submit(task)         #将任务提交给线程池

res.add_done_callback(finished)       #当子线程结束后异步调用取得子线程结果返回给主线程
print(res)              #<所属的类:Future 该线程自己内存空间:at 0x20ca77fbe48 线程目前所处状态:state=running>
print("over")

还有一个函数 shutdown()可以关闭线程池,但是他是默认让主进程处于等待状态,这样异步调用就变成了同步调用
这样可不是我们想要的结果

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

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

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

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

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

import requests

from concurrent.futures import ThreadPoolExecutor     #导入线程池
from threading import current_thread

pool = ThreadPoolExecutor()      #创建线程池

def get_data(url):
    response = requests.get(url)         #抓取网页文件
    print("%s下载完成"%current_thread().name)
    return url,response.text
    # parser(url,response.text)       #解析网站文本信息


def parser(*args):      #传过来的是一个元组形式的参数,元组内只有一个参数
    print(args)
    url,data = obj.result()             #对返回过来的线程结果进行解压赋值传回来的是一个两个值
    print("%s 长度 %s"%(url,len(data)))
    print("%s 解析完成"% current_thread().name)

url = 'http://www.jd.com'

obj = pool.submit(get_data,url)
obj.add_done_callback(parser)          # 异步调用将子线程运行结束后的结果立即返回给主线程
print("任务完成")

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

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

注意:

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

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

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

 

猜你喜欢

转载自www.cnblogs.com/1624413646hxy/p/10982140.html