2016-09-25 295 views
-2

我在我的QT程序中使用多線程。我需要將數據傳遞給來自主gui線程的工作線程中的worker對象。我在QObject子類中創建了一個setData函數來傳遞主要gui線程中的所有必要數據。但是我通過查看setData函數中的QThread :: currentThreadId()來驗證函數是從主線程調用的。即使worker對象函數是從主線程調用的,這是否確保工作線程仍然擁有自己的數據副本(這是重入類所需的)?請記住,這是在工作線程啓動之前發生的。QT多線程數據從主線程傳遞給工作線程

此外,如果在沒有動態內存的類中使用基本數據類型,並且只要其所有其他成員數據都是可重入的,那麼沒有靜態全局變量就是該類可重入的? (它有折返數據成員一樣將QString,qlists等加上基本整數布爾變量等)

感謝您的幫助

編輯新的內容:

我的主要問題是根本適合從主gui線程中調用另一個線程中的QObject子類方法,以便將我的數據傳遞給工作線程以進行工作(在我的情況下,包含備份作業信息的自定義類用於長待處理文件掃描和數據備份)。數據傳遞全部發生在線程啓動之前,所以兩個線程都沒有同時修改數據的危險(我認爲但我不是多線程專家......)聽起來好像從你的文章中這樣做的方式是使用從主線程到工作線程中的插槽傳遞數據的信號。我已經確認我的數據備份作業是可重入的,所以我需要做的就是確保工作線程在它們自己的這些類的實例上工作。此外,通過調用QObject子類方法完成的數據傳輸在工作線程啓動之前完成 - 是否可以防止競爭條件並且安全嗎?

此外,根據「訪問其他線程的QObject的子類」一節here它看起來有點危險在QObject的子類使用插槽...

確定這裏是我最近忙的代碼... 編輯隨着代碼:

void Replicator::advancedAllBackup() 
{ 
    updateStatus("<font color = \"green\">Starting All Advanced Backups</font>"); 
    startBackup(); 

    worker = new Worker; 
    worker->moveToThread(workerThread); 
    setupWorker(normal); 

    QList<BackupJob> jobList; 
    for (int backupCount = 0; backupCount < advancedJobs.size(); backupCount++) 
     jobList << advancedJobs[backupCount]; 

    worker->setData(jobList); 
    workerThread->start(); 
} 

的STARTBACKUP功能設置一些布爾和更新GUI。設置工作程序函數連接工作線程和工作對象的所有信號和插槽。 setData函數將工作器作業列表數據設置爲後端數據,並在線程啓動之前調用,因此沒有併發性。 然後我們開始線程並完成它的工作。

而這裏的工人代碼:

void setData(QList<BackupJob> jobs) { this->jobs = jobs; } 

所以我的問題是:這是安全的?

+0

「通過調用QObject子類方法完成的數據傳輸是在工作線程啓動之前完成的 - 這是否會阻止競爭條件並且安全?」這裏的'QObject'沒有關係。重要的是,如果實施你的方法,實際上你的整個班級都是安全的。您如何期望我們能夠決定在沒有您提供任何代碼供我們談論的情況下? –

+0

「在QObject子類中使用插槽看起來有點危險......「這是一個無用的觀察(你實際上不能使用它),一個特定的方法,無論是否是槽,都是線程安全的或不是線程安全的,如果它不是線程安全的,那麼它只能從你應該聲明它是這樣調用的:在這些方法的開頭添加'Q_ASSERT(QThead :: currentThread()== thread());'可以從任何線程調用線程安全的方法。一個線程不安全的方法安全地調用發送一個信號並將該方法連接到它 –

回答

1

爲了使用Qt的機制在隊列之間傳遞數據,你不能直接調用對象的函數。您需要可以使用signal/slot mechanism,或者您可以使用QMetaObject::invokeMethod電話:

QMetaObject::invokeMethod(myObject, "mySlotFunction", 
          Qt::QueuedConnection, 
          Q_ARG(int, 42)); 

如果發送方和接收對象有正在運行的事件隊列,這隻會工作 - 即主要的或QThread的基於線程。

對於你的問題的另一部分,請參閱重入Qt的文檔部分: http://doc.qt.io/qt-4.8/threads-reentrancy.html#reentrant

許多Qt類是可重入的,但因爲使他們線程安全的,他們不是線程安全的, 會導致額外的開銷 反覆鎖定和解鎖QMutex。例如,QString是 可重入,但不是線程安全的。您可以同時安全地從多個線程安全地訪問QString的不同 實例,但是您的 無法同時安全地從多個線程 中安全地訪問QString的同一實例(除非您使用QMutex自行保護訪問)。

+1

1.'QObject'不運行事件隊列,事件隊列是每個線程而不是每個對象。沒關係,你可以從任何線程發出一個信號,包括原生的原生線程。執行'main'的線程並不是以'QThread'開始的:)接收線程需要一個正在運行的事件循環來處理事件,但不一定需要使用'QThread'開始,你可以使用'std :: thread'來運行一個事件循環;一個隱藏的實例一旦你開始循環,'QThread'就會被Qt自動構建。 –

2

你的問題有一些誤解。

重入和多線程是正交的概念。單線程代碼很容易被迫應付重入 - 並且一旦你重新進入事件循環(因此你不應該)。

您問的問題與更正,因此:如果數據成員支持多線程訪問,類的方法是線程安全的嗎?答案是肯定的。但這是一個最沒用的答案,因爲你錯誤地認爲你使用的數據類型支持這種訪問。他們最有可能不要

實際上,除非明確地找出它們,否則不太可能使用多線程安全的數據類型。 POD類型不是,大多數C++標準類型都不是,大多數Qt類型都不是。只是爲了沒有誤解:QString不是多線程安全的數據類型!下面的代碼是未定義行爲(它會崩潰,燒傷和發送電子郵件到你的配偶,似乎是從非法情人):

QString str{"Foo"}; 
for (int i = 0; i < 1000; ++i) 
    QtConcurrent::run([&]{ str.append("bar"); }); 

的後續問題可能是:

  • 我的數據成員是否支持多線程訪問?我以爲他們做到了。

    不,他們不是,除非你顯示的代碼證明,否則。

  • 我甚至需要支持多線程訪問嗎?

    也許吧。但避免完全需要它更容易。

與Qt類型有關的混淆的可能來源是它們的隱式共享語義。值得慶幸的是,他們對多線程的關係是相當簡單的表達:

  1. Qt的隱含共享類的任何實例可以從任何一個線程在給定時間訪問。推論:每個線程需要一個實例。複製你的對象,並在每個副本中使用它自己的線程 - 這是非常安全的。這些實例最初可能共享數據,並且Qt將確保任何copy-on-writes都可以爲您安全地完成線程。

  2. 邊欄:如果使用迭代器或內部指針指向非常量實例上的數據,則必須在構造迭代器/指針之前強制對象detach()迭代器的問題在於,當對象的數據分離時它們會失效,並且可能在實例爲非const的任何線程中發生分離 - 因此至少有一個線程最終會導致無效的迭代器。我不會再討論這個問題,因爲隱含共享的數據類型安全地實現和使用是非常棘手的。使用C++ 11,不再需要隱式共享:它們是C++ 98中缺少移動語義的解決方法。

那是什麼意思呢?這意味着這樣的:

// Unsafe: str1 potentially accessed from two threads at once 
QString str1{"foo"}; 
QtConcurrent::run([&]{ str1.apppend("bar"); }); 
str1.append("baz"); 

// Safe: each instance is accessed from one thread only 
QString str1{"foo"}; 
QString str2{str1}; 
QtConcurrent::run([&]{ str1.apppend("bar"); }); 
str2.append("baz"); 

原代碼可以因此被固定:

QString str{"Foo"}; 
for (int i = 0; i < 1000; ++i) 
    QtConcurrent::run([=]() mutable { str.append("bar"); }); 

這並不是說,該代碼是非常有用的:當所述函子內破壞修改的數據丟失工作者線程。但它用來說明如何處理Qt值類型和多線程。這就是它的工作原理:構造仿函數的每個實例時將獲取str的副本。然後這個仿函數被傳遞給一個工作線程來執行,在那裏它的字符串拷貝被追加到。該副本最初與原始線程中的str實例共享數據,但QString將線程安全地複製數據。你可以明確地寫出來的仿函數要清楚發生了什麼:

QString str{"Foo"}; 
struct Functor { 
    QString str; 
    Functor(const QString & str) : str{str} {} 
    void operator()() { 
    str.append("bar"); 
    } 
}; 
for (int i = 0; i < 1000; ++i) 
    QtConcurrent::run(Functor(str)); 

我們如何處理與使用Qt類型進出一個工人對象之間傳遞數據?所有與對象的通信,當它在工作者線程中時,都必須通過信號/插槽完成。 Qt會自動以線程安全的方式爲我們複製數據,以便每個實例的值只能在一個線程中訪問。例如:

class ImageSource : public QObject { 
    QImage render() { 
    QImage image{...}; 
    QPainter p{image}; 
    ... 
    return image; 
    } 
public: 
    Q_SIGNAL newImage(const QImage & image); 
    void makeImage() { 
    QtConcurrent::run([this]{ 
     emit newImage(render()); 
    }); 
    } 
}; 

int main(int argc, char ** argv) { 
    QApplication app...; 
    ImageSource source; 
    QLabel label; 
    label.show(); 
    connect(source, &ImageSource::newImage, &label, [&](const QImage & img){ 
    label.setPixmap(QPixmap::fromImage(img)); 
    }); 
    source.makeImage(); 
    return app.exec(); 
} 

源信號和標籤線程上下文之間的連接是自動的。該信號恰好在默認線程池中的工作線程中發出。在信號發射時,源線和目標線程進行比較,如果不同,仿函數將包裝在事件中,事件發佈標籤,並且標籤的QObject::event將運行設置像素映射的仿函數。這是所有線程安全的,並利用Qt使其幾乎毫不費力。目標線程上下文&label非常重要:如果沒有它,函數將運行在工作線程中,而不是UI線程。

請注意,我們甚至不必將對象移動到工作線程:實際上,應該避免將QObject移動到工作線程,除非對象確實需要對事件作出反應並且不僅僅是生成一塊數據。您通常想要移動處理通信的對象或從UI抽象出的複雜應用程序控制器。單純的數據生成通常可以使用QtConcurrent::run使用信號來抽象出將工作線程數據提取到另一個線程的線程安全魔術。

+0

感謝您的回覆。我的回覆不適合在這裏,所以我張貼在下面。 –