官方解释: ''' 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使用者来说是非常实用的。
在使用Python中进行编程时,程序员无需参与内存的管理工作,这是因为Python有自带的内存管理机制,简称GC。那么GC与GIL有什么关联?
当内存占用达到某个阈值时,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
从图中可以看出GC在于线程之间竞争解释器的执行权,cpu什么时候切换,切换到哪里去我们并不知道,假如线程正在定义
a = 10首先先申请内存空间将10放进去后,这时候切换到GC持有执行权,一看有个10没有被引用,那肯定就把它清理掉了,这样
还怎么定义变量a = 10,所以加互斥锁非常有必要了,
有了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锁后对需要进行修改的地方上锁没有完成修改不会释放锁,这样下一个线程拿到的数据就是修改过的数据
这样重复在子线程运行完毕以后就可以完成对数据的修改。
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密集型所以使用多线程
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")
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("任务完成")
如果把任务比喻为烧水,没有回调时就只能守着水壶等待水开,有了回调相当于换了一个会响的水壶,烧水期间可用作其他的事情,等待水开了水壶会自动发出声音,这时候再回来处理。水壶自动发出声音就是回调。
注意:
-
使用进程池时,回调函数都是主进程中执行执行
-
使用线程池时,回调函数的执行线程是不确定的,哪个线程空闲就交给哪个线程
-
回调函数默认接收一个参数就是这个任务对象自己,再通过对象的result函数来获取任务的处理结果