C++:如何异步调用同步库调用

C++: How to call a synchronous library call asynchronously?

本文关键字:调用 同步 异步 何异步 C++      更新时间:2023-10-16

我使用的库有一个阻塞调用,如果不成功,它永远不会超时。我希望能够更优雅地处理这种错误情况。我知道必须有一种方法将调用封装在工作线程(或其他类型的委托对象)中,等待x秒,然后在x秒过去时抛出异常。我只需要为库中的一个函数执行此操作。我该如何着手实现这一点?我在网上看到了类似的例子,但没有一个正是我想要做的。谢谢!

我的答案是"不要尝试这样做"。

当然,你可能会发现一些黑客在你的特定情况下会起作用。但是这里的比赛条件很难解决。

显而易见的方法是让线程A进行阻塞调用,然后设置线程B在超时时杀死A

但是。。。如果超时在A从阻塞调用返回的同时到期,该怎么办?具体来说,如果B认为是时候杀死A了,那么您的操作系统调度程序决定运行A一段时间,然后您的操作程序决定运行杀死AB代码,该怎么办?

一句话:你最终会在A执行过程中的某个不确定点杀死它。(例如,也许它刚刚从储蓄账户中扣除了500美元,但还没有向支票账户中增加500美元。可能性是无限的…)

好吧,这样您就可以让线程A存在,其唯一目的是运行库调用,然后在调用结束时发出条件或其他信号。至少在原则上这是可行的。但即便如此,如果库本身有一些内部状态处于不一致状态,A是否会在不合时宜的时刻被杀死呢?

C++11标准中省略了异步线程取消是有充分理由的。只需说不。修复库例程。无论花费多少,从长远来看,它几乎肯定比你正在尝试的更便宜。

使用C++11,然后为该调用显式启动线程,看起来可能是:

// API
T call(int param);
// asynchronous call with 42 as parameter
auto future = std::async(std::launch::async, call, 42);
// let's wait for 40 ms
auto constexpr duration = std::chrono::milliseconds(40);
if(future.wait_for(duration) == std::future_status::timeout) {
    // We waited for 40ms and had a timeout, now what?
} else {
    // Okay, result is available through future.get()
    // if call(42) threw an exception then future.get() will
    // rethrow that exception so it's worth doing even if T is void
    future.get();
}

正如你所看到的,在超时的情况下,你会遇到一个大问题,因为你永远都被阻塞的线程卡住了。这可以说不是C++11std::future的错:相当数量的线程抽象最多只能提供协作取消,但这仍然不足以拯救您。

如果你没有使用C++11,那么Boost.Thread与boost::unique_future有一个非常相似的接口(其中wait_fortimed_wait,并返回bool),尽管它没有类似于std::async的东西,所以你必须自己做一些繁忙的工作(例如boost::packaged_task+boost::thread)。文档中提供了详细信息。

显然,进行阻塞调用的线程不能自行终止,它将被阻塞。

一种方法是启动进行阻塞调用的线程a,然后启动另一个在超时期间休眠的线程B,然后杀死线程a。受互斥体保护的共享标志可以指示操作是否成功,基于此可以引发异常。

第二种方法(非常类似)是启动线程A,线程A依次启动线程B,休眠超时,然后杀死线程B。

首选线程库的细节(例如允许哪些线程相互杀死)和阻塞函数的性质将确切地影响您如何执行此操作。

在Windows上,您会想要做这样的事情:

//your main thread
DWORD threadID;
HANDLE h = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&ThreadProc, 0, 0, &threadID);
DWORD ret = 0xFFFFFF;
for (int i = 0; i < /*some timeout*/; i++) {
    ret = WaitForSingleObject(h, 100);
    if (ret == WAIT_OBJECT_0) break;
}
if (ret != WAIT_OBJECT_0) {
    DWORD exitCode;
    TerminateThread(h, &exitCode); // you will want to stop the thread as it isn't exiting.
    /*throw*/;
}

//Thread Routine
DWORD ThreadProc (LPVOID threadParam) {
    //call your function here
    return 0;
}

这里的想法是旋转一个线程来做你想要的工作。然后您可以在中等待该线程100毫秒的增量(或任何您想要的)。如果它没有在特定的时间段内结束,您可以抛出一个异常。

存在一些问题。首先,库是否保持任何内部状态,这些状态将因库调用失败而不可用?如果是这样的话,你就会被绊倒,因为被阻止的失败调用之后的调用也会失败,或者更糟的是,在没有任何异常或其他通知的情况下生成错误的结果。

如果库是安全的,那么您确实可以尝试线程关闭调用,并等待某个超时事件。现在你需要处理@Nemo的担忧——你需要注意如何处理结果的返回。具体如何做到这一点取决于,嗯,您打算如何从调用库的线程返回结果。通常,两个线程都会进入一个关键部分,在返回结果的lib线程和指示lib线程永远不返回任何东西的超时线程之间进行安全仲裁(例如,通过在其中设置标志),如果lib调用返回,则退出。

孤立自由。线程就是这样一种方式,如果lib调用永远不会返回,就会导致线程泄漏。您是否能够吸收此类泄漏,或者安全地最终强制终止孤立线程,取决于您和您的应用程序:)