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 586 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 Christian Ehrlicher

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

    JonBJ Offline
    JonBJ Offline
    JonB
    wrote last edited by
    #3

    @Christian-Ehrlicher
    I read the OP's question earlier. So that I understand: if they genuinely do not "modify them somewhere", and the OP's original shown code is all there is and does reading only, would it then be OK if that reading code is called from multiple threads, potentially simultaneously?

    1 Reply Last reply
    0
    • 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