-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
227 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
/// @file | ||
/// @brief Class @ref cubos::core::thread::Task. | ||
/// @ingroup core-thread | ||
|
||
#pragma once | ||
|
||
#include <condition_variable> | ||
#include <mutex> | ||
|
||
#include <cubos/core/log.hpp> | ||
|
||
namespace cubos::core::thread | ||
{ | ||
/// @brief Provides a mechanism to access the results of asynchronous operations. | ||
/// @tparam T Result type. | ||
/// @ingroup core-thread | ||
template <typename T> | ||
class Task final | ||
{ | ||
public: | ||
~Task() | ||
{ | ||
this->discard(); | ||
} | ||
|
||
/// @brief Constructs. | ||
Task() | ||
{ | ||
mData = new Data(); | ||
} | ||
|
||
/// @brief Copy constructs. | ||
/// @param other Task. | ||
Task(const Task& other) | ||
: mData{other.mData} | ||
{ | ||
std::unique_lock lock{mData->mMutex}; | ||
mData->mRefCount += 1; | ||
} | ||
|
||
/// @brief Move constructs. | ||
/// @param other Task. | ||
Task(Task&& other) noexcept | ||
: mData{other.mData} | ||
{ | ||
other.mData = nullptr; | ||
} | ||
|
||
/// @brief Copy assigns. | ||
/// @param other Task. | ||
Task& operator=(const Task& other) | ||
{ | ||
this->discard(); | ||
mData = other.mData; | ||
std::unique_lock lock{mData->mMutex}; | ||
mData->mRefCount += 1; | ||
} | ||
|
||
/// @brief Move assigns. | ||
/// @param other Task. | ||
Task& operator=(Task&& other) noexcept | ||
{ | ||
this->discard(); | ||
mData = other.mData; | ||
other.mData = nullptr; | ||
} | ||
|
||
/// @brief Finishes the task, setting its result and notifying a waiting thread. | ||
/// @param value Task result. | ||
void finish(T value) | ||
{ | ||
CUBOS_ASSERT(mData != nullptr, "Task has been discarded"); | ||
|
||
// Set the result and notify one waiting thread. | ||
std::unique_lock lock(mData->mMutex); | ||
CUBOS_ASSERT(!mData->mDone, "Task has already been finished"); | ||
|
||
new (&mData->mValue) T(std::move(value)); | ||
mData->mDone = true; | ||
mData->mCondition.notify_all(); | ||
} | ||
|
||
/// @brief Discards any result eventually received. The task is left in an invalid state. | ||
void discard() | ||
{ | ||
if (mData != nullptr) | ||
{ | ||
std::unique_lock lock{mData->mMutex}; | ||
mData->mRefCount -= 1; | ||
if (mData->mRefCount == 0) | ||
{ | ||
lock.unlock(); | ||
delete mData; | ||
} | ||
mData = nullptr; | ||
} | ||
} | ||
|
||
/// @brief Returns whether the task has finished. | ||
/// @return Whether the task has finished. | ||
bool isDone() const | ||
{ | ||
CUBOS_ASSERT(mData != nullptr, "Task has been discarded"); | ||
std::unique_lock lock(mData->mMutex); | ||
return mData->mDone; | ||
} | ||
|
||
/// @brief Blocks until the task finishes and then returns its result. | ||
/// @return Task result. | ||
T result() | ||
{ | ||
CUBOS_ASSERT(mData != nullptr, "Task has been discarded"); | ||
|
||
// Wait until a result is obtained. When it is, consume it. | ||
{ | ||
std::unique_lock lock(mData->mMutex); | ||
mData->mCondition.wait(lock, [this]() { return mData->mDone; }); | ||
CUBOS_ASSERT(!mData->mConsumed, "Task result has already been consumed"); | ||
mData->mConsumed = true; | ||
} | ||
|
||
// Move the result and reset the task. | ||
T result = std::move(mData->mValue); | ||
this->discard(); | ||
return result; | ||
} | ||
|
||
private: | ||
/// @brief Tracks the eventual result of the task, and is used to deliver it. | ||
struct Data | ||
{ | ||
int mRefCount{1}; | ||
bool mDone{false}; | ||
bool mConsumed{false}; | ||
union { | ||
T mValue; | ||
}; | ||
|
||
std::mutex mMutex; | ||
std::condition_variable mCondition; | ||
|
||
// NOLINTBEGIN(modernize-use-equals-default) | ||
Data() | ||
{ | ||
} | ||
// NOLINTEND(modernize-use-equals-default) | ||
|
||
~Data() | ||
{ | ||
if (mDone) | ||
{ | ||
mValue.~T(); | ||
} | ||
} | ||
}; | ||
|
||
Data* mData; | ||
}; | ||
} // namespace cubos::core::thread |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
#include <cubos/core/thread/task.hpp> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
#include <thread> | ||
|
||
#include <doctest/doctest.h> | ||
|
||
#include <cubos/core/thread/task.hpp> | ||
|
||
#include "../utils.hpp" | ||
|
||
using cubos::core::thread::Task; | ||
|
||
TEST_CASE("thread::Task") | ||
{ | ||
SUBCASE("task which produces an int") | ||
{ | ||
Task<int> task{}; | ||
|
||
REQUIRE_FALSE(task.isDone()); | ||
|
||
auto thread = std::thread{[&task]() { | ||
std::this_thread::sleep_for(std::chrono::milliseconds{100}); | ||
task.finish(42); | ||
}}; | ||
|
||
SUBCASE("join before") | ||
{ | ||
thread.join(); | ||
REQUIRE(task.isDone()); | ||
REQUIRE(task.result() == 42); | ||
} | ||
|
||
SUBCASE("join after") | ||
{ | ||
task.isDone(); // May return either true or false. | ||
REQUIRE(task.result() == 42); | ||
thread.join(); | ||
} | ||
} | ||
|
||
SUBCASE("check if task is destroyed properly") | ||
{ | ||
Task<DetectDestructor> task{}; | ||
bool destroyed = false; | ||
|
||
// A copy of the task is finished. | ||
{ | ||
Task<DetectDestructor> task2{task}; | ||
REQUIRE_FALSE(task.isDone()); | ||
REQUIRE_FALSE(task2.isDone()); | ||
task2.finish({&destroyed}); | ||
REQUIRE_FALSE(destroyed); | ||
REQUIRE(task2.isDone()); | ||
} | ||
REQUIRE(task.isDone()); | ||
|
||
// Value isn't destroyed yet. | ||
REQUIRE_FALSE(destroyed); | ||
|
||
// Value is destroyed only after being accessed. | ||
task.result(); | ||
REQUIRE(destroyed); | ||
} | ||
} |