在上一节教程的例子中,我们演示了一个简单的异步编程的例子。
我们请大家首先阅读 Delphi 自带的异步编程模型,那么它存在的问题:
- 生命周期问题:
- 没有和现有的组件体系结合,如果 BeginInvoke 引发的异步调用,返回回调时异步作业的创建者被释放,那么如果我们在其中访问创建者相关的实例,就有可能出现问题。
- 异步操作的回调中,无法检测到依赖的对象是否被释放掉,当然开发人员也就没有办法做进一步处理。
- 异步操作如果需要较长的处理时间,CompletedSynchronously 操作可能长时间无法结束,造成线程被卡死在等待阶段。
- Cancel 的操作是不可预期的,甚至是否支持也取决于异步调用的实现者。
- 实例管理问题:
- 同上面所说,如果要取消或等待异步作业完成,BeginInvoke 的返回值必需保存下来,这在编码时会带来额外的复杂度
TZAsync 考虑到了这些问题,并提供了自己的异步编程解决方案:
- 生命周期问题
- 和组件生命周期管理相结合,在组件释放前,会自动将相应的作业标记为取消。
- 用户可以自维护生命周期,IZAsyncInvoker 创建时可以传递一个 Sender 参数,在需要时,调用 CancelAsyncInvokes 来取消 Sender 关联的一个或全部异步作业
- 如果异步调用中,不依赖其它资源,则不存在需要管理的问题。
- 参数传递推荐使用局部变量或者是值拷贝,传递指针类型时,请审慎处理,保障其在异步调用完成前的有效性,或者在指针类型的实例释放时,主动调用 CancelAsyncInvokes 来通知异步作业不应继续。
- 实例管理问题
- 直接用局部变量就可以,不是必需做为类的成员变量或全局变量。只需要在异步调用完成时,设置局部变量的值指向空来释放引用计数就可以。大家不用担心在异步作业中引用的变量会占用当前线程栈的问题,这个问题我在详解 Delphi 的匿名函数实现 的文章中,进行了阐述,有兴趣的朋友可以回看那篇文章。
- 不试图硬性中止异步作业的执行,而是由通过判断 IsCanceled 标志位来中止作业的执行,来保证不会访问被释放的对象,从而避免内存泄露等问题的发生。
综上所述,虽然 ZAsyncInvoker 提供了一个良好的异步编程框架,但它并不能帮你解决所有的的问题。良好的异步编程习惯还是很重要的。
- 一般情况下,因为 TZAsyncProc 是一个匿名函数,直接使用局部变量就能满足参数传递需要。当然也可以用 TZNamedParams 类型来传递参数,但无论如何,一旦用到指针的参数,请再三确认其生命周期。
- 尽量不要试图强行中止一个后台线程作业,因为那可能带来内存泄露。通过在其中必要的位置加入 IsCanceled 判断,可以及时的中止后续代码执行并安全退出。
- 尽量避免长时间作业,如果不能避免,那么:
- 不要在主线程中等待作业完成,那样可能会造成程序没有响应
- 不要在代码中不做任何判断就访问可能随时被释放的资源,那样可能会造成 AV 错误。