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