草庐IT

c++ - 如何利用 Qt 使 QObject 方法线程安全?

coder 2024-02-02 原文

假设我们在 QObject 中编写了一个非常量方法-派生类:

class MyClass : public QObject {
  int x;
public:
  void method(int a) {
    x = a; // and possibly other things
  };
};

我们想让该方法成为线程安全的:这意味着从任意线程调用它,并且从多个线程并发调用,不应引入未定义的行为。
  • Qt 提供了哪些机制/API 来帮助我们使该方法成为线程安全的?
  • 当该方法也执行“其他事情”时,可以使用 Qt 中的哪些机制/API?
  • 是否有任何可能的“其他事物”分类可以告知要使用的 Qt 特定机制/API?

  • 题外话是 C++ 标准本身提供的机制,以及确保线程安全的通用/非 Qt 特定方法。

    最佳答案

    适用的 Qt API 取决于线程安全方法的功能。让我们从最一般到最具体的情况进行介绍。

    信号

    信号主体由 moc 生成工具并且是线程安全的。

    推论1:所有直接连接的槽/仿函数必须是线程安全的 : 否则 打破信号合约 .虽然信号槽系统允许代码解耦,但直接连接的特定情况会将信号的要求泄露给连接的代码!

    推论2:直接连接比自动连接更紧密。

    在对象的线程中做工作

    最通用的方法是确保该方法始终在对象的 thread() 中执行。 .这使得它相对于对象是线程安全的,但是当然,从方法中使用任何其他对象也必须是线程安全的。

    通常,线程不安全的方法只能从对象的 thread() 中调用。 :

    void MyObject::method() {
      Q_ASSERT(thread() == QThread::currentThread());
      ...
    }
    

    无线程对象的特殊情况需要小心。当一个对象的线程完成时,它就变成了无线程的。然而,仅仅因为对象是无线程的并不能使其所有方法都是线程安全的。出于线程安全的目的,最好选择一个线程来“拥有”此类对象。这样的线程可能是主线程:
    Q_ASSERT(QThread::currentThread() == (thread() ? thread() : qApp()->thread()));
    

    我们的工作就是实现这一主张。就是这样:
  • 利用线程安全信号。

    由于信号是线程安全的,我们可以使我们的方法成为信号,并将其实现托管在插槽中:
    class MyObject : public QObject {
      Q_OBJECT
      int x;
      void method_impl(int a) {
        x = a;
      }
      Q_SIGNAL void method_signal(int);
    public:
      void method(int a) { method_signal(a); }
      MyObject(QObject * parent = nullptr) : QObject{parent} {
        connect(this, &MyObject::method, this, &MyObject::method_impl);
      }
    };
    

    这种方法可以支持断言,但很冗长,并且每个参数都执行额外的动态分配(至少从 Qt 5.7 开始)。
  • 将仿函数中的调用分派(dispatch)给对象的线程。

    many ways of doing it ;让我们展示一个执行最少动态分配的方法:在大多数情况下,正好是一个。

    我们可以将方法的调用包装在一个仿函数中,并确保它以线程安全的方式执行:
    void method1(int val) {
       if (!isSafe(this))
          return postCall(this, [=]{ method1(val); });
       qDebug() << __FUNCTION__;
       num = val;
    }
    

    如果当前线程是对象的线程,则没有开销,也没有数据复制。否则,调用将推迟到对象线程中的事件循环,或者如果对象是无线程的,则调用将推迟到主事件循环。
    bool isSafe(QObject * obj) {
       Q_ASSERT(obj->thread() || qApp && qApp->thread() == QThread::currentThread());
       auto thread = obj->thread() ? obj->thread() : qApp->thread();
       return thread == QThread::currentThread();
    }
    
    template <typename Fun> void postCall(QObject * obj, Fun && fun) {
       qDebug() << __FUNCTION__;
       struct Event : public QEvent {
          using F = typename std::decay<Fun>::type;
          F fun;
          Event(F && fun) : QEvent(QEvent::None), fun(std::move(fun)) {}
          Event(const F & fun) : QEvent(QEvent::None), fun(fun) {}
          ~Event() { fun(); }
       };
       QCoreApplication::postEvent(
                obj->thread() ? obj : qApp, new Event(std::forward<Fun>(fun)));
    }
    
  • 将调用分派(dispatch)到对象的线程。

    这是上述的变体,但不使用仿函数。 postCall函数可以显式包装参数:
    void method2(const QString &val) {
       if (!isSafe(this))
          return postCall(this, &Class::method2, val);
       qDebug() << __FUNCTION__;
       str = val;
    }
    

    然后:
    template <typename Class, typename... Args>
    struct CallEvent : public QEvent {
       // See https://stackoverflow.com/a/7858971/1329652
       // See also https://stackoverflow.com/a/15338881/1329652
       template <int ...> struct seq {};
       template <int N, int... S> struct gens { using type = typename gens<N-1, N-1, S...>::type; };
       template <int ...S>        struct gens<0, S...> { using type = seq<S...>; };
       template <int ...S>        void callFunc(seq<S...>) { (obj->*method)(std::get<S>(args)...); }
       Class * obj;
       void (Class::*method)(Args...);
       std::tuple<typename std::decay<Args>::type...> args;
       CallEvent(Class * obj, void (Class::*method)(Args...), Args&&... args) :
          QEvent(QEvent::None), obj(obj), method(method), args(std::move<Args>(args)...) {}
       ~CallEvent() { callFunc(typename gens<sizeof...(Args)>::type()); }
    };
    
    template <typename Class, typename... Args> void postCall(Class * obj, void (Class::*method)(Args...), Args&& ...args) {
       qDebug() << __FUNCTION__;
       QCoreApplication::postEvent(
                obj->thread() ? static_cast<QObject*>(obj) : qApp, new CallEvent<Class, Args...>{obj, method, std::forward<Args>(args)...});
    }
    

  • 保护对象的数据

    如果该方法对一组成员进行操作,则可以使用互斥锁来序列化对这些成员的访问。杠杆QMutexLocker通过构造表达您的意图并避免未发布的互斥锁错误。
    class MyClass : public QObject {
      Q_OBJECT
      QMutex m_mutex;
      int m_a;
      int m_b;
    public:
      void method(int a, int b) {
        QMutexLocker lock{&m_mutex};
        m_a = a;
        m_b = b;
      };
    };
    

    在使用特定于对象的互斥锁和在对象线程中调用方法体之间的选择取决于应用程序的需要。如果方法中访问的所有成员都是私有(private)的,那么使用互斥锁是有意义的,因为我们处于控制之中,并且可以通过设计确保所有访问都受到保护。使用特定于对象的互斥锁也将方法与对象事件循环上的争用分离开来——因此可能具有性能优势。另一方面,如果该方法必须访问它不拥有的对象上的线程不安全的方法,那么互斥锁就不够用了,方法的主体应该在对象的线程中执行。

    读取简单的成员变量

    如果 const 方法读取可以包装在 QAtomicInteger 中的单条数据或 QAtomicPointer ,我们可以使用原子字段:
    class MyClass : public QObject {
      QAtomicInteger<int> x;
    public:
      /// Thread-Safe
      int method() const {
        return x.load();
      };
    };
    

    修改简单成员变量

    如果该方法修改了可以包裹在QAtomicInteger中的单条数据或 QAtomicPointer , 该操作可以使用原子原语完成,我们可以使用原子字段:
    class MyClass : public QObject {
      QAtomicInteger<int> x;
    public:
      /// Thread-Safe
      void method(int a) {
        x.fetchAndStoreOrdered(a);
      };
    };
    

    这种方法通常不会扩展到修改多个成员:某些成员更改而其他成员不更改的中间状态将对其他线程可见。通常这会破坏其他代码所依赖的不变量。

    关于c++ - 如何利用 Qt 使 QObject 方法线程安全?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40382820/

    有关c++ - 如何利用 Qt 使 QObject 方法线程安全?的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    4. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

      我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

    5. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    6. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

      给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

    7. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

      我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

    8. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

      我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

    9. Ruby 方法() 方法 - 2

      我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

    10. ruby - 如何指定 Rack 处理程序 - 2

      Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

    随机推荐