Python 并发系列 (V) - asyncio 的核心概念和基本架构
声明:本文针对的是python3.4以后的版本的,因为从3.4开始才引入asyncio,后面的3.5 3.6 3.7版本是向前兼容的,只不过语法上面有稍微的改变。比如在3.4版本中使用@asyncio.coroutine装饰器和yield from语句,但是在3.5以后的版本中使用async、await两个关键字代替,虽然语法上稍微有所差异,但是原理是一样的。本文用最通俗的语言解释了pythonasyncio背后的一些核心概念,简要解析了asyncio的设计架构,并给出了使用python进行asyncio异步编程的一般模板。
一、一些最重要的概念
1、协程(coroutine)——本质就是一个函数
所谓的“协程”就是一个函数,这个函数需要有两个基本的组成要素,第一,需要使用@asyncio.coroutine进行装饰;第二,函数体内一定要有yield from 返回的的generator,或者是说使用yield from 返回另一个协程对象。
当然,这两个条件并不是硬性规定的,如果没有这两个条件,依然是函数,只不过是普通函数而已。
怎么判断一个函数是不是协程?通过asyncio.iscoroutine(obj)和asyncio.iscoroutinefunction
(func)加以判断,返回true,则是。
那么协程函数有什么作用呢?
(1)result = yield from future
作用一:返回future的结果。什么是future?后面会讲到。当协程函数执行到这一句,协程会被悬挂起来,知道future的结果被返回。如果是future被中途取消,则会触发CancelledError异常。
由于task是future的子类,后面也会介绍,关于future的所有应用,都同样适用于task
(2)result = yield from coroutine
等候另一个协程函数返回结果或者是触发异常
(3)result= yield from task
返回一个task的结果
(4)return expression
作为一个函数,他本身也是可以返回某一个结果的
(5)raise exception
2、事件循环——event_loop
协程函数,不是像普通函数那样直接调用运行的,必须添加到事件循环中,然后由事件循环去运行,单独运行协程函数是不会有结果的,看一个简单的例子:
import time
import asyncio
async def say_after_time(delay,what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"开始时间为: {time.time()}")
await say_after_time(1,"hello")
await say_after_time(2,"world")
print(f"结束时间为: {time.time()}")
loop=asyncio.get_event_loop() #创建事件循环对象
#loop=asyncio.new_event_loop() #与上面等价,创建新的事件循环
loop.run_until_complete(main()) #通过事件循环对象运行协程函数
loop.close()
在python3.6版本中,如果我们单独像执行普通函数那样执行一个协程函数,只会返回一个coroutine对象(python3.7)如下所示:
>>> main()
<coroutine object main at 0x1053bb7c8>
(1)获取事件循环对象的几种方式:
下面几种方式可以用来获取、设置、创建事件循环对象loop
loop=asyncio.
get_running_loop
() 返回(获取)在当前线程中正在运行的事件循环,如果没有正在运行的事件循环,则会显示错误;它是python3.7中新添加的
loop=asyncio.
get_event_loop
() 获得一个事件循环,如果当前线程还没有事件循环,则创建一个新的事件循环loop;
loop=asyncio.
set_event_loop
(loop) 设置一个事件循环为当前线程的事件循环;
loop=asyncio.
new_event_loop
() 创建一个新的事件循环
(2)通过事件循环运行协程函数的两种方式:
(1)方式一:创建事件循环对象loop,即asyncio.get_event_loop(),通过事件循环运行协程函数
(2)方式二:直接通过asyncio.run(function_name)运行协程函数。但是需要注意的是,首先run函数是python3.7版本新添加的,前面的版本是没有的;其次,这个run函数总是会创建一个新的事件循环并在run结束之后关闭事件循环,所以,如果在同一个线程中已经有了一个事件循环,则不能再使用这个函数了,因为同一个线程不能有两个事件循环,而且这个run函数不能同时运行两次,因为他已经创建一个了。即同一个线程中是不允许有多个事件循环loop的。
asyncio.run()是python3.7 新添加的内容,也是后面推荐的运行任务的方式,因为它是高层API,后面会讲到它与asyncio.run_until_complete()的差异性,run_until_complete()是相对较低层的API。
注意:到底什么是事件循环?如何理解?
可以这样理解:线程一直在各个协程方法之间永不停歇的游走,遇到一个yield from 或者await就悬挂起来,然后又走到另外一个方法,依次进行下去,知道事件循环所有的方法执行完毕。实际上loop是BaseEventLoop的一个实例,我们可以查看定义,它到底有哪些方法可调用。
3、什么是awaitable对象——即可暂停等待的对象
有三类对象是可等待的,即 coroutines, Tasks, and Futures.
coroutine:本质上就是一个函数,一前面的生成器yield和yield from为基础,不再赘述;
Tasks: 任务,顾名思义,就是要完成某件事情,其实就是对协程函数进一步的封装;
Future:它是一个“更底层”的概念,他代表一个一步操作的最终结果,因为一步操作一般用于耗时操作,结果不会立即得到,会在“将来”得到异步运行的结果,故而命名为Future。
三者的关系,coroutine可以自动封装成task,而Task是Future的子类。
4、什么是task任务
如前所述,Task
用来 并发调度的协程,即对协程函数的进一步包装?那为什么还需要包装呢?因为单纯的协程函数仅仅是一个函数而已,将其包装成任务,任务是可以包含各种状态的,异步编程最重要的就是对异步操作状态的把控了。
(1)创建任务(两种方法):
方法一:task = asyncio.create_task(coro()) # 这是3.7版本新添加的
方法二:task = asyncio.ensure_future(coro())
也可以使用
loop.
create_future
()
loop.
create_task
(coro)
也是可以的。
备注:关于任务的详解,会在后面的系列文章继续讲解,本文只是概括性的说明。
(2)获取某一个任务的方法:
方法一:task=asyncio.
current_task
(loop=None)
返回在某一个指定的loop中,当前正在运行的任务,如果没有任务正在运行,则返回None;
如果loop为None,则默认为在当前的事件循环中获取,
方法二:asyncio.
all_tasks
(loop=None)
返回某一个loop中还没有结束的任务
5、什么是future?
Future是一个较低层的可等待(awaitable)对象,他表示的是异步操作的最终结果,当一个Future对象被等待的时候,协程会一直等待,直到Future已经运算完毕。
Future是Task的父类,一般情况下,已不用去管它们两者的详细区别,也没有必要去用Future,用Task就可以了,
返回 future 对象的低级函数的一个很好的例子是 loop.run_in_executor().
二、asyncio的基本架构
前面介绍了asyncio里面最为核心的几个概念,如果能够很好地理解这些概念,对于学习协程是非常有帮助的,但是按照我个人的风格,我会先说asyncio的架构,理解asyncio的设计架构有助于更好地应用和理解。
asyncio分为高层API和低层API,我们都可以使用,就像我前面在讲matplotlib的架构的时候所讲的一样,我们前面所讲的Coroutine和Tasks属于高层API,而Event Loop 和Future属于低层API。当然asyncio所涉及到的功能远不止于此,我们只看这么多。下面是是高层API和低层API的概览:
High-level APIs
- Coroutines and Tasks(本文要写的)
- Streams
- Synchronization Primitives
- Subprocesses
- Queues
- Exceptions
Low-level APIs
- Event Loop(下一篇要写的)
- Futures
- Transports and Protocols
- Policies
- Platform Support
所谓的高层API主要是指那些asyncio.xxx()的方法,
1、常见的一些高层API方法
(1)运行异步协程
asyncio.run
(coro, *, debug=False) #运行一个一步程序,参见上面
(2)创建任务
task=asyncio.
create_task
(coro) #python3.7 ,参见上面
task = asyncio.ensure_future(coro())
(3)睡眠
await asyncio.
sleep
(delay, result=None, *, loop=None)
这个函数表示的是:当前的那个任务(协程函数)睡眠多长时间,而允许其他任务执行。这是它与time.sleep()的区别,time.sleep()是当前线程休息,注意他们的区别哦。
另外如果提供了参数result,当当前任务(协程)结束的时候,它会返回;
loop参数将会在3.10中移除,这里就不再说了。
(4)并发运行多个任务await asyncio.
gather
(*coros_or_futures, loop=None, return_exceptions=False)
它本身也是awaitable的。
*coros_or_futures是一个序列拆分操作,如果是以个协程函数,则会自动转换成Task。
当所有的任务都完成之后,返回的结果是一个列表的形式,列表中值的顺序和*coros_or_futures完成的顺序是一样的。
return_exceptions:False,这是他的默认值,第一个出发异常的任务会立即返回,然后其他的任务继续执行;
True,对于已经发生了异常的任务,也会像成功执行了任务那样,等到所有的任务执行结束一起将错误的结果返回到最终的结果列表里面。
如果gather()本身被取消了,那么绑定在它里面的任务也就取消了。
(5)防止任务取消
await asyncio.
shield
(*arg, *, loop=None)
它本身也是awaitable的。顾名思义,shield为屏蔽、保护的意思,即保护一个awaitable 对象防止取消,一般情况下不推荐使用,而且在使用的过程中,最好使用try语句块更好。
try:
res = await shield(something())
except CancelledError:
res = None
(6)设置timeout——一定要好好理解
await asyncio.
wait_for
(aw, timeout, *, loop=None)
如果aw是一个协程函数,会自动包装成一个任务task。参见下面的例子:
import asyncio
async def eternity():
print('我马上开始执行')
await asyncio.sleep(3600) #当前任务休眠1小时,即3600秒
print('终于轮到我了')
async def main():
# Wait for at most 1 second
try:
print('等你3秒钟哦')
await asyncio.wait_for(eternity(), timeout=3) #休息3秒钟了执行任务
except asyncio.TimeoutError:
print('超时了!')
asyncio.run(main())
'''运行结果为:
等你3秒钟哦
我马上开始执行
超时了!
'''
为什么?首先调用main()函数,作为入口函数,当输出‘等你3秒钟哦’之后,main挂起,执行eternity,然后打印‘我马上开始执行’,然后eternity挂起,而且要挂起3600秒,大于3,这时候出发TimeoutError。修改一下:‘’
import asyncio
async def eternity():
print('我马上开始执行')
await asyncio.sleep(2) #当前任务休眠2秒钟,2<3
print('终于轮到我了')
async def main():
# Wait for at most 1 second
try:
print('等你3秒钟哦')
await asyncio.wait_for(eternity(), timeout=3) #给你3秒钟执行你的任务
except asyncio.TimeoutError:
print('超时了!')
asyncio.run(main())
'''运行结果为:
等你3秒钟哦
我马上开始执行
终于轮到我了
'''
总结:当异步操作需要执行的时间超过waitfor设置的timeout,就会触发异常,所以在编写程序的时候,如果要给异步操作设置timeout,一定要选择合适,如果异步操作本身的耗时较长,而你设置的timeout太短,会涉及到她还没做完,就抛出异常了。
(7)多个协程函数时候的等候
await asyncio.
wait
(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)
与上面的区别是,第一个参数aws是一个集合,要写成集合set的形式,比如:
{func(),func(),func3()}
表示的是一系列的协程函数或者是任务,其中协程会自动包装成任务。事实上,写成列表的形式也是可以的。
注意:该函数的返回值是两个Tasks/Futures的集合:
(done, pending)
其中done是一个集合,表示已经完成的任务tasks;pending也是一个集合,表示还没有完成的任务。
常见的使用方法为:done, pending = await asyncio.wait(aws)
参数解释:
timeout (a float or int), 同上面的含义一样,需要注意的是,这个不会触发asyncio.TimeoutError异常,如果到了timeout还有任务没有执行完,那些没有执行完的tasks和futures会被返回到第二个集合pending里面。
return_when参数,顾名思义,他表示的是,什么时候wait函数该返回值。只能够去下面的几个值。
Constant | Description |
---|---|
FIRST_COMPLETED |
first_completes.当任何一个task或者是future完成或者是取消,wait函数就返回 |
FIRST_EXCEPTION |
当任何一个task或者是future触发了某一个异常,就返回,.如果是所有的task和future都没有触发异常,则等价与下面的 ALL_COMPLETED . |
ALL_COMPLETED |
当所有的task或者是future都完成或者是都取消的时候,再返回。 |
如下面例子所示:
import asyncio
import time
a=time.time()
async def hello1(): #大约2秒
print("Hello world 01 begin")
yield from asyncio.sleep(2)
print("Hello again 01 end")
async def hello2(): #大约3秒
print("Hello world 02 begin")
yield from asyncio.sleep(3)
print("Hello again 02 end")
async def hello3(): #大约4秒
print("Hello world 03 begin")
yield from asyncio.sleep(4)
print("Hello again 03 end")
async def main(): #入口函数
done,pending=await asyncio.wait({hello1(),hello2(),hello3()},return_when=asyncio.FIRST_COMPLETED)
for i in done:
print(i)
for j in pending:
print(j)
asyncio.run(main()) #运行入口函数
b=time.time()
print('---------------------------------------')
print(b-a)
'''运行结果为:
Hello world 02 begin
Hello world 01 begin
Hello world 03 begin
Hello again 01 end
<Task finished coro=<hello1() done, defined at e:\Python学习\基础入门\asyncio3.4详解\test11.py:46> result=None>
<Task pending coro=<hello3() running at e:\Python学习\基础入门\asyncio3.4详解\test11.py:61> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object
at 0x000001FA8D394438>()]>>
<Task pending coro=<hello2() running at e:\Python学习\基础入门\asyncio3.4详解\test11.py:55> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object
at 0x000001FA8D394378>()]>>
---------------------------------------
2.033155679702759
'''
从上面可以看出,hello1()试运行结束了的,hello2()和hello3()还没结束。
(8)asyncio.as_completed()函数
这个函数我没有找到合适的中文名称去描述,所以哪位大神如果知道,望告知,不胜感激!它的函数原型如下:
asyncio.
as_completed
(aws, *, loop=None, timeout=None)
第一个参数aws:同上面一样,是一个集合{}集合里面的元素是coroutine、task或者future
第三个参数timeout:意义和上面讲的的一样
那到底什么作用呢?其实很简单,个人感觉有点鸡肋,从一个例子看起:
import asyncio
import time
import threading
a=time.time()
@asyncio.coroutine
def hello1():
print("Hello world 01 begin")
yield from asyncio.sleep(5) #大约5秒
print("Hello again 01 end")
return '哈哈1'
@asyncio.coroutine
def hello2():
print("Hello world 02 begin")
yield from asyncio.sleep(3) #大约3秒
print("Hello again 02 end")
return '哈哈2'
@asyncio.coroutine
def hello3():
print("Hello world 03 begin")
yield from asyncio.sleep(4) #大约4秒
print("Hello again 03 end")
return '哈哈3'
async def main():
s=asyncio.as_completed({hello1(),hello2(),hello3()})
for f in s:
result=await f
print(result)
asyncio.run(main())
b=time.time()
print('---------------------------------------')
print(b-a)
'''运行结果为:
Hello world 03 begin
Hello world 01 begin
Hello world 02 begin
Hello again 01 end
哈哈1
Hello again 02 end
哈哈2
Hello again 03 end
哈哈3
---------------------------------------
4.0225794315338135
'''
结论:asyncio.as_completed()函数返回的是一个可迭代(iterator)的对象,对象的每个元素就是一个future对象,很多小伙伴说,这不是相当于没变吗?其实返回的future集合是对参数的future集合重新组合,组合的顺序就是,最先执行完的协程函数(coroutine、task、future)最先返回,从上面的代码可知,参数为
aws={hello1(),hello2(),hello3()},因为hello1大约花费5秒、hello2大约花费3秒、hello3大约花费4秒。返回的结果为
s={hello2()、hello3()、hello(1)},因为hello2时间最短,故而放在前面,hello1时间最长,故而放在最后面。然后对返回的集合s开始迭代。
2、Task 类详解
先来看一下Task类的简单介绍(英文原文文档)。
class
asyncio.
Task
(coro, *, loop=None)A Future-like object that runs a Python coroutine. Not thread-safe.
Tasks are used to run coroutines in event loops. If a coroutine awaits on a Future, the Task suspends the execution of the coroutine and waits for the completion of the Future. When the Future is done, the execution of the wrapped coroutine resumes.
Event loops use cooperative scheduling: an event loop runs one Task at a time. While a Task awaits for the completion of a Future, the event loop runs other Tasks, callbacks, or performs IO operations.
Use the high-level asyncio.create_task() function to create Tasks, or the low-level loop.create_task() or ensure_future() functions. Manual instantiation of Tasks is discouraged.
To cancel a running Task use the cancel() method. Calling it will cause the Task to throw a CancelledError exception into the wrapped coroutine. If a coroutine is awaiting on a Future object during cancellation, the Future object will be cancelled.
cancelled() can be used to check if the Task was cancelled. The method returns
True
if the wrapped coroutine did not suppress the CancelledError exception and was actually cancelled.asyncio.Task inherits from Future all of its APIs except Future.set_result() and Future.set_exception().
Tasks support the contextvars module. When a Task is created it copies the current context and later runs its coroutine in the copied context.
上面的文字描述中推出了几个非常重要的信息,特在此总结如下:
(1)他是作为一个python协程对象,和Future对象很像的这么一个对象,但不是线程安全的;他继承了Future所有的API,,除了Future.set_result()和Future.set_Exception();
(2)使用高层API asyncio.ccreate_task()创建任务,或者是使用低层API loop.create_task()或者是loop.ensure_future()创建任务对象;
(3)相比于协程函数,任务时有状态的,可以使用Task.cancel()进行取消,这会触发CancelledError异常,使用cancelled()检查是否取消。
下面介绍Task类常见的一些使用函数
(1)cancel
()
Request the Task to be cancelled.
其实前面已经有所介绍,最好是使用他会出发CancelledError异常,所以需要取消的协程函数里面的代码最好在try-except语句块中进行,这样方便触发异常,打印相关信息,但是Task.cancel()没有办法保证任务一定会取消,而Future.cancel()是可以保证任务一定取消的。可以参见下面的一个例子:
import asyncio
async def cancel_me():
print('cancel_me(): before sleep')
try:
await asyncio.sleep(3600) #模拟一个耗时任务
except asyncio.CancelledError:
print('cancel_me(): cancel sleep')
raise
finally:
print('cancel_me(): after sleep')
async def main():
#通过协程创建一个任务,需要注意的是,在创建任务的时候,就会跳入到异步开始执行
#因为是3.7版本,创建一个任务就相当于是运行了异步函数cancel_me
task = asyncio.create_task(cancel_me())
#等待一秒钟
await asyncio.sleep(1)
print('main函数休息完了')
#发出取消任务的请求
task.cancel()
try:
await task #因为任务被取消,触发了异常
except asyncio.CancelledError:
print("main(): cancel_me is cancelled now")
asyncio.run(main())
'''运行结果为:
cancel_me(): before sleep
main函数休息完了
cancel_me(): cancel sleep
cancel_me(): after sleep
main(): cancel_me is cancelled now
'''
运行过程分析:
首先run函数启动主函数入口main,在main中,因为第一句话就是调用异步函数cancel_me()函数,所以先打印出了第一句话;
然后进入cancel_me中的try语句,遇到await,暂停,这时候返回main中执行,但是有在main中遇到了await,也会暂停,但是由于main中只需要暂停1秒,而cancel_me中要暂停3600秒,所以等到main的暂停结束后,接着运行main,所以打印出第二句话;
接下来遇到取消任务的请求task.cancel(),然后继续执行main里面的try,又遇到了await,接着main进入暂停,接下来进入到cancel_me函数中,但是由于main中请求了取消任务,所以那个耗时3600秒的任务就不再执行了,直接触发了Cancelled_Error异常,打印出第三句话,接下来又raise一个异常信息;
接下来执行cancel_me的finally,打印出第四句话,此时cancel_me执行完毕,由于他抛出了一个异常,返回到主程序main中,触发异常,打印出第五句话。
(2)done
()
当一个被包装得协程既没有触发异常、也没有被取消的时候,意味着它是done的,返回true。
(3)result
()
返回任务的执行结果,
当任务被正常执行完毕,则返回结果;
当任务被取消了,调用这个方法,会触发CancelledError异常;
当任务返回的结果是无用的时候,则调用这个方法会触发InvalidStateError;
当任务出发了一个异常而中断,调用这个方法还会再次触发这个使程序中断的异常。
(4)exception
()
返回任务的异常信息,触发了什么异常,就返回什么异常,如果任务是正常执行的无异常,则返回None;
当任务被取消了,调用这个方法会触发CancelledError异常;
当任务没有做完,调用这个方法会触发InvalidStateError异常。
下面还有一些不常用的方法,如下:
(5)add_done_callback
(callback, *, context=None)
(6)remove_done_callback
(callback)
(7)get_stack
(*, limit=None)
(8)print_stack
(*, limit=None, file=None)
(9)all_tasks
(loop=None),这是一个类方法
(10)current_task
(loop=None),这是一个类方法
3、异步函数的结果获取
对于异步编程、异步函数而言,最重要的就是异步函数调用结束之后,获取异步函数的返回值,我们可以有以下几种方式来获取函数的返回值,第一是直接通过Task.result()来获取;第二种是绑定一个回调函数来获取,即函数执行完毕后调用一个函数来获取异步函数的返回值。
(1)直接通过result来获取
import asyncio
import time
async def hello1(a,b):
print("Hello world 01 begin")
await asyncio.sleep(3) #模拟耗时任务3秒
print("Hello again 01 end")
return a+b
coroutine=hello1(10,5)
loop = asyncio.get_event_loop() #第一步:创建事件循环
task=asyncio.ensure_future(coroutine) #第二步:将多个协程函数包装成任务列表
loop.run_until_complete(task) #第三步:通过事件循环运行
print('-------------------------------------')
print(task.result())
loop.close()
'''运行结果为
Hello world 01 begin
Hello again 01 end
-------------------------------------
15
'''
(2)通过定义回调函数来获取
import asyncio
import time
async def hello1(a,b):
print("Hello world 01 begin")
await asyncio.sleep(3) #模拟耗时任务3秒
print("Hello again 01 end")
return a+b
def callback(future): #定义的回调函数
print(future.result())
loop = asyncio.get_event_loop() #第一步:创建事件循环
task=asyncio.ensure_future(hello1(10,5)) #第二步:将多个协程函数包装成任务
task.add_done_callback(callback) #并被任务绑定一个回调函数
loop.run_until_complete(task) #第三步:通过事件循环运行
loop.close() #第四步:关闭事件循环
'''运行结果为:
Hello world 01 begin
Hello again 01 end
15
'''
注意:所谓的回调函数,就是指协程函数coroutine执行结束时候会调用回调函数。并通过参数future获取协程执行的结果。我们创建的task和回调里的future对象,实际上是同一个对象,因为task是future的子类。
三、asyncio异步编程的基本模板
事实上,在使用asyncio进行异步编程的时候,语法形式往往是多样性的,虽然理解异步编程的核心思想很重要,但是实现的时候终究还是要编写语句的,本次给出的模板,是两个不同的例子,例子一是三个异步方法,它们都没有参数,没有返回值,都模拟一个耗时任务;例子二是三个异步方法,都有参数,都有返回值。
1、python3.7之前的版本
(1)例子一:无参数、无返回值
import asyncio
import time
a=time.time()
async def hello1():
print("Hello world 01 begin")
await asyncio.sleep(3) #模拟耗时任务3秒
print("Hello again 01 end")
async def hello2():
print("Hello world 02 begin")
await asyncio.sleep(2) #模拟耗时任务2秒
print("Hello again 02 end")
async def hello3():
print("Hello world 03 begin")
await asyncio.sleep(4) #模拟耗时任务4秒
print("Hello again 03 end")
loop = asyncio.get_event_loop() #第一步:创建事件循环
tasks = [hello1(), hello2(),hello3()] #第二步:将多个协程函数包装成任务列表
loop.run_until_complete(asyncio.wait(tasks)) #第三步:通过事件循环运行
loop.close() #第四步:取消事件循环
'''运行结果为:
Hello world 02 begin
Hello world 03 begin
Hello world 01 begin
Hello again 02 end
Hello again 01 end
Hello again 03 end
'''
(2)例子二:有参数、有返回值
import asyncio
import time
async def hello1(a,b):
print("Hello world 01 begin")
await asyncio.sleep(3) #模拟耗时任务3秒
print("Hello again 01 end")
return a+b
async def hello2(a,b):
print("Hello world 02 begin")
await asyncio.sleep(2) #模拟耗时任务2秒
print("Hello again 02 end")
return a-b
async def hello3(a,b):
print("Hello world 03 begin")
await asyncio.sleep(4) #模拟耗时任务4秒
print("Hello again 03 end")
return a*b
loop = asyncio.get_event_loop() #第一步:创建事件循环
task1=asyncio.ensure_future(hello1(10,5))
task2=asyncio.ensure_future(hello2(10,5))
task3=asyncio.ensure_future(hello3(10,5))
tasks = [task1,task2,task3] #第二步:将多个协程函数包装成任务列表
loop.run_until_complete(asyncio.wait(tasks)) #第三步:通过事件循环运行
print(task1.result()) #并且在所有的任务完成之后,获取异步函数的返回值
print(task2.result())
print(task3.result())
loop.close() #第四步:关闭事件循环
'''运行结果为:
Hello world 01 begin
Hello world 02 begin
Hello world 03 begin
Hello again 02 end
Hello again 01 end
Hello again 03 end
15
5
50
'''
(3)总结:四步走(针对python3.7之前的版本)
第一步·:构造事件循环
loop=asyncio.get_running_loop() #返回(获取)在当前线程中正在运行的事件循环,如果没有正在运行的事件循环,则会显示错误;它是python3.7中新添加的
loop=asyncio.get_event_loop() #获得一个事件循环,如果当前线程还没有事件循环,则创建一个新的事件循环loop;
loop=asyncio.set_event_loop(loop) #设置一个事件循环为当前线程的事件循环;
loop=asyncio.new_event_loop() #创建一个新的事件循环
第二步:将一个或者是多个协程函数包装成任务Task
#高层API
task = asyncio.create_task(coro(参数列表)) # 这是3.7版本新添加的
task = asyncio.ensure_future(coro(参数列表))
#低层API
loop.create_future(coro)
loop.create_task(coro)
'''需要注意的是,在使用Task.result()获取协程函数结果的时候,使用asyncio.create_task()却会显示错
但是使用asyncio.ensure_future却正确,本人暂时不知道原因,哪位大神知道,望告知,不胜感激!'''
第三步:通过事件循环运行
loop.run_until_complete(asyncio.wait(tasks)) #通过asyncio.wait()整合多个task
loop.run_until_complete(asyncio.gather(tasks)) #通过asyncio.gather()整合多个task
loop.run_until_complete(task_1) #单个任务则不需要整合
loop.run_forever() #但是这个方法在新版本已经取消,不再推荐使用,因为使用起来不简洁
'''
使用gather或者wait可以同时注册多个任务,实现并发,但他们的设计是完全不一样的,在前面的2.1.(4)中已经讨论过了,主要区别如下:
(1)参数形式不一样
gather的参数为 *coroutines_or_futures,即如这种形式
tasks = asyncio.gather(*[task1,task2,task3])或者
tasks = asyncio.gather(task1,task2,task3)
loop.run_until_complete(tasks)
wait的参数为列表或者集合的形式,如下
tasks = asyncio.wait([task1,task2,task3])
loop.run_until_complete(tasks)
(2)返回的值不一样
gather的定义如下,gather返回的是每一个任务运行的结果,
results = await asyncio.gather(*tasks)
wait的定义如下,返回dones是已经完成的任务,pending是未完成的任务,都是集合类型
done, pending = yield from asyncio.wait(fs)
(3)后面还会讲到他们的进一步使用
'''
简单来说:async.wait会返回两个值:done和pending,done为已完成的协程Task,pending为超时未完成的协程Task,需通过future.result调用Task的result。而async.gather返回的是已完成Task的result。
第四步:关闭事件循环
loop.close()
'''
以上示例都没有调用 loop.close,好像也没有什么问题。所以到底要不要调 loop.close 呢?
简单来说,loop 只要不关闭,就还可以再运行:
loop.run_until_complete(do_some_work(loop, 1))
loop.run_until_complete(do_some_work(loop, 3))
loop.close()
但是如果关闭了,就不能再运行了:
loop.run_until_complete(do_some_work(loop, 1))
loop.close()
loop.run_until_complete(do_some_work(loop, 3)) # 此处异常
建议调用 loop.close,以彻底清理 loop 对象防止误用
'''
2、python3.7版本
在最新的python3.7版本中,asyncio又引进了一些新的特性和API,
(1)例子一:无参数、无返回值
import asyncio
import time
async def hello1():
print("Hello world 01 begin")
await asyncio.sleep(3) #模拟耗时任务3秒
print("Hello again 01 end")
async def hello2():
print("Hello world 02 begin")
await asyncio.sleep(2) #模拟耗时任务2秒
print("Hello again 02 end")
async def hello3():
print("Hello world 03 begin")
await asyncio.sleep(4) #模拟耗时任务4秒
print("Hello again 03 end")
async def main():
results=await asyncio.gather(hello1(),hello2(),hello3())
for result in results:
print(result) #因为没返回值,故而返回None
asyncio.run(main())
'''运行结果为:
Hello world 01 begin
Hello world 02 begin
Hello world 03 begin
Hello again 02 end
Hello again 01 end
Hello again 03 end
None
None
None
'''
(2)例子二:有参数、有返回值
import asyncio
import time
async def hello1(a,b):
print("Hello world 01 begin")
await asyncio.sleep(3) #模拟耗时任务3秒
print("Hello again 01 end")
return a+b
async def hello2(a,b):
print("Hello world 02 begin")
await asyncio.sleep(2) #模拟耗时任务2秒
print("Hello again 02 end")
return a-b
async def hello3(a,b):
print("Hello world 03 begin")
await asyncio.sleep(4) #模拟耗时任务4秒
print("Hello again 03 end")
return a*b
async def main():
results=await asyncio.gather(hello1(10,5),hello2(10,5),hello3(10,5))
for result in results:
print(result)
asyncio.run(main())
'''运行结果为:
Hello world 01 begin
Hello world 02 begin
Hello world 03 begin
Hello again 02 end
Hello again 01 end
Hello again 03 end
15
5
50
'''
(3)总结:两步走(针对python3.7)
第一步:构建一个入口函数main
他也是一个异步协程函数,即通过async定义,并且要在main函数里面await一个或者是多个协程,同前面一样,我可以通过gather或者是wait进行组合,对于有返回值的协程函数,一般就在main里面进行结果的获取。
第二步:启动主函数main
这是python3.7新添加的函数,就一句话,即
asyncio.run(main())
注意:
不再需要显式的创建事件循环,因为在启动run函数的时候,就会自动创建一个新的事件循环。而且在main中也不需要通过事件循环去掉用被包装的协程函数,只需要向普通函数那样调用即可 ,只不过使用了await关键字而已。
四、协程编程的优点:
1、无cpu分时切换线程保存上下文问题(协程上下文怎么保存)
2、遇到io阻塞切换(怎么实现的)
3、无需共享数据的保护锁(为什么)
4、系列文章下篇预告——介绍低层的API,事件循环到底是怎么实现的以及future类的实现。
上一篇: 开源与新质生产力
下一篇: 什么是 Python 中的 coroutine?如何使用 async 和 await 关键字实现一个 coroutine?如何用 Python 实现简单的异步 RESTful API 客户端?
推荐阅读
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
Python 并发系列 (V) - asyncio 的核心概念和基本架构
-
Python 并发系列 (V) - asyncio 的核心概念和基本架构