feat: add DThreadUtils class

Log: 添加DThreadUtils类
This commit is contained in:
heyuming 2023-06-15 13:59:23 +08:00 committed by Comix
parent f5fd6cf9a8
commit 2df5ca70dd
9 changed files with 514 additions and 13 deletions

View File

@ -0,0 +1,55 @@
/*!
@~chinese
@ingroup dutil
@file include/util/dthreadutils.h
本文件定义了线程相关的帮助类
@class DThreadUtils
@brief 线程帮助类
@details 本类主要用来进行异步线程调用, 此外本类所有的public接口都是线程安全的
@fn static DThreadUtils& DThreadUtils::gui()
@brief 获取以GUI线程初始化的静态对象
@return DThreadUtils& 静态对象的引用
@fn QThread* DThreadUtils::thread() const noexcept
@brief 获取DThreadUtils对应的线程
@return QThread* 对应线程的QThread对象的指针
@fn template <typename Func, typename... Args> inline auto run(QObject *context,typename QtPrivate::FunctionPointer<Func>::Object *obj, Func fun, Args &&...args)
@brief 在对应的线程执行传入的成员函数, 非阻塞
@param[in] context 对象上下文, 用来在执行调用时判断对象是否存在
@param[in] obj 对象指针
@param[in] fun 成员函数指针
@param[in] args 对应函数的参数
@return 以成员函数返回值类型实例化的QFuture对象
@fn template <typename Func, typename... Args> inline auto run(typename QtPrivate::FunctionPointer<Func>::Object *obj, Func fun, Args &&...args)
@brief 在对应的线程执行传入的成员函数, 非阻塞
@param[in] obj 对象指针
@param[in] fun 成员函数指针
@param[in] args 对应函数的参数
@return 以成员函数返回值类型实例化的QFuture对象
@fn template <typename Func, typename... Args> inline QFuture<std::invoke_result_t<std::decay_t<Func>, Args...>> run(QObject *context, Func fun, Args &&...args)
@brief 在对应的线程执行传入的成员函数, 非阻塞
@param[in] context 对象上下文, 用来在执行调用时判断对象是否存在
@param[in] fun 成员函数指针
@param[in] args 对应函数的参数
@return 以成员函数返回值类型实例化的QFuture对象
@fn template <typename Func, typename... Args> inline QFuture<std::invoke_result_t<std::decay_t<Func>, Args...>> run(Func fun, Args &&...args)
@brief 在对应的线程执行传入的成员函数, 非阻塞
@param[in] fun 可调用对象
@param[in] args 调用对应的参数
@return 以函数返回值类型实例化的QFuture对象
@fn template <typename... T> inline decltype(auto) exec(T &&...args)
@brief 在对应的线程执行传入的成员函数, 阻塞
@details 本函数是run函数的包装
@note 调用此函数的一方需要确保对应线程有事件循环, 否则会无限等待
@param[in] args 参数包, 具体含义参考run函数
@return 传入函数的返回值
*/

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2020 - 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
@ -13,8 +13,15 @@
#include <QPointer>
#include <QDebug>
#if DTK_VERSION >= DTK_VERSION_CHECK(6, 0, 0, 0)
#include <QFuture>
#include <QPromise>
#include <QEvent>
#endif
DCORE_BEGIN_NAMESPACE
#if DTK_VERSION < DTK_VERSION_CHECK(6, 0, 0, 0)
namespace DThreadUtil {
typedef std::function<void()> FunctionType;
@ -152,7 +159,170 @@ inline typename QtPrivate::FunctionPointer<Fun>::ReturnType
return runInMainThread(obj, obj, fun, std::forward<Args>(args)...);
}
}
#else
class LIBDTKCORESHARED_EXPORT DThreadUtils final
{
friend class Caller;
public:
explicit DThreadUtils(QThread *thread);
~DThreadUtils();
static DThreadUtils &gui();
QThread *thread() const noexcept;
template <typename Func, typename... Args>
inline auto run(QObject *context,typename QtPrivate::FunctionPointer<Func>::Object *obj, Func fun, Args &&...args)
{
return call(context, fun, *obj, std::forward<Args>(args)...);
}
template <typename Func, typename... Args>
inline auto run(typename QtPrivate::FunctionPointer<Func>::Object *obj, Func fun, Args &&...args)
{
if constexpr (std::is_base_of<QObject, typename QtPrivate::FunctionPointer<Func>::Object>::value) {
return call(obj, fun, *obj, std::forward<Args>(args)...);
} else {
return call(static_cast<QObject *>(nullptr), fun, *obj, std::forward<Args>(args)...);
}
}
template <typename Func, typename... Args>
inline QFuture<std::invoke_result_t<std::decay_t<Func>, Args...>> run(QObject *context, Func fun, Args &&...args)
{
return call(context, fun, std::forward<Args>(args)...);
}
template <typename Func, typename... Args>
inline QFuture<std::invoke_result_t<std::decay_t<Func>, Args...>> run(Func fun, Args &&...args)
{
return call(static_cast<QObject *>(nullptr), fun, std::forward<Args>(args)...);
}
template <typename... T>
inline decltype(auto) exec(T &&...args)
{
auto future = run(std::forward<T>(args)...);
if (!thread()->isRunning()) {
qWarning() << "The target thread is not running, maybe lead to deadlock.";
}
future.waitForFinished();
if constexpr (std::is_same_v<decltype(future), QFuture<void>>) {
return;
} else {
return future.result();
}
}
private:
class AbstractCallEvent : public QEvent
{
public:
AbstractCallEvent(QEvent::Type type)
: QEvent(type)
{
}
virtual void call() = 0;
};
template <typename Func, typename... Args>
class Q_DECL_HIDDEN CallEvent : public AbstractCallEvent
{
using FunInfo = QtPrivate::FunctionPointer<std::decay_t<Func>>;
using ReturnType = std::invoke_result_t<std::decay_t<Func>, Args...>;
public:
CallEvent(QEvent::Type type, Func &&fun, Args &&...args)
: AbstractCallEvent(type)
, function(std::forward<Func>(fun))
, arguments(std::forward<Args>(args)...)
{
}
QEvent *clone() const override { return nullptr; }
void call() override
{
if (promise.isCanceled()) {
return;
}
if (contextChecker == context) {
promise.start();
#ifndef QT_NO_EXCEPTIONS
try {
#endif
if constexpr (std::is_void_v<ReturnType>) {
std::apply(function, arguments);
} else {
promise.addResult(std::apply(function, arguments));
}
#ifndef QT_NO_EXCEPTIONS
} catch (...) {
promise.setException(std::current_exception());
}
#endif
promise.finish();
} else {
promise.start();
promise.setException(std::make_exception_ptr(std::runtime_error("The context object is destroyed.")));
promise.finish();
}
}
Func function;
const std::tuple<Args...> arguments;
QPromise<ReturnType> promise;
QObject *context{nullptr};
QPointer<QObject> contextChecker;
};
template <typename Func, typename... Args>
inline auto call(QObject *context, Func fun, Args &&...args)
{
using FuncInfo = QtPrivate::FunctionPointer<std::decay_t<Func>>;
using ReturnType = std::invoke_result_t<std::decay_t<Func>, Args...> ;
if constexpr (FuncInfo::IsPointerToMemberFunction) {
static_assert(std::is_same_v<std::decay_t<typename QtPrivate::List<Args...>::Car>, typename FuncInfo::Object>,
"The obj and function are not compatible.");
static_assert(
QtPrivate::CheckCompatibleArguments<typename QtPrivate::List<Args...>::Cdr, typename FuncInfo::Arguments>::value,
"The args and function are not compatible.");
} else if constexpr (FuncInfo::ArgumentCount != -1) {
static_assert(QtPrivate::CheckCompatibleArguments<QtPrivate::List<Args...>, typename FuncInfo::Arguments>::value,
"The args and function are not compatible.");
} else { // for lambda and impl operator()
static_assert(std::is_invocable_r_v<ReturnType, Func, Args...>,
"The callable object can't invoke with supplied args");
}
QPromise<ReturnType> promise;
auto future = promise.future();
if (Q_UNLIKELY(QThread::currentThread() == m_thread)) {
promise.start();
if constexpr (std::is_void_v<ReturnType>) {
std::invoke(fun, std::forward<Args>(args)...);
} else {
promise.addResult(std::invoke(fun, std::forward<Args>(args)...));
}
promise.finish();
} else {
auto event = new CallEvent<Func, Args...>(eventType, std::move(fun), std::forward<Args>(args)...);
event->promise = std::move(promise);
event->context = context;
event->contextChecker = context;
QCoreApplication::postEvent(ensureThreadContextObject(), event);
}
return future;
}
QObject *ensureThreadContextObject();
static inline QEvent::Type eventType;
QThread *m_thread;
QAtomicPointer<QObject> threadContext;
};
#endif // version macro end
DCORE_END_NAMESPACE
#endif // DTHREADUTILS_H
#endif // protect macro end

View File

@ -66,12 +66,12 @@ if(LINUX)
target_link_libraries(
${LIB_NAME} PUBLIC
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::CorePrivate
Qt${QT_VERSION_MAJOR}::DBus
Qt${QT_VERSION_MAJOR}::Xml
)
target_link_libraries(${LIB_NAME} PRIVATE
ICU::uc
Qt${QT_VERSION_MAJOR}::CorePrivate
uchardet
)
if("${QT_VERSION_MAJOR}" STREQUAL "5")

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2020 - 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
@ -6,6 +6,7 @@
DCORE_BEGIN_NAMESPACE
#if DTK_VERSION < DTK_VERSION_CHECK(6, 0, 0, 0)
namespace DThreadUtil {
FunctionCallProxy::FunctionCallProxy(QThread *thread)
{
@ -41,6 +42,67 @@ void FunctionCallProxy::proxyCall(QSemaphore *s, QThread *thread, QObject *targe
proxy.callInLiveThread(s, target ? target : &proxy, &fun);
s->acquire();
}
} // end namespace DThreadUtil
}
#else
class Q_DECL_HIDDEN Caller : public QObject
{
public:
explicit Caller()
: QObject()
{
}
bool event(QEvent *event) override
{
if (event->type() == DThreadUtils::eventType) {
auto ev = static_cast<DThreadUtils::AbstractCallEvent *>(event);
ev->call();
return true;
}
return QObject::event(event);
}
};
DThreadUtils::DThreadUtils(QThread *thread)
: m_thread(thread)
, threadContext(nullptr)
{
}
DThreadUtils::~DThreadUtils()
{
delete threadContext.loadRelaxed();
}
DThreadUtils &DThreadUtils::gui()
{
static auto global = DThreadUtils(QCoreApplication::instance()->thread());
return global;
}
QThread *DThreadUtils::thread() const noexcept
{
return m_thread;
}
QObject *DThreadUtils::ensureThreadContextObject()
{
QObject *context;
if (!threadContext.loadRelaxed()) {
context = new Caller();
context->moveToThread(m_thread);
if (!threadContext.testAndSetRelaxed(nullptr, context)) {
context->moveToThread(nullptr);
delete context;
}
}
context = threadContext.loadRelaxed();
Q_ASSERT(context);
return context;
}
#endif
DCORE_END_NAMESPACE

View File

@ -40,10 +40,17 @@ else()
)
endif()
file(GLOB UTILS_HEADER
${CMAKE_CURRENT_LIST_DIR}/../../include/util/*
${PROJECT_SOURCE_DIR}/include/util/*.h
${CMAKE_CURRENT_LIST_DIR}/ddbusinterface_p.h
${CMAKE_CURRENT_LIST_DIR}/ddbusextendedpendingcallwatcher_p.h
)
if("${QT_VERSION_MAJOR}" STREQUAL "6")
list(REMOVE_ITEM UTILS_SOURCE "${CMAKE_CURRENT_LIST_DIR}/dtimedloop.cpp")
list(REMOVE_ITEM UTILS_HEADER "${PROJECT_SOURCE_DIR}/include/util/dtimedloop.h") # no longer be used
list(REMOVE_ITEM UTILS_HEADER "${PROJECT_SOURCE_DIR}/include/util/dasync.h")
endif()
set(utils_SRC
${UTILS_HEADER}
${UTILS_SOURCE}

View File

@ -80,6 +80,7 @@ file(GLOB FackDBus
if("${QT_VERSION_MAJOR}" STREQUAL "6")
list(REMOVE_ITEM TEST_SOURCE "${CMAKE_CURRENT_LIST_DIR}/ut_gsettingsbackend.cpp")
list(REMOVE_ITEM TEST_SOURCE "${CMAKE_CURRENT_LIST_DIR}/ut_dasync.cpp")
endif()
set(test_SRC

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2017 - 2022 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2017 - 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
@ -14,7 +14,9 @@ int main(int argc, char *argv[])
QCoreApplication app(argc, argv);
app.setApplicationName("tests");
app.setOrganizationName("deepin");
#if DTK_VERSION < DTK_VERSION_CHECK(6, 0, 0, 0)
DTimedLoop loop;
#endif
testing::InitGoogleTest(&argc, argv);
int retVal = RUN_ALL_TESTS();
@ -23,5 +25,9 @@ int main(int argc, char *argv[])
__sanitizer_set_report_path("asan.log");
#endif
#if DTK_VERSION < DTK_VERSION_CHECK(6, 0, 0, 0)
return loop.exec(0, "main execution") + retVal;
#else
return 0;
#endif
}

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2020 - 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
@ -6,11 +6,16 @@
#include <gtest/gtest.h>
#include <QTest>
#include <QtConcurrent>
#include <QSignalSpy>
#include <DThreadUtils>
#include <string>
#include <thread>
#include <chrono>
DCORE_USE_NAMESPACE
#if DTK_VERSION < DTK_VERSION_CHECK(6, 0, 0, 0)
class ThreadUtils : public QObject
{
Q_OBJECT
@ -59,4 +64,198 @@ TEST_F(ut_DThreadUtils, CallInMainThread)
m_threadutil->testCallInMainThread();
}
#else
class ut_DThreadUtils :public testing::Test{
public:
virtual void SetUp()
{
t = new QThread();
t->start();
m_threadutil = new DThreadUtils(t);
}
virtual void TearDown()
{
t->exit();
t->wait();
delete t;
delete m_threadutil;
}
protected:
QThread *t{nullptr};
DThreadUtils *m_threadutil{nullptr};
};
class QWorker : public QObject{
Q_OBJECT
public:
explicit QWorker(QObject *parent = nullptr):QObject(parent){}
~QWorker() = default;
public Q_SLOTS:
int testFunc(int i, double j) {
int r = i + j;
std::this_thread::sleep_for(std::chrono::seconds(2));
emit testFuncTrigger(r);
return i + j;
}
Q_SIGNALS:
void testFuncTrigger(int v);
};
class CallableObject{
public:
CallableObject() = default;
~CallableObject() = default;
QString operator()(const QString& str){
s += str;
return s;
}
QString testFunc(const QString& str){
return s;
}
private:
QString s{"CallableObject: "};
};
TEST_F(ut_DThreadUtils,testThread)
{
auto tmp = m_threadutil->thread();
EXPECT_EQ(tmp, t);
}
TEST_F(ut_DThreadUtils, testRunWithQObj)
{
QWorker w;
QSignalSpy spy(&w,SIGNAL(testFuncTrigger(int)));
auto result = m_threadutil->run(&w, &QWorker::testFunc, 10, 24.6);
std::this_thread::sleep_for(std::chrono::seconds(1));
EXPECT_TRUE(result.isStarted());
EXPECT_TRUE(result.isRunning());
result.waitForFinished();
EXPECT_TRUE(result.isFinished());
auto raw = result.result();
EXPECT_EQ(raw, 34);
EXPECT_EQ(spy.count(), 1);
}
TEST_F(ut_DThreadUtils,testRunWithLambda)
{
const QString& ref = "long ref";
int num = 10;
auto threadId1 = std::this_thread::get_id();
auto result = m_threadutil->run([&num](decltype(threadId1) id){
EXPECT_NE(std::this_thread::get_id(),id);
return true;
},threadId1);
result.waitForFinished();
auto raw = result.result();
EXPECT_TRUE(raw);
}
TEST_F(ut_DThreadUtils,testRunWithCallableObj){
CallableObject obj;
QString tmp{"Hello"};
const auto& ref = tmp;
auto result1 = m_threadutil->run(obj,tmp);
result1.waitForFinished();
auto raw1 = result1.result();
EXPECT_EQ(raw1, QString{"CallableObject: Hello"});
auto result2 = m_threadutil->run(&obj,&CallableObject::testFunc, tmp);
result2.waitForFinished();
auto raw2 = result2.result();
EXPECT_EQ(raw2, QString{"CallableObject: "});
}
TEST_F(ut_DThreadUtils, testExecWithQObj)
{
QWorker w;
QSignalSpy spy(&w, SIGNAL(testFuncTrigger(int)));
auto result = m_threadutil->exec(&w, &QWorker::testFunc, 10, 24.6);
EXPECT_EQ(result, 34);
EXPECT_EQ(spy.count(), 1);
}
TEST_F(ut_DThreadUtils, testExecWithLambda)
{
const QString &ref = "long ref";
int num = 10;
auto threadId1 = std::this_thread::get_id();
auto result = m_threadutil->exec(
[&num](decltype(threadId1) id) {
EXPECT_NE(std::this_thread::get_id(), id);
return true;
},
threadId1);
EXPECT_TRUE(result);
}
TEST_F(ut_DThreadUtils, testExecWithCallableObj)
{
CallableObject obj;
QString tmp{"Hello"};
const auto &ref = tmp;
auto result1 = m_threadutil->exec(obj, tmp);
EXPECT_EQ(result1, QString{"CallableObject: Hello"});
auto result2 = m_threadutil->exec(&obj, &CallableObject::testFunc, tmp);
EXPECT_EQ(result2, QString{"CallableObject: "});
}
TEST_F(ut_DThreadUtils, testDirectlyInvoke)
{
DThreadUtils tu(QThread::currentThread());
QWorker w;
QSignalSpy spy(&w, SIGNAL(testFuncTrigger(int)));
auto result = tu.run(&w, &QWorker::testFunc, 10, 24.6);
auto raw = result.result(); // no wait
EXPECT_EQ(raw, 34);
EXPECT_EQ(spy.count(), 1);
}
TEST_F(ut_DThreadUtils, testCancel)
{
CallableObject obj;
QString tmp{"Hello"};
int cancelCounter{0};
const auto &ref = tmp;
auto result1 = m_threadutil->run(obj, tmp);
auto cancelResult = result1.onCanceled([&cancelCounter]() {
cancelCounter += 1;
return QString{"failed"};
});
result1.cancel();
EXPECT_FALSE(result1.isFinished());
EXPECT_FALSE(result1.isValid());
EXPECT_TRUE(result1.isCanceled());
cancelResult.waitForFinished();
EXPECT_EQ(cancelCounter, 1);
EXPECT_EQ(cancelResult.result(), QString{"failed"});
}
TEST_F(ut_DThreadUtils, testDestructCancel)
{
auto w = new QWorker{};
auto failedCounter{0};
auto result = m_threadutil->run(w, &QWorker::testFunc, 10, 24.6);
delete w;
auto failedResult = result.onFailed([&failedCounter]() {
failedCounter += 1;
return -1;
});
EXPECT_FALSE(result.isValid());
failedResult.waitForFinished();
EXPECT_EQ(failedCounter, 1);
EXPECT_EQ(failedResult.result(), -1);
}
#endif
#include "ut_dthreadutils.moc"

View File

@ -1,13 +1,14 @@
// SPDX-FileCopyrightText: 2017 - 2022 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2017 - 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#pragma once
#include "dtkcore_global.h"
#include <gtest/gtest.h>
#if DTK_VERSION < DTK_VERSION_CHECK(6, 0, 0, 0)
#include "dtimedloop.h"
DCORE_USE_NAMESPACE
#endif
// 有返回值的 lambda 表达式、函数里面使用 ASSERT_XXX
// 总之,不管有没有返回值,用它就对了