From 35a2d598ab070a0c9e7fb72b332cbaed6728da9f Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Wed, 8 Nov 2023 15:30:40 +0100 Subject: [PATCH] Utils: Add SynchronizedValue Change-Id: I0af6998f540ba688fa54d9e43e33cb3cb0fc54e8 Reviewed-by: hjk --- src/libs/utils/synchronizedvalue.h | 370 ++++++++++++++++++ tests/auto/utils/CMakeLists.txt | 1 + .../utils/synchronizedvalue/CMakeLists.txt | 4 + .../synchronizedvalue/synchronizedvalue.qbs | 7 + .../tst_synchronizedvalue.cpp | 219 +++++++++++ tests/auto/utils/utils.qbs | 1 + 6 files changed, 602 insertions(+) create mode 100644 src/libs/utils/synchronizedvalue.h create mode 100644 tests/auto/utils/synchronizedvalue/CMakeLists.txt create mode 100644 tests/auto/utils/synchronizedvalue/synchronizedvalue.qbs create mode 100644 tests/auto/utils/synchronizedvalue/tst_synchronizedvalue.cpp diff --git a/src/libs/utils/synchronizedvalue.h b/src/libs/utils/synchronizedvalue.h new file mode 100644 index 00000000000..a95104ec4cb --- /dev/null +++ b/src/libs/utils/synchronizedvalue.h @@ -0,0 +1,370 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include +#include +#include + +namespace Utils { + +/*! + \brief A wrapper that provides thread-safe access to the wrapped type using a read/write mutex. + + Examples: + + \code + + void writeAndGet() { + SynchronizedValue synchronizedString; + // To update the value of the synchronized object, you can use the write function. + synchronizedString.write([](QString &str) { str = "Hello World"; }); + + // If you just need a value from the synchronized object, you can use the get function + qDebug() << "New value is:" << synchronizedString.get([](const QString &str) { return str; }); + } + + void read() { + SynchronizedValue> synchronized; + + QString both; + + // If you want to access multiple members of the synchronized object, you can use the read function + synchronized.read([&both](const QPair &pair) { + qDebug() << "First value is:" << pair.first(); + qDebug() << "Second value is:" << pair.second(); + both = pair.first() + pair.second(); + // ... + }); + } + + // You can use the SynchronizedValue::update() to return whether the value was changed: + void setString(const QString &newString) { + const bool wasChanged = m_synchronizedString.update([&newString](QString &str) { + if (newString == str) + return false; + str = newString; + return true; + })); + + if (wasChanged) + emit stringChanged(newString); + } + + // You can also use a lock type to get access + void withLocks() { + SynchronizedValue synchronizedData; + *synchronizedData.writeLocked() = "Hello World"; + qDebug() << *synchronizedData.readLocked() << "== Hello World"; + + auto lk = synchronizedData.writeLocked(); + assert(lk.ownsLock()); + *lk = "I am locked"; + } + + \endcode +*/ +template +class SynchronizedValue +{ + template + friend std::tuple synchronize(SV &...sv); + +public: + SynchronizedValue() = default; + + SynchronizedValue(const SynchronizedValue &other) + { + std::shared_lock lk(other.mutex); + value = other.value; + } + + SynchronizedValue(const T &other) + : value(other) + {} + + class shared_lock + { + public: + shared_lock(T const &value_, std::shared_mutex &mutex) + : lock(mutex) + , value(value_) + {} + + shared_lock(T const &value_, std::shared_mutex &mutex, std::try_to_lock_t) + : lock(mutex, std::try_to_lock) + , value(value_) + {} + + shared_lock(T const &value_, std::shared_mutex &mutex, std::defer_lock_t) + : lock(mutex, std::defer_lock) + , value(value_) + {} + + shared_lock(T const &value_, std::shared_mutex &mutex, std::adopt_lock_t) + : lock(mutex, std::adopt_lock) + , value(value_) + {} + + bool ownsLock() const { return lock.owns_lock(); } + + const T *operator->() const + { + Q_ASSERT(ownsLock()); + return &value; + } + + const T &operator*() const + { + Q_ASSERT(ownsLock()); + return value; + } + + private: + std::shared_lock lock; + T const &value; + }; + + class unique_lock + { + public: + unique_lock(T &value_, std::shared_mutex &mutex) + : lock(mutex) + , value(value_) + {} + + unique_lock(T &value_, std::shared_mutex &mutex, std::try_to_lock_t) + : lock(mutex, std::try_to_lock) + , value(value_) + {} + + unique_lock(T &value_, std::shared_mutex &mutex, std::defer_lock_t) + : lock(mutex, std::defer_lock) + , value(value_) + {} + + unique_lock(T &value_, std::shared_mutex &mutex, std::adopt_lock_t) + : lock(mutex, std::adopt_lock) + , value(value_) + {} + + bool ownsLock() const { return lock.owns_lock(); } + + T *operator->() const + { + Q_ASSERT(ownsLock()); + return &value; + } + + T &operator*() const + { + Q_ASSERT(ownsLock()); + return value; + } + + private: + std::unique_lock lock; + T &value; + }; + + [[nodiscard]] shared_lock readLocked() const { return shared_lock(value, mutex); } + [[nodiscard]] shared_lock readLocked(std::try_to_lock_t) const + { + return shared_lock(value, mutex, std::try_to_lock); + } + + [[nodiscard]] unique_lock writeLocked() { return unique_lock(value, mutex); } + [[nodiscard]] unique_lock writeLocked(std::try_to_lock_t) + { + return unique_lock(value, mutex, std::try_to_lock); + } + + //! Call func with a const reference to the wrapped object + void read(const std::function &func) const + { + std::shared_lock lk(mutex); + func(value); + } + + //! Call func with a const reference to the wrapped object and returns the result of func + template + [[nodiscard]] R get(const std::function &func) const + { + std::shared_lock lk(mutex); + return func(value); + } + + [[nodiscard]] T get() const + { + std::shared_lock lk(mutex); + return value; + } + + //! Call func with a mutable reference to the wrapped object + void write(const std::function &func) + { + std::unique_lock lk(mutex); + func(value); + } + + //! Call func with a mutable reference to the wrapped object and returns the result of func + template + [[nodiscard]] R update(const std::function &func) + { + std::unique_lock lk(mutex); + return func(value); + } + + SynchronizedValue &operator=(const SynchronizedValue &other) + { + std::unique_lock lk(mutex, std::defer_lock); + std::shared_lock lkOther(other.mutex, std::defer_lock); + std::lock(lk, lkOther); + value = other.value; + return *this; + } + + SynchronizedValue &operator=(const T &other) + { + std::unique_lock lk(mutex); + value = other; + return *this; + } + + bool operator!=(const SynchronizedValue &rhs) const + { + std::shared_lock lk(mutex, std ::defer_lock); + std::shared_lock lkOther(rhs.mutex, std ::defer_lock); + std::lock(lk, lkOther); + return value != rhs.value; + } + + bool operator==(const SynchronizedValue &rhs) const + { + std::shared_lock lk(mutex, std ::defer_lock); + std::shared_lock lkOther(rhs.mutex, std ::defer_lock); + std::lock(lk, lkOther); + return value == rhs.value; + } + + bool operator<(const SynchronizedValue &rhs) const + { + std::shared_lock lk(mutex, std ::defer_lock); + std::shared_lock lkOther(rhs.mutex, std ::defer_lock); + std::lock(lk, lkOther); + return value < rhs.value; + } + + bool operator<=(const SynchronizedValue &rhs) const + { + std::shared_lock lk(mutex, std ::defer_lock); + std::shared_lock lkOther(rhs.mutex, std ::defer_lock); + std::lock(lk, lkOther); + return value <= rhs.value; + } + + bool operator>(const SynchronizedValue &rhs) const + { + std::shared_lock lk(mutex, std ::defer_lock); + std::shared_lock lkOther(rhs.mutex, std ::defer_lock); + std::lock(lk, lkOther); + return value > rhs.value; + } + + bool operator>=(const SynchronizedValue &rhs) const + { + std::shared_lock lk(mutex, std ::defer_lock); + std::shared_lock lkOther(rhs.mutex, std ::defer_lock); + std::lock(lk, lkOther); + return value >= rhs.value; + } + + bool operator>(const T &rhs) const + { + std::shared_lock lk(mutex); + return value > rhs; + } + + bool operator>=(const T &rhs) const + { + std::shared_lock lk(mutex); + return value >= rhs; + } + + bool operator!=(const T &rhs) const + { + std::shared_lock lk(mutex); + return value != rhs; + } + + bool operator==(const T &rhs) const + { + std::shared_lock lk(mutex); + return value == rhs; + } + + bool operator<(const T &rhs) const + { + std::shared_lock lk(mutex); + return value < rhs; + } + + bool operator<=(const T &rhs) const + { + std::shared_lock lk(mutex); + return value <= rhs; + } + +private: + template + friend bool operator!=(const L &lhs, const SynchronizedValue &rhs) + { + return rhs != lhs; + } + + template + friend bool operator==(const L &lhs, const SynchronizedValue &rhs) + { + return rhs == lhs; + } + + template + friend bool operator<(const L &lhs, const SynchronizedValue &rhs) + { + return rhs > lhs; + } + + template + friend bool operator<=(const L &lhs, const SynchronizedValue &rhs) + { + return rhs >= lhs; + } + + template + friend bool operator>(const L &lhs, const SynchronizedValue &rhs) + { + return rhs < lhs; + } + + template + friend bool operator>=(const L &lhs, const SynchronizedValue &rhs) + { + return rhs <= lhs; + } + +private: + mutable std::shared_mutex mutex; + T value; +}; + +//! Lock a number of SynchronizedValue's using a dead-lock free algorithm. ( see std::lock() ) +template +std::tuple synchronize(SV &...sv) +{ + std::lock(sv.mutex...); + typedef std::tuple t_type; + return t_type(typename SV::unique_lock(sv.value, sv.mutex, std::adopt_lock)...); +} + +} // namespace Utils diff --git a/tests/auto/utils/CMakeLists.txt b/tests/auto/utils/CMakeLists.txt index e78cb3380cc..d19ac4acd63 100644 --- a/tests/auto/utils/CMakeLists.txt +++ b/tests/auto/utils/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(fileutils) add_subdirectory(fsengine) add_subdirectory(fuzzymatcher) add_subdirectory(indexedcontainerproxyconstiterator) +add_subdirectory(synchronizedvalue) add_subdirectory(mathutils) add_subdirectory(multicursor) add_subdirectory(persistentsettings) diff --git a/tests/auto/utils/synchronizedvalue/CMakeLists.txt b/tests/auto/utils/synchronizedvalue/CMakeLists.txt new file mode 100644 index 00000000000..2e3d679d7fb --- /dev/null +++ b/tests/auto/utils/synchronizedvalue/CMakeLists.txt @@ -0,0 +1,4 @@ +add_qtc_test(tst_utils_synchronizedvalue + DEPENDS Utils + SOURCES tst_synchronizedvalue.cpp +) diff --git a/tests/auto/utils/synchronizedvalue/synchronizedvalue.qbs b/tests/auto/utils/synchronizedvalue/synchronizedvalue.qbs new file mode 100644 index 00000000000..d001951d3fa --- /dev/null +++ b/tests/auto/utils/synchronizedvalue/synchronizedvalue.qbs @@ -0,0 +1,7 @@ +import qbs + +QtcAutotest { + name: "synchronizedvalue autotest" + Depends { name: "Utils" } + files: "tst_synchronizedvalue.cpp" +} diff --git a/tests/auto/utils/synchronizedvalue/tst_synchronizedvalue.cpp b/tests/auto/utils/synchronizedvalue/tst_synchronizedvalue.cpp new file mode 100644 index 00000000000..6a1d8fe8cfc --- /dev/null +++ b/tests/auto/utils/synchronizedvalue/tst_synchronizedvalue.cpp @@ -0,0 +1,219 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include + +#include + +#include + +namespace Utils { + +struct TestData +{ + TestData() = default; + TestData(int iValue) + : i(iValue) + {} + + bool operator==(const TestData &other) const { return i == other.i && str == other.str; } + bool operator!=(const TestData &other) const { return !(*this == other); } + QString str; + int i{20}; +}; + +class tst_synchronizedvalue : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() {} + + void read() + { + SynchronizedValue data; + + data.read([](const auto &d) { + QCOMPARE(d.str, QString()); + QCOMPARE(d.i, 20); + }); + } + + void ctor() + { + SynchronizedValue data(200); + QCOMPARE(data.get([](const auto &d) { return d.i; }), 200); + } + + void initializerCtor() + { + SynchronizedValue> data({1, 2, 3}); + QCOMPARE(data.get>([](const auto &d) { return d; }), QList({1, 2, 3})); + } + + void get() + { + SynchronizedValue data(200); + QCOMPARE(data.get([](const auto &d) { return d.i; }), 200); + } + + void constLock() + { + SynchronizedValue data(200); + QCOMPARE(data.readLocked()->i, 200); + QCOMPARE(data.get().i, 200); + + auto lk = data.readLocked(); + QCOMPARE(lk->i, 200); + + // This should fail to compile: + // data.readLocked()->i = 200; + } + + void lock() + { + SynchronizedValue data(123); + data.writeLocked()->i = 200; + QCOMPARE(data.get([](const auto &d) { return d.i; }), 200); + + { + auto wlk = data.writeLocked(); + wlk->str = "Write locked"; + } + + QCOMPARE(data.readLocked()->str, "Write locked"); + + SynchronizedValue lockedStr("Hello World"); + QCOMPARE(*lockedStr.readLocked(), "Hello World"); + *lockedStr.writeLocked() = "Hello World 2"; + QCOMPARE(*lockedStr.readLocked(), "Hello World 2"); + } + + void trivialGet() + { + SynchronizedValue data(200); + TestData d = data.get(); + QCOMPARE(data, d); + QCOMPARE(d.i, 200); + } + + void equalop() + { + SynchronizedValue data(200); + SynchronizedValue data2(300); + + data = data2; + + QCOMPARE(data.get([](const auto &d) { return d.i; }), 300); + QCOMPARE(data2.get([](const auto &d) { return d.i; }), 300); + + TestData dataNoLock(1337); + data = dataNoLock; + + QCOMPARE(data.get([](const auto &d) { return d.i; }), 1337); + } + + void compareEq() + { + SynchronizedValue data(200); + SynchronizedValue data2(300); + + QVERIFY(data != data2); + QVERIFY(!(data == data2)); + + data2.write([](auto &d) { d.i = 200; }); + + QVERIFY(data == data2); + QVERIFY(!(data != data2)); + } + + void operators() + { + SynchronizedValue data(200); + SynchronizedValue data2(300); + + QVERIFY(data < data2); + QVERIFY(data <= data2); + QVERIFY(data2 > data); + QVERIFY(data2 >= data); + QVERIFY(data2 != data); + QVERIFY(!(data2 == data)); + + QVERIFY(data > 100); + QVERIFY(data >= 100); + QVERIFY(data >= 200); + QVERIFY(data < 300); + QVERIFY(data <= 300); + + QVERIFY(100 < data); + QVERIFY(200 <= data); + QVERIFY(199 <= data); + QVERIFY(300 > data); + QVERIFY(200 >= data); + QVERIFY(201 >= data); + QVERIFY(200 == data); + QVERIFY(200 != data2); + } + + void copyCtor() + { + SynchronizedValue data(123); + SynchronizedValue data2(data); + + QCOMPARE(data.get([](const auto &d) { return d.i; }), 123); + QCOMPARE(data2.get([](const auto &d) { return d.i; }), 123); + + TestData dataNoLock(1337); + SynchronizedValue data3(dataNoLock); + + QCOMPARE(data3.get([](const auto &d) { return d.i; }), 1337); + } + + void multilock() + { + SynchronizedValue value(100); + + // Multiple reader, no writer + { + QCOMPARE(value.get(), 100); + auto firstLock = value.readLocked(); + QVERIFY(firstLock.ownsLock()); + auto secondLock = value.readLocked(); + QVERIFY(secondLock.ownsLock()); + + QCOMPARE(*firstLock, 100); + QCOMPARE(*secondLock, 100); + + auto thirdLock = value.writeLocked(std::try_to_lock); + QVERIFY(!thirdLock.ownsLock()); + } + + // Single writer, no readers + { + auto firstLock = value.writeLocked(); + QVERIFY(firstLock.ownsLock()); + auto secondLock = value.writeLocked(std::try_to_lock); + QVERIFY(!secondLock.ownsLock()); + + auto readLock = value.readLocked(std::try_to_lock); + QVERIFY(!readLock.ownsLock()); + } + } + + void synchronizeMultiple() + { + SynchronizedValue sv1; + SynchronizedValue sv2; + + auto [lk1, lk2] = synchronize(sv1, sv2); + + QVERIFY(lk1.ownsLock()); + QVERIFY(lk2.ownsLock()); + } +}; + +} // namespace Utils + +QTEST_GUILESS_MAIN(Utils::tst_synchronizedvalue) + +#include "tst_synchronizedvalue.moc" diff --git a/tests/auto/utils/utils.qbs b/tests/auto/utils/utils.qbs index 9f5a580435a..a8329b582c7 100644 --- a/tests/auto/utils/utils.qbs +++ b/tests/auto/utils/utils.qbs @@ -13,6 +13,7 @@ Project { "fsengine/fsengine.qbs", "fuzzymatcher/fuzzymatcher.qbs", "indexedcontainerproxyconstiterator/indexedcontainerproxyconstiterator.qbs", + "synchronizedvalue/synchronizedvalue.qbs", "mathutils/mathutils.qbs", "multicursor/multicursor.qbs", "persistentsettings/persistentsettings.qbs",