我有一个Qt应用程序,它使用GNU/Linux,MacOS和Windows下的各种pcsc
实现连接到读卡器。与卡的所有通信都在工作线程中运行。
在一种情况下,用户开始需要通过读卡器与卡进行通信的操作。读卡器有一个键盘,在身份验证过程中,用户必须在读卡器的键盘上输入他们的 PIN。
此操作通过调用SCardControl()
来实现(例如,请参阅Microsoft文档)。只要用户正在使用读取器,对SCardControl()
的调用就不会终止,并且工作线程就会被它阻止。
此时,用户可能决定在操作仍处于挂起状态时关闭应用程序。此时关闭应用程序会导致应用程序崩溃(在带有信号SIGABRT
的 Linux 上),因为:
- 工作线程被阻塞,等待
SCardControl()
返回。 - 主线程无法停止阻塞的线程:
quit()
和terminate()
都不会导致线程完成。 - 退出应用程序时,将销毁工作线程的
QThread
对象,并且由于线程仍处于运行状态,因此它会引发一个信号来指示错误。
我已经尝试了几种解决方案。
- 子类
QThread
并创建一个工作线程,该线程调用setTerminationEnabled(true);
以允许通过QThread::terminate()
终止。这在MacOS
上不起作用:当QThread
被销毁时,线程仍处于运行状态,并发出信号SIGABRT
。 - 关机时处理信号
SIGABRT
并忽略它。这似乎不是一个好主意,但我想在丢弃它之前尝试一下。忽略信号SIGABRT
后,接收到信号SIGSEGV
,应用程序崩溃。我已经调整了这里描述的方法。 - 尝试通过从主线程向读卡器发送命令来取消阻止线程。我尝试了
SCardCancel()
,SCardDisconnect()
和SCardReleaseContext()
,但这些命令对阻塞的线程没有任何影响。
我觉得很奇怪,当线程在某些函数调用中被阻塞时,不可能完全关闭应用程序,但是我尝试过的所有解决方案都没有奏效,而且我已经没有想法了。我忽略了什么吗?有人有什么有用的提示吗?
编辑
我查看了QThread
的Qt源代码,发现在类Unix平台上,QThread::terminate()
内部使用pthread_cancel()
。但显然pthread_cancel()
不起作用/对Darwin
什么都不做,请参阅例如 这里 和 这里.
因此,也许我真的必须选择向用户显示一个对话框,要求从读卡器中删除卡。
如果在调用中被阻止,则无法从外部完全关闭线程。但是,您可以阻止用户在操作完成之前退出应用程序。
void MainWindow::closeEvent(QCloseEvent *closeEvent) {
if (workerBlocked) closeEvent->ignore();
}
此外,还可以显示一个对话框,告知用户必须先完成操作。
此外,如果可能,您可以让窗口关闭,但通过设置qApp->setQuitOnLastWindowClosed(false);
使应用程序保持活动状态,直到操作完成
问题归结为这样一个事实,即在关联的线程运行时,QThread
对象是不可破坏的。通常,它会在调试输出中像这样打印语句:
QThread:在线程仍在运行时销毁
不要为尝试让SCardControl
返回而苦恼,以便可以安全地退出工作线程(因为只要用户与读取器交互,它就不会返回)。相反,您可以按照此答案以安全的方式销毁QThread
对象,只需对当前实现进行最少的更改。
这里有一个例子来说明我的意思:
#include <QtWidgets>
//a thread that can be destroyed at any time
//see http://stackoverflow.com/a/25230470
class SafeThread : public QThread{
using QThread::run;
public:
explicit SafeThread(QObject* parent= nullptr):QThread(parent){}
~SafeThread(){ quit(); wait(); }
};
//worker QObject class
class Worker : public QObject {
Q_OBJECT
public:
explicit Worker(QObject* parent = nullptr):QObject(parent){}
~Worker(){}
Q_SLOT void doBlockingWork() {
emit started();
//the sleep call blocks the worker thread for 10 seconds!
//consider it a mock call to the SCardControl function
QThread::sleep(10);
emit finished();
}
Q_SIGNAL void started();
Q_SIGNAL void finished();
};
int main(int argc, char* argv[]) {
QApplication a(argc, argv);
//setup worker thread and QObject
Worker worker;
SafeThread thread;
worker.moveToThread(&thread);
thread.start();
//setup GUI components
QWidget w;
QVBoxLayout layout(&w);
QPushButton button("start working");
QLabel status("idle");
layout.addWidget(&button);
layout.addWidget(&status);
//connect signals/slots
QObject::connect(&worker, &Worker::started, &status,
[&status]{ status.setText("working. . .");} );
QObject::connect(&worker, &Worker::finished, &status,
[&status]{ status.setText("idle");} );
QObject::connect(&button, &QPushButton::clicked, &worker, &Worker::doBlockingWork);
w.show();
return a.exec();
}
#include "main.moc"
请注意,SafeThread
的析构函数确保wait()
,直到关联的线程完成执行。只有之后,主线程才能继续调用QThread
的析构函数。