Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. Iterating shared QMap on multiple threads

Iterating shared QMap on multiple threads

Scheduled Pinned Locked Moved Solved General and Desktop
19 Posts 6 Posters 532 Views 2 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • Christian EhrlicherC Offline
    Christian EhrlicherC Offline
    Christian Ehrlicher
    Lifetime Qt Champion
    wrote last edited by
    #4

    Read-only is safe as long as noone else is modifying it or accessing the code with non-const functions which the op is doing for sure somehwere - otherwise the problem would not arise.

    Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
    Visit the Qt Academy at https://academy.qt.io/catalog

    1 Reply Last reply
    1
    • Christian EhrlicherC Christian Ehrlicher

      Don't access containers from two different threads without proper locking. I would guess you modify them somewhere.

      B Offline
      B Offline
      BrianL
      wrote last edited by
      #5

      @Christian-Ehrlicher It definitely wasn't modified after initialization, but there might be some container quirk I wasn't aware about, I'll provided the code for GetEntries() as context:

      Database.h

      using Entries = QMap<QString, QStringList>;
      using LanguageEntries = QMap<LanguageType, Entries>;
      using EntryDatabase = QMap<QString, LanguageEntries>;
      
      // this class is a singleton
      class Database
      {
      private:
          static Database& instance();
          static EntryDatabase& GetEntryDatabase();
      
          static bool EnsureDatabase(QString const& path);
          static Entries const& GetEntries(QString const& path, LanguageType language);
      
          EntryDatabase m_entryDatabase;
      }
      

      Database.cpp

      Database& Database::instance()
      {
          static Database database;
          return database;
      }
      
      EntryDatabase &Database::GetEntryDatabase()
      {
          return instance().m_entryDatabase;
      }
      
      bool Database::EnsureDatabase(const QString &path)
      {
          // check if database already exist
          EntryDatabase& database = GetEntryDatabase();
          if (database.contains(path)) return true;
      
          // inserts into database, this is only run on GUI thread
          // BEFORE any thread get created that calls GetEntries()
          LanguageEntries languageEntries;
          for (...)
          {
              Entries entries;
              for (...)
              {
                  entries.insert(it.key(), valueList);
              }
              languageEntries.insert(language, entries);
          }
          database.insert(path, languageEntries);
          return true;
      }
      
      const Entries &Database::GetEntries(const QString &path, LanguageType language)
      {
          EntryDatabase const& database = GetEntryDatabase();
          if (database.contains(path))
          {
              LanguageEntries const& languageEntries = database[path];
              if (languageEntries.contains(language))
              {
                  return GetEntryDatabase()[path][language];
              }
          }
      
          static Entries emptyEntries;
          return emptyEntries;
      }
      
      Christian EhrlicherC 1 Reply Last reply
      0
      • B BrianL

        @Christian-Ehrlicher It definitely wasn't modified after initialization, but there might be some container quirk I wasn't aware about, I'll provided the code for GetEntries() as context:

        Database.h

        using Entries = QMap<QString, QStringList>;
        using LanguageEntries = QMap<LanguageType, Entries>;
        using EntryDatabase = QMap<QString, LanguageEntries>;
        
        // this class is a singleton
        class Database
        {
        private:
            static Database& instance();
            static EntryDatabase& GetEntryDatabase();
        
            static bool EnsureDatabase(QString const& path);
            static Entries const& GetEntries(QString const& path, LanguageType language);
        
            EntryDatabase m_entryDatabase;
        }
        

        Database.cpp

        Database& Database::instance()
        {
            static Database database;
            return database;
        }
        
        EntryDatabase &Database::GetEntryDatabase()
        {
            return instance().m_entryDatabase;
        }
        
        bool Database::EnsureDatabase(const QString &path)
        {
            // check if database already exist
            EntryDatabase& database = GetEntryDatabase();
            if (database.contains(path)) return true;
        
            // inserts into database, this is only run on GUI thread
            // BEFORE any thread get created that calls GetEntries()
            LanguageEntries languageEntries;
            for (...)
            {
                Entries entries;
                for (...)
                {
                    entries.insert(it.key(), valueList);
                }
                languageEntries.insert(language, entries);
            }
            database.insert(path, languageEntries);
            return true;
        }
        
        const Entries &Database::GetEntries(const QString &path, LanguageType language)
        {
            EntryDatabase const& database = GetEntryDatabase();
            if (database.contains(path))
            {
                LanguageEntries const& languageEntries = database[path];
                if (languageEntries.contains(language))
                {
                    return GetEntryDatabase()[path][language];
                }
            }
        
            static Entries emptyEntries;
            return emptyEntries;
        }
        
        Christian EhrlicherC Offline
        Christian EhrlicherC Offline
        Christian Ehrlicher
        Lifetime Qt Champion
        wrote last edited by
        #6

        @BrianL said in Iterating shared QMap on multiple threads:

        LanguageEntries const& languageEntries = database[path];

        Here you modify database when path is not yet in there. The same goes for GetEntryDatabase()[path][language].
        So my your code shows what I already told you - you modify the container from different threads which results in undefined behavior.

        Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
        Visit the Qt Academy at https://academy.qt.io/catalog

        B I 2 Replies Last reply
        0
        • Christian EhrlicherC Christian Ehrlicher

          @BrianL said in Iterating shared QMap on multiple threads:

          LanguageEntries const& languageEntries = database[path];

          Here you modify database when path is not yet in there. The same goes for GetEntryDatabase()[path][language].
          So my your code shows what I already told you - you modify the container from different threads which results in undefined behavior.

          B Offline
          B Offline
          BrianL
          wrote last edited by
          #7

          @Christian-Ehrlicher But I have made sure the path exist first before calling database[path] tho?

          if (database.contains(path))
          {
              LanguageEntries const& languageEntries = database[path];
          
          Christian EhrlicherC 1 Reply Last reply
          0
          • B BrianL

            @Christian-Ehrlicher But I have made sure the path exist first before calling database[path] tho?

            if (database.contains(path))
            {
                LanguageEntries const& languageEntries = database[path];
            
            Christian EhrlicherC Offline
            Christian EhrlicherC Offline
            Christian Ehrlicher
            Lifetime Qt Champion
            wrote last edited by
            #8

            @BrianL said in Iterating shared QMap on multiple threads:

            anguageEntries const& languageEntries = database[path];

            And here database container gets modified when the path does not yet exists. So the container might grow.
            Use a proper locking mechanism

            Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
            Visit the Qt Academy at https://academy.qt.io/catalog

            B 1 Reply Last reply
            1
            • Christian EhrlicherC Christian Ehrlicher

              @BrianL said in Iterating shared QMap on multiple threads:

              anguageEntries const& languageEntries = database[path];

              And here database container gets modified when the path does not yet exists. So the container might grow.
              Use a proper locking mechanism

              B Offline
              B Offline
              BrianL
              wrote last edited by
              #9

              @Christian-Ehrlicher
              I'm sorry if I'm missing something 😅, as I said I've checked

              if (database.contains(path))
              

              before calling

              LanguageEntries const& languageEntries = database[path];
              

              so path should already existed in database?

              Christian EhrlicherC 1 Reply Last reply
              0
              • B BrianL

                @Christian-Ehrlicher
                I'm sorry if I'm missing something 😅, as I said I've checked

                if (database.contains(path))
                

                before calling

                LanguageEntries const& languageEntries = database[path];
                

                so path should already existed in database?

                Christian EhrlicherC Offline
                Christian EhrlicherC Offline
                Christian Ehrlicher
                Lifetime Qt Champion
                wrote last edited by
                #10

                @BrianL said in Iterating shared QMap on multiple threads:

                so path should already existed in database?

                You're right here but

                https://doc.qt.io/qt-6/qmap.html#operator-5b-5d-1

                operator[] const returns a value. So the reference you return goes out of scope.

                Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
                Visit the Qt Academy at https://academy.qt.io/catalog

                B 1 Reply Last reply
                2
                • Christian EhrlicherC Christian Ehrlicher

                  @BrianL said in Iterating shared QMap on multiple threads:

                  so path should already existed in database?

                  You're right here but

                  https://doc.qt.io/qt-6/qmap.html#operator-5b-5d-1

                  operator[] const returns a value. So the reference you return goes out of scope.

                  B Offline
                  B Offline
                  BrianL
                  wrote last edited by
                  #11

                  @Christian-Ehrlicher
                  ohh I see it now, I was trying to avoid using .value() because that returns a copy, but didn't notice operator[] const also returns a copy....

                  In that case is there no way to return a reference from QMap unless I made it return a non-const reference?

                  1 Reply Last reply
                  0
                  • Christian EhrlicherC Christian Ehrlicher

                    @BrianL said in Iterating shared QMap on multiple threads:

                    LanguageEntries const& languageEntries = database[path];

                    Here you modify database when path is not yet in there. The same goes for GetEntryDatabase()[path][language].
                    So my your code shows what I already told you - you modify the container from different threads which results in undefined behavior.

                    I Offline
                    I Offline
                    IgKh
                    wrote last edited by IgKh
                    #12

                    @Christian-Ehrlicher @BrianL

                    The LanguageEntries const& languageEntries = database[path]; part is indeed problematic, but not because of what you think. database is a reference to a constant, so the const version of operator[] is called. However do notice that unlike the non-const version it returns a T, not a T&. Assigning it to a const l-value reference performs lifetime extension of the temporary that got returned until the end of the scope, but you are playing with fire doing it like that (e.g if you tried to return it). Due to the Copy-on-Write nature of Qt containers (which will lead me to the next point), they should generally be passed and assigned by value and not by reference (which might seem strange, but they are all actually shared-ownership reference wrappers anyway).

                    On the other hand GetEntryDatabase()[path][language] is exactly a mutating call, since it invokes the non-const operator[]. ANY non-const method on a CoW Qt container will first detach() the caller, even if it didn't end up modifying anything. The detach itself is thread-safe, as the copy-on-write semantics are implemented with atomic operations, but it does cause some iterator invalidation. The Qt documentation actually has a specific section on this - Implicit sharing iterator problem.

                    That specific case of detaching operation that doesn't actually modify the container is why it looks like if you fix the end iterator it works fine. It doesn't really. In such cases you should stick to external synchronization, or perhaps use the STL containers - they aren't thread safe as well, but their semantics are more obvious to C++ programmers.

                    B 1 Reply Last reply
                    4
                    • I IgKh

                      @Christian-Ehrlicher @BrianL

                      The LanguageEntries const& languageEntries = database[path]; part is indeed problematic, but not because of what you think. database is a reference to a constant, so the const version of operator[] is called. However do notice that unlike the non-const version it returns a T, not a T&. Assigning it to a const l-value reference performs lifetime extension of the temporary that got returned until the end of the scope, but you are playing with fire doing it like that (e.g if you tried to return it). Due to the Copy-on-Write nature of Qt containers (which will lead me to the next point), they should generally be passed and assigned by value and not by reference (which might seem strange, but they are all actually shared-ownership reference wrappers anyway).

                      On the other hand GetEntryDatabase()[path][language] is exactly a mutating call, since it invokes the non-const operator[]. ANY non-const method on a CoW Qt container will first detach() the caller, even if it didn't end up modifying anything. The detach itself is thread-safe, as the copy-on-write semantics are implemented with atomic operations, but it does cause some iterator invalidation. The Qt documentation actually has a specific section on this - Implicit sharing iterator problem.

                      That specific case of detaching operation that doesn't actually modify the container is why it looks like if you fix the end iterator it works fine. It doesn't really. In such cases you should stick to external synchronization, or perhaps use the STL containers - they aren't thread safe as well, but their semantics are more obvious to C++ programmers.

                      B Offline
                      B Offline
                      BrianL
                      wrote last edited by
                      #13

                      @IgKh said in Iterating shared QMap on multiple threads:

                      ANY non-const method on a CoW Qt container will first detach() the caller, even if it didn't end up modifying anything. The detach itself is thread-safe, as the copy-on-write semantics are implemented with atomic operations, but it does cause some iterator invalidation.

                      So even if I change my code to use T& operator[], it would still invalidate currently in-use iterator from other threads?

                      Entries &EntryDatabase::GetEntries(const QString &path, LanguageType language)
                      {
                          EntryDatabase& database = GetDatabase();
                          if (database.contains(path))
                          {
                              LanguageEntries& languageEntries = database[path];
                              if (languageEntries.contains(language))
                              {
                                  return languageEntries[language];
                              }
                          }
                      
                          static OCREntries emptyEntries;
                          return emptyEntries;
                      }
                      
                      I 1 Reply Last reply
                      0
                      • B BrianL

                        @IgKh said in Iterating shared QMap on multiple threads:

                        ANY non-const method on a CoW Qt container will first detach() the caller, even if it didn't end up modifying anything. The detach itself is thread-safe, as the copy-on-write semantics are implemented with atomic operations, but it does cause some iterator invalidation.

                        So even if I change my code to use T& operator[], it would still invalidate currently in-use iterator from other threads?

                        Entries &EntryDatabase::GetEntries(const QString &path, LanguageType language)
                        {
                            EntryDatabase& database = GetDatabase();
                            if (database.contains(path))
                            {
                                LanguageEntries& languageEntries = database[path];
                                if (languageEntries.contains(language))
                                {
                                    return languageEntries[language];
                                }
                            }
                        
                            static OCREntries emptyEntries;
                            return emptyEntries;
                        }
                        
                        I Offline
                        I Offline
                        IgKh
                        wrote last edited by
                        #14

                        @BrianL said in Iterating shared QMap on multiple threads:

                        So even if I change my code to use T& operator[], it would still invalidate currently in-use iterator from other threads?

                        Basically yes. It is more nuanced than that, but discussing that would be just an academic exercise. As said, I think you are better served by std::vector and std::map for this use case.

                        B JonBJ 2 Replies Last reply
                        0
                        • I IgKh

                          @BrianL said in Iterating shared QMap on multiple threads:

                          So even if I change my code to use T& operator[], it would still invalidate currently in-use iterator from other threads?

                          Basically yes. It is more nuanced than that, but discussing that would be just an academic exercise. As said, I think you are better served by std::vector and std::map for this use case.

                          B Offline
                          B Offline
                          BrianL
                          wrote last edited by
                          #15

                          @IgKh Thanks for the answer! I have switch to using std::map now

                          1 Reply Last reply
                          0
                          • B BrianL has marked this topic as solved
                          • I IgKh

                            @BrianL said in Iterating shared QMap on multiple threads:

                            So even if I change my code to use T& operator[], it would still invalidate currently in-use iterator from other threads?

                            Basically yes. It is more nuanced than that, but discussing that would be just an academic exercise. As said, I think you are better served by std::vector and std::map for this use case.

                            JonBJ Offline
                            JonBJ Offline
                            JonB
                            wrote last edited by JonB
                            #16

                            @IgKh said in Iterating shared QMap on multiple threads:

                            Basically yes. It is more nuanced than that, but discussing that would be just an academic exercise. As said, I think you are better served by std::vector and std::map for this use case.

                            Without getting deep into a "nuanced" discussion, could you just summarise why these std ones are more suited here than the Qt ones, please? Is it because they do not get into references or temporaries or detaches or what? And the problem/difference would not arise/matter if multi-thread was not involved?

                            Christian EhrlicherC S 2 Replies Last reply
                            0
                            • JonBJ JonB

                              @IgKh said in Iterating shared QMap on multiple threads:

                              Basically yes. It is more nuanced than that, but discussing that would be just an academic exercise. As said, I think you are better served by std::vector and std::map for this use case.

                              Without getting deep into a "nuanced" discussion, could you just summarise why these std ones are more suited here than the Qt ones, please? Is it because they do not get into references or temporaries or detaches or what? And the problem/difference would not arise/matter if multi-thread was not involved?

                              Christian EhrlicherC Offline
                              Christian EhrlicherC Offline
                              Christian Ehrlicher
                              Lifetime Qt Champion
                              wrote last edited by
                              #17

                              @JonB said in Iterating shared QMap on multiple threads:

                              Is it because they do not get into references or temporaries or detaches or what?

                              std::map operator[] returns a reference whereas QMap operator[] const returns a temporary. That's the problem here.

                              Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
                              Visit the Qt Academy at https://academy.qt.io/catalog

                              1 Reply Last reply
                              2
                              • sierdzioS Offline
                                sierdzioS Offline
                                sierdzio
                                Moderators
                                wrote last edited by
                                #18

                                I guess it's also an option to use QMap::find() to get a const iterator which will not be a copy of the data, unless I misremember stuff.

                                (Z(:^

                                1 Reply Last reply
                                3
                                • JonBJ JonB

                                  @IgKh said in Iterating shared QMap on multiple threads:

                                  Basically yes. It is more nuanced than that, but discussing that would be just an academic exercise. As said, I think you are better served by std::vector and std::map for this use case.

                                  Without getting deep into a "nuanced" discussion, could you just summarise why these std ones are more suited here than the Qt ones, please? Is it because they do not get into references or temporaries or detaches or what? And the problem/difference would not arise/matter if multi-thread was not involved?

                                  S Offline
                                  S Offline
                                  SimonSchroeder
                                  wrote last edited by
                                  #19

                                  @JonB said in Iterating shared QMap on multiple threads:

                                  Without getting deep into a "nuanced" discussion, could you just summarise why these std ones are more suited here than the Qt ones, please?

                                  I would say the most important point for this would be the "detaching" issue. Especially in the context of multithreading it is quite hard to figure out when detaches might happen. I guess the best way with QMap is to sprinkle qAsConst (or now even std::as_const) as often as possible. std::map just doesn't have this problem, so it is easier to get right with the STL compared to Qt.

                                  1 Reply Last reply
                                  1

                                  • Login

                                  • Login or register to search.
                                  • First post
                                    Last post
                                  0
                                  • Categories
                                  • Recent
                                  • Tags
                                  • Popular
                                  • Users
                                  • Groups
                                  • Search
                                  • Get Qt Extensions
                                  • Unsolved