37 GIL 线程池 同步异步 阻塞非阻塞-程序员宅基地

技术标签: 爬虫  内存管理  c/c++  

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_30486037/article/details/96004706

智能推荐

破解starUML3.1.0(Ubuntu18.04).md-程序员宅基地

文章浏览阅读3k次,点赞3次,收藏7次。文章目录参考博客一. 解包破解1.1 下载starUML程序1.2 解包程序1.3 去掉license验证1.3.1 安装asar1.3.2 解压app.asar1.3.3 修改源码1.3.4 替换 app.asar二. AppImageKit 打包2.1 下载AppImageKit2.2 重新打包参考博客破解StarUML3.01最新版 for Linux(Ubuntu16LTS)App...

干货!5大移动端表单设计原则及案例赏析_手机端表格怎么写逻辑-程序员宅基地

文章浏览阅读1.5w次,点赞2次,收藏10次。当我们在享受手机App为生活带来的巨大便利时,无形中已经经历了多种多样的移动表单设计形式。而表单设计又是移动应用设计中与用户产生最多交互的步骤,包括用户注册、订阅服务、用户反馈、问卷表单、买卖交易等等。一个优秀的表单设计更有助于提升用户体验,提高转化率,达到更好的营销效果。每一个表单设计的页面都有一个特定的目的,或是吸引注册,或是达成交易。考虑不周甚至是错误的设计有可能会导致用户的流失或交易失败。..._手机端表格怎么写逻辑

计算机按键音乐葫芦娃,Arduino学习笔记—超简单制作音乐(播放葫芦娃)-程序员宅基地

文章浏览阅读1.1k次。一曲葫芦娃 带你回归美好童年本文是个人学习心得,供新人参考,老鸟可瞬间飘过。本文很简单,需要用到的材料:adruino uno一块(其他也可),面保线若干条,蜂鸣器或小喇叭一个(小喇叭更好蜂鸣器要接电阻不然声音有点刺耳)连接方法如图:首先讲下简单的乐理知识,知道音乐是怎么演奏出来的自然就可以通过代码来进行编排了。1.演奏单音符一首乐曲有若干音符组成,一个音符对应一个频率。我们知道到相对应的频率..._.葫撸娃.net

爱贝支付 - 服务端 - nodejs实现_export const generatesign = (signstr) => { let sig-程序员宅基地

文章浏览阅读591次。爱贝支付nodejs实现,完整的代码,复制即可执行总结几个小坑:1. 使用私钥进行签名的问题,爱贝支付的签名规则是采用RSA MD5数字签名算法,私钥签名、公钥验签 所以本案例中用的是 nodejs 的 crypto 模块// 签名crypto.createSign('md5WithRSAEncryption');//验签crypto.createVerify('..._export const generatesign = (signstr) => { let sign = crypto.createsign('md5

VS2017使用scanf_s函数报错: (ucrtbased.dll)写入位置 0x00F6B000 时发生访问冲突。_vs2017写入位置时发生访问冲突-程序员宅基地

文章浏览阅读1.5w次,点赞46次,收藏59次。#include <stdio.h>#include <malloc.h>int main(){ char *str= (char *)malloc(20*sizeof(char)); scanf_s("%s", str); printf("%s\n",str);} 在使用VS2017时,应编译器要求需使用更加安全的..._vs2017写入位置时发生访问冲突

2048游戏的实现用C语言_2048游戏的实现用c语言studying one-程序员宅基地

文章浏览阅读118次。#include<stdlib.h>#include<stdio.h>#include<time.h>#define ROW 4#define LINE 4//这个数组为 全局数组--全局数组可以被该文件内任意函数调用//调用的前提--出现在任何函数之前int array_2048[ROW][LINE] = { 0 };//现在设定一个函数,功能是设置某个位置的随机值void position()//随机产生4或2{ int i..._2048游戏的实现用c语言studying one

随便推点

Android毕设项目功能:商城列表与购物车展示(二)_搭构购物车列表条目android-程序员宅基地

文章浏览阅读2.8k次。在上一篇博客中,为大家展示了最终完成效果图,并且分析了界面之间的关系,以及每个界面布局结构中包含的控件信息,对于总体功能数据源进行了封装和介绍。并且重点说明了第一个界面商品分类界面的实现方法。在本篇博客中我们继续操作,完成具体分类的商品信息列表界面的展示。效果图如下:需求分析:此界面的布局结构为上中下结构,可使用线性布局进行排列,上半部分为标题栏,左右两个图标都具备点击功能,左边点击后..._搭构购物车列表条目android

R语言画图表_r语言图表-程序员宅基地

文章浏览阅读3.3k次。R 编程语言中有许多库用来创建图表,主要有6种图表1. 条形图条形图表示矩形条中的数据,其长度与变量的值成比例。R 使用 barplot()函数来创建条形图。R 可以在条形图中绘制垂直和水平条。在条形图中,每个条可以被赋予不同的颜色。语法使用 R 创建条形图的基本语法是barplot(H, xlab, ylab, main, names.arg, col)以下是使用的参数的描述:H..._r语言图表

Netty之线程唤醒wakeup [续]_netty wakeup-程序员宅基地

文章浏览阅读268次。在之前的Netty之线程唤醒wakeup文章中, 介绍了如何唤醒Netty中的监听线程. 接下来我们通过源码的角度,结合一些命令,看一下它的实现.// WakeUp.javaimport java.net.InetSocketAddress;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java._netty wakeup

java encodeuricomponent 编码_encodeURIComponent编码与解码-程序员宅基地

文章浏览阅读1.3w次。问题:JavaScript用encodeURIComponentt编码后无法再后台解码的问题。目前写法:window.self.location="list.jsp?searchtext="+encodeURIComponent(seartext);java处理的代码为:searchtext=java.net.URLDecoder.decode(searchtext,"UTF-8");咋一看觉的没..._java encodeuricomponent

HBase与Zookeeper的关系_hbase和zookeeper的关系-程序员宅基地

文章浏览阅读2.7k次,点赞3次,收藏9次。HBase与Zookeeper的关系一、HBase与Zookeeper的关系ZookeeperClientMasterRegionServer一、HBase与Zookeeper的关系Client客户端、Master、Region都会通过心跳机制(RPC通信)与zookeeper保持联系。当在Hbase中插入或读取数据时流程如下:在Client中写一个Java类运行,客户端只需要连接zoo..._hbase和zookeeper的关系

点击列表项跳转_如何在Windows 10中增加跳转列表项的数量-程序员宅基地

文章浏览阅读520次。点击列表项跳转In previous versions of Windows, you could change the number of recent items shown in jump lists with a simple option in taskbar properties. For whatever reason, Microsoft removed thisability ..._要显示在跳转列表中最近使用的项目数

推荐文章

热门文章

相关标签