非阻塞工作者 - 中断文件复制

2021-12-09 00:00:00 qt c++ qfile qt4.8

我正在处理非常大的文件,大小超过数百 GB.用户需要能够在磁盘之间移动这些文件,并且在没有默认文件管理器的受限系统上.用户有可能意识到他们犯了一个错误并取消了操作,据我所知,用户将不得不等待当前的复制或重命名操作完成.这可能会让他们感到沮丧,因为他们可能等待几分钟,却发现他们的许多 GB 文件仍在复制.在 Copy 的情况下,我可以删除第二个文件,但在我用来移动文件的重命名的情况下,我必须反向重复该操作才能撤消它,这是不可接受的.

有什么方法可以中断我在 QFile 文档中没有看到的 copy() 和 rename(),还是我需要将自己的类放在一起来处理复制和重命名?

解决方案

我认为文件大小对重命名需要多长时间没有任何影响.

对于副本 - Qt 没有提供任何内置功能,您必须自己实现它.这里的关键问题是您必须找到某种方法来连续轮询副本取消.这意味着你不能为了能够处理事件而锁定主线程.

无论您是为了保持主线程响应而使用额外线程,还是决定使用主线程 - 在这两种情况下,您都需要实现分段"复制 - 使用缓冲区一次一个块,直到文件被复制或复制被取消.您需要它来处理用户事件并跟踪复制进度.

我建议你实现一个 QObject 派生的复制助手工作类,它跟踪文件名、总大小、缓冲区大小、进度和取消时的清理.那么是选择在主线程中使用还是在专用线程中使用就是一个选择了.

找到了,但你最好仔细检查一下,因为它是作为一个例子完成的并且没有经过彻底的测试:

class CopyHelper : public QObject {Q_OBJECTQ_PROPERTY(qreal 进度读取进度写入设置进度通知进度更改)民众:CopyHelper(QString sPath, QString dPath, quint64 bSize = 1024 * 1024) :isCancelled(false), bufferSize(bSize), prog(0.0), source(sPath), destination(dPath), position(0) { }~CopyHelper() { free(buff);}qreal progress() const { return prog;}void setProgress(qreal p) {如果 (p != prog) {编 = p;发出progressChanged();}}公共插槽:无效开始(){if (!source.open(QIODevice::ReadOnly)) {qDebug() <<无法开源,中止";发出完成();返回;}文件大小 = source.size();if (!destination.open(QIODevice::WriteOnly)) {qDebug() <<无法打开目的地,正在中止";//也许检查覆盖并要求继续发出完成();返回;}如果 (!destination.resize(fileSize)) {qDebug() <<无法调整大小,正在中止";发出完成();返回;}buff = (char*)malloc(bufferSize);如果(!buff){qDebug() <<无法分配缓冲区,正在中止";发出完成();返回;}QMetaObject::invokeMethod(this, "step", Qt::QueuedConnection);//timer.start();}无效步骤(){如果 (!isCancelled) {如果(位置<文件大小){quint64 块 = 文件大小 - 位置;quint64 l = 块 >缓冲区大小 ?缓冲区大小:块;source.read(buff, l);目的地.write(buff, l);位置 += l;source.seek(位置);目的地.寻求(位置);setProgress((qreal)position/fileSize);//std::this_thread::sleep_for(std::chrono::milliseconds(100));//用于检测QMetaObject::invokeMethod(this, "step", Qt::QueuedConnection);} 别的 {//qDebug() <<timer.elapsed();发出完成();返回;}} 别的 {if (!destination.remove()) qDebug() <<"删除失败";发出完成();}}无效取消(){ isCancelled = true;}信号:void progressChanged();无效完成();私人的:bool 已取消;quint64 缓冲区大小;qreal 程序;QFile 源、目标;quint64 文件大小,位置;字符 * 增益;//QElapsedTimer 定时器;};

done() 信号用于deleteLater() 复制助手/关闭复制对话框或其他任何东西.您可以启用经过计时器并使用它来实现经过时间属性和估计时间.暂停是另一个可能实现的功能.使用 QMetaObject::invokeMethod() 允许事件循环定期处理用户事件,以便您可以取消和更新从 0 到 1 的进度.您也可以轻松调整它以移动文件.

I'm dealing with very large files, in excess of hundreds of GB in size. The User needs to be able to move these files between disks and is on a restricted system with no default file manager. It's possible for the User to realize they've made a mistake and cancel the operation, and as far as I can tell the User will have to wait for the current copy or rename operation to complete. This can leave them feeling frustrated as they wait potentially minutes, only to see that their many GB file still copied. In the case of Copy I could delete the second file, but in the case of rename, which I'm using to move files, I'd have to repeat the operation in reverse to undo it, and that's simply not acceptable.

Is there some way to interrupt copy() and rename() that I'm not seeing in the documentation for QFile, or will I need to put together my own class to handle copy and rename?

解决方案

I don't think the file size has any effect on how long a renaming will take.

For the copy - Qt offers nothing built in, you have to implement it yourself. The key gotcha here is that you will have to find some way to poll for a copy cancellation continuously. This means you cannot lock the main thread in order to be able to process events.

Whether you go for an extra thread in order to keep the main thread responsive, or decide to use the main thread - in both cases you will need to implement "fragmented" copying - one chunk at a time using a buffer, until the file is copied or copying is cancelled. You need this to be able to process user events and track copying progress.

I suggest you implement a QObject derived copy helper worker class which tracks file name, total size, buffer size, progress and clean up on cancellation. Then it is a matter of choice whether you will use it in the main thread or in a dedicated thread.

EDIT: Found it, but you better double check it, since it was done as an example and has not been thoroughly tested:

class CopyHelper : public QObject {
    Q_OBJECT
    Q_PROPERTY(qreal progress READ progress WRITE setProgress NOTIFY progressChanged)
public:
    CopyHelper(QString sPath, QString dPath, quint64 bSize = 1024 * 1024) :
        isCancelled(false), bufferSize(bSize), prog(0.0), source(sPath), destination(dPath), position(0) { }
    ~CopyHelper() { free(buff); }

    qreal progress() const { return prog; }
    void setProgress(qreal p) {
        if (p != prog) {
            prog = p;
            emit progressChanged();
        }
    }

public slots:
    void begin() {
        if (!source.open(QIODevice::ReadOnly)) {
            qDebug() << "could not open source, aborting";
            emit done();
            return;
        }
        fileSize = source.size();
        if (!destination.open(QIODevice::WriteOnly)) {
            qDebug() << "could not open destination, aborting";
            // maybe check for overwriting and ask to proceed
            emit done();
            return;
        }
        if (!destination.resize(fileSize)) {
            qDebug() << "could not resize, aborting";
            emit done();
            return;
        }
        buff = (char*)malloc(bufferSize);
        if (!buff) {
            qDebug() << "could not allocate buffer, aborting";
            emit done();
            return;
        }
        QMetaObject::invokeMethod(this, "step", Qt::QueuedConnection);
        //timer.start();
    }
    void step() {
        if (!isCancelled) {
            if (position < fileSize) {
                quint64 chunk = fileSize - position;
                quint64 l = chunk > bufferSize ? bufferSize : chunk;
                source.read(buff, l);
                destination.write(buff, l);
                position += l;
                source.seek(position);
                destination.seek(position);
                setProgress((qreal)position / fileSize);
                //std::this_thread::sleep_for(std::chrono::milliseconds(100)); // for testing
                QMetaObject::invokeMethod(this, "step", Qt::QueuedConnection);
            } else {
                //qDebug() << timer.elapsed();
                emit done();
                return;
            }
        } else {
            if (!destination.remove()) qDebug() << "delete failed";
            emit done();
        }
    }
    void cancel() { isCancelled = true; }

signals:
    void progressChanged();
    void done();

private:
    bool isCancelled;
    quint64 bufferSize;
    qreal prog;
    QFile source, destination;
    quint64 fileSize, position;
    char * buff;
    //QElapsedTimer timer;
};

The done() signal is used to deleteLater() the copy helper / close copy dialog or whatever. You can enable the elapsed timer and use it to implement an elapsed time property and estimated time as well. Pausing is another possible feature to implement. Using QMetaObject::invokeMethod() allows the event loop to periodically process user events so you can cancel and update progress, which goes from 0 to 1. You can easily tweak it for moving files as well.

相关文章