非线程安全 即 多个线程访问同一个资源,会 有问题
该锁只存在Cpython中,这并不是Python这门语言的 除了Cpython之外 Jpython, pypy,解释器
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将使得程序整体效率降低!
释放:
该线程任务结束
该线程遇到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)
自定义的线程锁与
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的占用率
同步-异步 指的是提交任务的方式
同步指调用:发起任务后必须在原地等待任务执行完成,才能继续执行
异步指调用:发起任务后必须不用等待任务执行,可以立即开启执行其他操作
程序中的异步调用并获取结果方式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来指定一个回调函数,
如果把任务比喻为烧水,没有回调时就只能守着水壶等待水开,有了回调相当于换了一个会响的水壶,烧水期间可用作其他的事情,等待水开了水壶会自动发出声音,这时候再回来处理。水壶自动发出声音就是回调。
注意:
-
使用进程池时,回调函数都是主进程中执行执行
-
使用线程池时,回调函数的执行线程是不确定的,哪个线程空闲就交给哪个线程
-
回调函数默认接收一个参数就是这个任务对象自己,再通过对象的result函数来获取任务的处理结果