34.4. 异步命令处理#
对于在常规同步应用程序中提交命令而言,PQexec
函数是足够的。但是,它有一些缺陷,对于某些用户而言可能很重要
PQexec
等待命令完成。应用程序可能还有其他工作要做(例如维护用户界面),在这种情况下,它不想阻塞等待响应。由于客户端应用程序在等待结果时被挂起,因此应用程序很难决定是否要尝试取消正在进行的命令。(可以从信号处理程序中完成,但不能以其他方式完成。)
PQexec
只能返回一个PGresult
结构。如果提交的命令字符串包含多个 命令,则除了最后一个PGresult
外,所有PGresult
都将被PQexec
丢弃。PQexec
始终收集命令的整个结果,并将其缓存在单个PGresult
中。虽然这简化了应用程序的错误处理逻辑,但对于包含许多行的结果而言,这可能不切实际。
不喜欢这些限制的应用程序可以使用PQexec
构建的基础函数:PQsendQuery
和PQgetResult
。还有PQsendQueryParams
、PQsendPrepare
、PQsendQueryPrepared
、PQsendDescribePrepared
和PQsendDescribePortal
,可以与PQgetResult
一起使用,以复制PQexecParams
、PQprepare
、PQexecPrepared
、PQdescribePrepared
和PQdescribePortal
的功能。
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
一样,它只允许在查询字符串中使用一个命令。PQsendPrepare
#发送一个请求以创建带有给定参数的已准备语句,而不等待完成。
int PQsendPrepare(PGconn *conn, const char *stmtName, const char *query, int nParams, const Oid *paramTypes);
这是
PQprepare
的异步版本:如果能够分派请求,则返回 1,否则返回 0。成功调用后,调用PQgetResult
以确定服务器是否成功创建已准备语句。该函数的参数与PQprepare
的处理方式相同。PQsendQueryPrepared
#发送一个请求以执行带有给定参数的已准备语句,而不等待结果。
int PQsendQueryPrepared(PGconn *conn, const char *stmtName, int nParams, const char * const *paramValues, const int *paramLengths, const int *paramFormats, int resultFormat);
这类似于
PQsendQueryParams
,但要执行的命令是通过命名先前准备的语句来指定的,而不是给出一个查询字符串。该函数的参数与PQexecPrepared
的处理方式相同。PQsendDescribePrepared
#提交一个请求以获取有关指定已准备语句的信息,而不等待完成。
int PQsendDescribePrepared(PGconn *conn, const char *stmtName);
这是
PQdescribePrepared
的异步版本:如果能够分派请求,则返回 1,否则返回 0。成功调用后,调用PQgetResult
以获取结果。该函数的参数与PQdescribePrepared
的处理方式相同。PQsendDescribePortal
#提交一个请求以获取有关指定门户的信息,而不等待完成。
int PQsendDescribePortal(PGconn *conn, const char *portalName);
这是
PQdescribePortal
的异步版本:如果能够分派请求,则返回 1,否则返回 0。成功调用后,调用PQgetResult
以获取结果。该函数的参数与PQdescribePortal
的处理方式相同。PQgetResult
#等待来自先前的
PQsendQuery
、PQsendQueryParams
、PQsendPrepare
、PQsendQueryPrepared
、PQsendDescribePrepared
、PQsendDescribePortal
或PQpipelineSync
调用的下一个结果,并返回它。当命令完成且没有更多结果时,将返回一个空指针。PGresult *PQgetResult(PGconn *conn);
PQgetResult
必须重复调用,直到它返回一个空指针,表示命令已完成。(如果在没有活动命令时调用,PQgetResult
将立即返回一个空指针。)PQgetResult
的每个非空结果都应使用前面描述的相同PGresult
访问器函数进行处理。完成后,不要忘记使用PQclear
释放每个结果对象。请注意,PQgetResult
仅在命令处于活动状态且必要的响应数据尚未被PQconsumeInput
读取时才会阻塞。在管道模式中,
PQgetResult
将正常返回,除非发生错误;对于在导致错误的查询之后发送的任何后续查询,直到(不包括)下一个同步点,将返回类型为PGRES_PIPELINE_ABORTED
的特殊结果,并且之后将返回一个空指针。当达到管道同步点时,将返回类型为PGRES_PIPELINE_SYNC
的结果。同步点之后的下一个查询的结果紧随其后(即,在同步点之后不会返回空指针)。注意
即使
PQresultStatus
指示出现致命错误,也应调用PQgetResult
,直到它返回一个空指针,以允许 libpq 完全处理错误信息。
使用PQsendQuery
和PQgetResult
可以解决PQexec
的一个问题:如果一个命令字符串包含多个SQL命令,则可以单独获取这些命令的结果。(顺便说一下,这允许一种简单的重叠处理形式:客户端可以在服务器仍在处理同一命令字符串中的后续查询时处理一个命令的结果。)
另一个可以使用PQsendQuery
和PQgetResult
获得的经常需要的特性是一次检索一行大型查询结果。这在第 34.6 节中进行了讨论。
单独调用PQgetResult
仍会导致客户端阻塞,直到服务器完成下一个SQL命令。通过正确使用另外两个函数可以避免这种情况
PQconsumeInput
#如果服务器有可用的输入,则使用它。
int PQconsumeInput(PGconn *conn);
PQconsumeInput
通常返回 1 表示 “无错误”,但如果有某种问题,则返回 0(在这种情况下,可以查阅PQerrorMessage
)。请注意,结果不会说明是否实际收集了任何输入数据。在调用PQconsumeInput
之后,应用程序可以检查PQisBusy
和/或PQnotifies
以查看其状态是否已更改。即使应用程序尚未准备好处理结果或通知,也可以调用
PQconsumeInput
。该函数将读取可用数据并将其保存在缓冲区中,从而导致select()
就绪指示消失。因此,应用程序可以使用PQconsumeInput
立即清除select()
条件,然后从容检查结果。PQisBusy
#如果命令繁忙,即
PQgetResult
将阻塞等待输入,则返回 1。返回 0 表示可以调用PQgetResult
并确保不会阻塞。int PQisBusy(PGconn *conn);
PQisBusy
本身不会尝试从服务器读取数据;因此,必须先调用PQconsumeInput
,否则繁忙状态将永远不会结束。
使用这些函数的典型应用程序将具有一个主循环,该主循环使用select()
或poll()
来等待它必须响应的所有条件。其中一个条件将是来自服务器的可用输入,就select()
而言,这意味着由PQsocket
标识的文件描述符上的可读数据。当主循环检测到输入就绪时,它应该调用PQconsumeInput
来读取输入。然后,它可以调用PQisBusy
,如果PQisBusy
返回 false (0),则调用PQgetResult
。它还可以调用PQnotifies
来检测NOTIFY
消息(请参见第 34.9 节)。
使用PQsendQuery
/PQgetResult
的客户端还可以尝试取消仍在由服务器处理的命令;请参见第 34.7 节。但是,无论PQcancel
的返回值如何,应用程序都必须使用PQgetResult
继续执行正常的读取结果序列。取消成功只会导致命令比预期提前终止。
通过使用上面描述的函数,可以在等待来自数据库服务器的输入时避免阻塞。但是,应用程序仍然有可能阻塞等待向服务器发送输出。这种情况相对少见,但如果发送非常长的 SQL 命令或数据值,则可能会发生这种情况。(不过,如果应用程序通过COPY IN
发送数据,则更有可能发生这种情况。)为了防止这种情况并实现完全非阻塞的数据库操作,可以使用以下附加函数。
PQsetnonblocking
#设置连接的非阻塞状态。
int PQsetnonblocking(PGconn *conn, int arg);
如果
arg
为 1,则将连接状态设置为非阻塞,如果arg
为 0,则设置为阻塞。如果成功,则返回 0,如果出错,则返回 -1。在非阻塞状态下,对
PQsendQuery
、PQputline
、PQputnbytes
、PQputCopyData
和PQendcopy
的成功调用不会阻塞;它们的更改会存储在本地输出缓冲区中,直到它们被刷新。不成功的调用将返回一个错误,并且必须重试。请注意,
PQexec
不支持非阻塞模式;如果调用它,它无论如何都会以阻塞方式执行。PQisnonblocking
#返回数据库连接的阻塞状态。
int PQisnonblocking(const PGconn *conn);
如果连接设置为非阻塞模式,则返回 1,如果设置为阻塞,则返回 0。
PQflush
#尝试将任何排队的输出数据刷新到服务器。如果成功(或发送队列为空),则返回 0,如果由于某些原因失败,则返回 -1,或者如果它无法发送发送队列中的所有数据(这种情况只可能发生在连接是非阻塞的情况下),则返回 1。
int PQflush(PGconn *conn);
在非阻塞连接上发送任何命令或数据后,请调用PQflush
。如果它返回 1,请等待套接字变为可读或可写。如果它变为可写,请再次调用PQflush
。如果它变为可读,请调用PQconsumeInput
,然后再次调用PQflush
。重复此操作,直到PQflush
返回 0。(有必要检查可读性并使用PQconsumeInput
清除输入,因为服务器可能会阻止尝试向我们发送数据,例如 NOTICE 消息,并且在读取其数据之前不会读取我们的数据。)一旦PQflush
返回 0,请等待套接字变为可读,然后按照上述说明读取响应。