27.4. 异步命令处理 #
PQexec函数足以满足普通同步应用程序提交命令的需要。不过,它有两个缺点,对某些用户可能很重要:
PQexec会等待命令完成。应用可能有其他工作要做(例如维护一个用户界面),这种情况下它不会想要阻塞等待响应。因为客户端应用的执行在它等待结果时会被挂起,对于应用来说很难决定要不要尝试取消正在进行的命令(这可以在一个信号处理器中完成,但别无他法)。
PQexec只能返回一个PGresult结构体。如果提交的命令串包含多个SQL命令,除了最后一个PGresult之外都会被PQexec丢弃。
不喜欢这些限制的应用可以改用构成 PQexec 的底层函数:PQsendQuery 和
PQgetResult。还有
PQsendQueryParams 和
PQsendQueryPrepared,它们与
PQgetResult 配合使用,可分别实现
PQexecParams 和 PQexecPrepared
的功能。
PQsendQuery向服务器提交一个命令而不等待结果。命令成功发出返回 1,否则返回 0(此时用
PQerrorMessage获取失败的更多信息)。int PQsendQuery(PGconn *conn, const char *command);
成功调用
PQsendQuery后,调用一次或多次PQgetResult获取结果。在PQgetResult返回空指针(表示命令已完成)之前,不得(在同一连接上)再次调用PQsendQuery。PQsendQueryParams向服务器提交一个命令和独立的参数而不等待结果。
int PQsendQueryParams(PGconn *conn, const char *command, int nParams, const Oid *paramTypes, const char * const *paramValues, const int *paramLengths, const int *paramFormats, int resultFormat);此函数等价于
PQsendQuery,区别在于查询参数可以与查询字符串分开指定。其函数参数的处理方式与PQexecParams完全相同。与PQexecParams一样,它不能用于 2.0 协议的连接,且查询字符串中只允许一个命令。PQsendQueryPrepared发送一个以给定参数执行预备语句的请求,而不等待结果。
int PQsendQueryPrepared(PGconn *conn, const char *stmtName, int nParams, const char * const *paramValues, const int *paramLengths, const int *paramFormats, int resultFormat);此函数类似于
PQsendQueryParams,但要执行的命令通过命名一个先前已准备好的语句来指定,而不是给出查询字符串。其函数参数的处理方式与PQexecPrepared完全相同。与PQexecPrepared一样,它不能用于 2.0 协议的连接。PQgetResult等待先前一次
PQsendQuery、PQsendQueryParams或PQsendQueryPrepared调用的下一个结果并将其返回。命令完成且不再有结果时返回空指针。PGresult *PQgetResult(PGconn *conn);
必须反复调用
PQgetResult直到它返回空指针,表示命令已完成。(如果在没有活跃命令时调用,PQgetResult会立即返回空指针。)对PQgetResult返回的每个非空结果,应使用前文所述的同一批PGresult访问函数处理。用完每个结果对象后别忘了用PQclear释放。注意,只有当命令处于活跃状态且必要的响应数据尚未被PQconsumeInput读取时,PQgetResult才会阻塞。
使用PQsendQuery和PQgetResult解决了PQexec的一个问题:如果命令字符串包含多个SQL命令,就能分别获取这些命令的结果。(这也支持一种简单的重叠处理方式:客户端可以处理某条命令的结果,同时服务器继续处理同一命令字符串中后面的查询。)然而,调用PQgetResult仍会导致客户端阻塞直到服务器完成下一条SQL命令。通过恰当使用另外两个函数可以避免这一点:
-
PQconsumeInput 如果服务器的输入可用,消费它。
int PQconsumeInput(PGconn *conn);
PQconsumeInput通常返回 1,表示“没有错误”;发生问题时则返回 0(此时可查看PQerrorMessage)。注意,返回值并不说明是否实际读取了输入数据。调用PQconsumeInput后,应用程序可以检查PQisBusy和/或PQnotifies,以确定其状态是否发生变化。即使应用程序尚未准备好处理结果或通知,也可以调用
PQconsumeInput。此函数会读取可用数据并将其保存在缓冲区中,从而清除select()的可读就绪指示。因此,应用程序可以用PQconsumeInput立即清除select()的就绪条件,随后在合适的时候检查结果。-
PQisBusy 如果命令仍在忙碌,则返回 1,意味着
PQgetResult会阻塞等待输入。返回 0 则表示可以调用PQgetResult,并保证不会阻塞。int PQisBusy(PGconn *conn);
PQisBusy本身将不会尝试从服务器读取数据,因此必须先调用PQconsumeInput,否则繁忙状态将永远不会结束。
使用这些函数的典型应用程序会在主循环中通过 select() 或 poll() 等待需要响应的各种条件。其中一个条件是服务器有可读取的输入;对于 select(),这意味着 PQsocket 标识的文件描述符上有可读数据。主循环检测到输入就绪时,应调用 PQconsumeInput 读取输入,然后调用 PQisBusy。如果 PQisBusy 返回假(0),就可以接着调用 PQgetResult。还可以调用 PQnotifies 检测 NOTIFY 消息(见第 27.6 节)。
使用
PQsendQuery/PQgetResult
的客户端还可以尝试取消一个仍在服务器中处理的命令。
PQrequestCancel请求服务器放弃对当前命令的处理。
int PQrequestCancel(PGconn *conn);
取消请求成功发出则返回 1,否则返回 0。(若为 0,
PQerrorMessage会说明原因。)但成功发出并不能保证请求会生效。无论PQrequestCancel的返回值如何,应用都必须继续用PQgetResult按正常顺序读取结果。如果取消生效,当前命令会提前终止并返回一个错误结果。如果取消失败(例如因为服务器已完成该命令的处理),则根本不会有可见的结果。注意,如果当前命令是事务块的一部分,取消将中止整个事务。
PQrequestCancel可以在信号处理器中安全地调用。因此,如果取消的决定可以在信号处理器中做出,也可以将它与普通的PQexec配合使用。例如,psql 从SIGINT信号处理器中调用PQrequestCancel,从而允许交互式地取消它通过PQexec发出的命令。
使用上述函数可以避免在等待数据库服务器输入时阻塞。不过,应用程序仍可能在等待向服务器发送输出时阻塞。这种情况较少见,但发送很长的 SQL 命令或数据值时可能发生。(如果应用程序通过 COPY IN 发送数据,发生的可能性则大得多。)为了防止这种情况,实现完全非阻塞的数据库操作,可以使用以下附加函数。
PQsetnonblocking设置连接的非阻塞状态。
int PQsetnonblocking(PGconn *conn, int arg);
如果
arg为1,则将连接状态设置为非阻塞,如果arg为0,则设置为阻塞。如果成功返回0,出错返回-1。在非阻塞状态下,对
PQsendQuery、PQputline、PQputnbytes和PQendcopy的调用不会阻塞;如果需要再次调用,它们会返回错误。请注意,
PQexec不遵守非阻塞模式;如果调用它,它将以阻塞方式执行。PQisnonblocking返回数据库连接的阻塞状态。
int PQisnonblocking(const PGconn *conn);
如果连接设置为非阻塞模式,则返回1,如果为阻塞,则返回0。
PQflush尝试将发送队列中的输出数据发送到服务器。成功(或发送队列为空)时返回 0;因某种原因失败时返回 -1;如果尚未能发送队列中的全部数据,则返回 1(这种情况只可能发生在非阻塞连接上)。
int PQflush(PGconn *conn);
在非阻塞连接上发送任何命令或数据之后,调用
PQflush。如果返回 1,等待套接字变为可写后再次调用它;重复直到返回 0。一旦
PQflush 返回 0,等待套接字变为可读,然后按前述方法读取响应。