| 操作系统 办公 实用知识 设计 开发 WEB开发 移动开发 数据库 软件工程 网管 安全 管理 信息化 答疑 渠道 |
网络和套接字指导原则线程问题 开发者在 Windows 或 Unix 等平台上的应用程序中使用网络时,通常会使用编块调用。这种调用将会在操作完成或失败后才返回。例如,常用的编块写调用将在全部数据成功发送或出错后才会返回。同样,许多程序员用于编块套接字的 fgets() 等调用也会等待至整行接收完成后才返回。 编块套接字类似于对本地 I/O 编块调用(如磁盘读写),因此被广泛使用。但网络与本地磁盘 I/O 有所不同,它本质上会受不可预见的延迟的制约。这些延迟可能会在任何操作中发生,并可能持续数分钟。 要在编块调用时防止应用程序锁定和不响应用户或其它事件,开发人员通常会使用线程,一般是每个网络连接一个线程。线程与编块调用一样,具有熟悉的线性范型的优点,但具有开销大且较为复杂的缺点。线程除了会消耗内存和其它系统资源之外,还会消耗其它不太明显的资源(如用于维持状态、同步等的结构空间)。线程会使应用程序变得非常复杂,从而导致代码和内存用量增加,而越复杂就越容易出现缺陷。此外,访问共享对象时线程还会导致并发性问题,而此领域本身就存在很多缺陷。 BREW™ 的回调不编块策略在一种考虑使用更简单程序的模型中提供等效功能,因此更容易写入和调试(因而更加稳定),而且使用的资源更少。 因为 BREW 不支持线程,所以 BREW API 相对较简单(无需调用创建、破坏、开始、停止和同步等线程)。BREW 层本身更小、更高效、更简单(因而也更可靠)。因此,将 BREW 移植到新的芯片集和手持设备时所需时间会更短、成本更低、工作量更小。 应用程序(在很多情况下,BREW 层本身)会通过避免各种多线程问题而受益。问题包括: 对共享数据的多线程访问导致极难察觉的故障 难以(或无法)充分测试多线程代码,尤其是在跨开发环境中 堵塞在编块调用中的线程的响应性问题 - 通常这些情况中的某些情况被忽视,因此,线程(依次为应用程序)无法不经某些随意的延迟而安全终止。这是移动手持设备资源严格受限的环境中存在的问题。 一般来说,不编块实施使用的内存更少(尤其包括栈内存),而且处理中断更加容易和简洁。 与不编块 API 相关的一个潜在困难包括处理为编块 API 编写的大块现有代码。但好在任何编块网络代码都可以“机械地”转换成不编块版本。它不是当前有软件执行此操作意义上的机械,而是进程可以体现在象编译器一样复杂的软件中意义上的机械。这种转换可能很耗时,但只要开发者清楚地了解并重视基本的等效概念,即使是较大的代码库也可以成功转换。 转换不编块代码 转换过程使用面向对象术语进行描述,但可以根据需要用 C 或 C++ 实施。 每个编块函数 - 即直接或间接等待外部事件的函数,可以转换为有三个或三个以上方法的对象(成员函数): 构造函数 析构函数 功函数或函数 以下操作应该可以确保有效转换: 定义对象/结构来保持持久状态。对于所有有值本地变量,如果其寿命值跨越了编块操作,则必须将这些变量移至对象(可能包括原始函数的自变量)。因为此对象是在堆上分配的,所以其成员在控件返回仍然保留。 创建一个“新”函数以分配和初始化状态对象。此函数以传递到原始编块函数的参数为参数。它还采用两个新参数(函数指针和空指针)来描述完成时要调用的回调。 创建一个“功”函数,包含原始编块函数的主体部分。它使用一个参数: 一个状态对象指针。此功函数由构造函数调度或调用。 功函数是一个状态机: 它使用当前状态值来执行开关语句。它包含每个状态的情况语句标记。由原始代码中的编块调用分隔的每个操作系列有一个状态(和其它看来有用的状态)。 调用后,功函数会继续处理原始函数的任务,直到它必须等待事件时为止(例如完成网络连接)。然后,它会将其状态保存到对象,并请求从适当机制中回调并返回。如果再次调用,它会从上次停止的位置继续(使用对象中存储的状态值作为其开关语句的索引)。这要求对此函数主体进行如下修改: 每个编块调用都会被代码序列所替换,该代码可以启动异步操作、调度完成时要回调的功函数本身、记录当前状态并将控件返回调用程序。每个状态都具有情况语句标记。 例如: WaitonKeypress(); 变成: KeypressNotify ( me, Object_Work ); 通知回调函数与功函数的原型不匹配的情况下,可以使用一个较小的助手函数。该助手函数可以匹配通知回调函数原型且只调用功函数。例如,BREW Connect() 回调传递结果值,助手函数会在状态对象中储存此结果值,并调用功函数。 每个以前编块的调用都被赋予了一个状态值。在功函数开始处,使用状态变量的开关将控制流导向相应的标记。根据情况,可以开关语句对每个状态只执行一条 goto 语句。这可以使您保持编块代码的原始结构(包括“for”和“while”循环)。不过,将代码移动到开关语句一般更容易进行保持和验证。(对于代码较长的情况,您可以将其移至 INLINE 函数。) 示例: switch ( me->nState ) 功函数通常在事件完成时由 BREW 调用,而原始函数仅在原始应用程序执行流程中被调用。因此,功函数会调用一个回调函数来通知其客户端(“调用程序”),而不是退出将控件返回调用程序并恢复原始控制流(如编块函数)。如果原始函数返回一个值,则最好做如下处理:向构造函数传递指向结果存储位置的指针。 “delete”函数会销毁该对象(在任何时候停止操作),释放分配的所有资源并取消可能已调度的任何操作。此函数也应该由功函数调用,以便在调用客户端回调函数前清除任何资源。 因为 BREW 没有线程和可重入功能,所以 delete 函数只可在功函数未激活时(已调度回调函数并退出)由客户端调用。这样就简化了 delete 和功函数,因为功函数不需为执行取消操作而锁定任何内容或进行测试,而 delete 函数只需清除(包括取消功函数请求的回调函数)和释放资源。 客户端(原始调用程序)可能会响应按“取消”按钮的用户,直接调用 delete 函数,中途中断操作。 转换示例 持久变量(绿色) 编块/不编块调用转换(蓝色) 不编块版本的新变量/代码(红色) 原始编块版本: 这是一个执行时间较长的编块函数。它省略了一些 ''#define'' 和出错检查并隐藏了许多无关的功能。“空”函数简化了此实例(有返回值的函数首先应转换成空函数。) void QueryDNS (const char *pszDomain, INAddr *paddrResult ) pcReq = malloc ( 300 ); do sendto ( s, DNSADDR, DNSPORT, pcReq, cbReq ); if ( nRcvd > 0 ) free ( pcReq ); 结果: 结果即为以下类定义(此处使用 C 代码并遵守面向对象的约定)。采用了 goto 形式,而不是将代码嵌入 switch 语句,因为它允许继续使用更多的原始控制流(包括循环),所以可以更清楚地解释转换。但如果不采用 goto 形式而是将代码移入 switch 语句会更加简洁,便于维护和调试。 请注意,此代码段有大量未执行的显著优化操作(例如,可以通过将状态 0 功函数移至 New 函数取消状态变量和开关;两个内存分配可以合并为一个;可以取消某些持久变量)。因为本例用于说明转换过程的一致性,所以省略了这些优化操作。 对于异步套接字操作,使用完成时回调接口,而不是 BREW 套接字 API 的准备重试时回调接口,以说明更具有一般性的情况。但区别很小。 typedef struct // 创建对象;初始化代码都在这里 me->pcReq = malloc ( 300 ); QueryDNS_Work ( me ); // 删除对象;清除代码都在这里 void QueryDNS_Work ( void *pvCxt ) switch ( me->nState ) do // recvfrom_timeout_asynch: 成功或超时后 querydns_st_2: } if ( me->nRcvd > 0 ) me->pAllDone ( me->ppClientPtr ); 用法差异: 调用程序不调用 QueryDNS() 并期望它返回时结果有效,而是使用: me->pqdns = QueryDNS_New ( pszDomain, &me->addr, 调用 MyObj_DNSDone() 时,me->addr 会保存结果。作为收尾,调用程序应正确支持取消操作。这包括: 调用回调函数时将 me->pqdns 设置为 NULL。 取消时,执行以下操作: if ( me->pqdns != NULL ) 请注意原始的编块版本不支持取消操作。对编块 C/C++ 环境中中断的正确支持涉及其它变量及在每一直接或间接编块的调用后对它们的明确测试、中断系统级等待操作(如 connect())的 OS 特定方法,以及跟踪需要中断的操作的其它“基础结构”(如 connect() 调用被埋藏在几级函数调用之下。)
今日推荐
|
重点推荐
领军企业技术文库
+更多领军技术文库
最新专题
电子杂志订阅
| ||||||||