这种基于线程 ID 的同步是否安全整洁?
Is such thread-id based synchronization safe and neat?
我编写了一个小的 AI 程序来解决 IQ Twist 难题。然后作为练习,我将其并行化。这是来源:
IQTwist Resolver.
搜索解决方案的算法是递归的。
我在递归函数的关键部分使用了基于线程 ID 的同步(将找到的形状收集到成员数组)。我指的关键部分:
bool IQTwistResolver::searchForSolution(const int type, const uint32 a_union)
{
if (m_stopSearch) //make all other threads unwind recursion and finish
return false;
if (TYPES_COUNT == type) //all types reached and solved - end recursion
{
m_mutex.lock();
if (std::thread::id::id() == m_firstFinder || std::this_thread::get_id() == m_firstFinder)
{
m_firstFinder = std::this_thread::get_id();
m_stopSearch = true;
}
m_mutex.unlock();
return true; //return true to collect the solution and unwind
}
...
我正在向专家寻求建议:
这种方法是否有任何可能的 weaknesses/flaws 或矫枉过正(也许我遗漏了一些更简单的解决方案)?
您会使用其他 'solution buffer' 保护方法吗?
也许您会使用完全不同的并行化方案(这也值得了解)?
您的解决方案应该会按预期工作,但是,使用 std::mutex
是最通用和最昂贵的解决方案。
另一种选择是使用 std::call_once
,它确保只有第一个线程进行调用。 IE。第一个找到解决方案的线程将设置搜索结果的值。
或者,您可以使用 std::atomic
来避免使用互斥体。而不是线程 id,线程特定变量的地址就足以区分线程。
例如:
#include <iostream>
#include <atomic>
#include <thread>
class FirstThread {
static thread_local std::thread::id const thread_id_;
std::atomic<std::thread::id const*> first_{0};
public:
std::thread::id const& id() const noexcept {
return thread_id_;
}
bool try_become_first() noexcept {
std::thread::id const* expected = 0;
return first_.compare_exchange_strong(expected, &thread_id_, std::memory_order_relaxed, std::memory_order_relaxed);
}
bool is_first() const noexcept {
return first_.load(std::memory_order_relaxed) == &thread_id_;
}
};
thread_local std::thread::id const FirstThread::thread_id_ = std::this_thread::get_id();
int main() {
FirstThread ft;
auto f = [&ft]() {
ft.try_become_first();
std::cout << "thread " << ft.id() << " is first: " << ft.is_first() << '\n';
};
f();
std::thread(f).join();
}
输出:
thread 139892443997984 is first: 1
thread 139892384220928 is first: 0
请注意,如果您不需要 std::this_thread::get_id()
返回的真实线程 ID,您可以只使用特定于线程的地址 bool
来标识不同的线程。
我放置 post 8 个月后,我意识到这个问题:
“这种方法是否有任何(...)矫枉过正(也许我缺少更简单的解决方案)?”
正确的答案是:是的,这是一种矫枉过正和代码混乱。线程 ID 和 m_firstFinder
成员的所有麻烦都是不必要的。
我已经使用了一个 bool
变量 m_stopSearch
,这可以完美地达到相同的同步目的,所以应该:
bool IQTwistResolver::searchForSolution(const int type, const uint32 a_union)
{
if (m_stopSearch) //make all other threads unwind recursion and finish
return false;
if (TYPES_COUNT == type) //all types reached and solved - end recursion
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_stopSearch)
{
m_stopSearch = true;
return true; //return true to collect the solution and unwind
}
return false; //make "late" solving thread return false to skip solution collection
}
...
我知道在这样一个松散的、“非完整上下文”的问题中,可能没有人真正遇到过这方面的问题,抱歉给您带来麻烦。为了保持validity/consistency我补充了我的发现
我编写了一个小的 AI 程序来解决 IQ Twist 难题。然后作为练习,我将其并行化。这是来源: IQTwist Resolver.
搜索解决方案的算法是递归的。
我在递归函数的关键部分使用了基于线程 ID 的同步(将找到的形状收集到成员数组)。我指的关键部分:
bool IQTwistResolver::searchForSolution(const int type, const uint32 a_union)
{
if (m_stopSearch) //make all other threads unwind recursion and finish
return false;
if (TYPES_COUNT == type) //all types reached and solved - end recursion
{
m_mutex.lock();
if (std::thread::id::id() == m_firstFinder || std::this_thread::get_id() == m_firstFinder)
{
m_firstFinder = std::this_thread::get_id();
m_stopSearch = true;
}
m_mutex.unlock();
return true; //return true to collect the solution and unwind
}
...
我正在向专家寻求建议:
这种方法是否有任何可能的 weaknesses/flaws 或矫枉过正(也许我遗漏了一些更简单的解决方案)?
您会使用其他 'solution buffer' 保护方法吗?
也许您会使用完全不同的并行化方案(这也值得了解)?
您的解决方案应该会按预期工作,但是,使用 std::mutex
是最通用和最昂贵的解决方案。
另一种选择是使用 std::call_once
,它确保只有第一个线程进行调用。 IE。第一个找到解决方案的线程将设置搜索结果的值。
或者,您可以使用 std::atomic
来避免使用互斥体。而不是线程 id,线程特定变量的地址就足以区分线程。
例如:
#include <iostream>
#include <atomic>
#include <thread>
class FirstThread {
static thread_local std::thread::id const thread_id_;
std::atomic<std::thread::id const*> first_{0};
public:
std::thread::id const& id() const noexcept {
return thread_id_;
}
bool try_become_first() noexcept {
std::thread::id const* expected = 0;
return first_.compare_exchange_strong(expected, &thread_id_, std::memory_order_relaxed, std::memory_order_relaxed);
}
bool is_first() const noexcept {
return first_.load(std::memory_order_relaxed) == &thread_id_;
}
};
thread_local std::thread::id const FirstThread::thread_id_ = std::this_thread::get_id();
int main() {
FirstThread ft;
auto f = [&ft]() {
ft.try_become_first();
std::cout << "thread " << ft.id() << " is first: " << ft.is_first() << '\n';
};
f();
std::thread(f).join();
}
输出:
thread 139892443997984 is first: 1
thread 139892384220928 is first: 0
请注意,如果您不需要 std::this_thread::get_id()
返回的真实线程 ID,您可以只使用特定于线程的地址 bool
来标识不同的线程。
我放置 post 8 个月后,我意识到这个问题:
“这种方法是否有任何(...)矫枉过正(也许我缺少更简单的解决方案)?”
正确的答案是:是的,这是一种矫枉过正和代码混乱。线程 ID 和 m_firstFinder
成员的所有麻烦都是不必要的。
我已经使用了一个 bool
变量 m_stopSearch
,这可以完美地达到相同的同步目的,所以应该:
bool IQTwistResolver::searchForSolution(const int type, const uint32 a_union)
{
if (m_stopSearch) //make all other threads unwind recursion and finish
return false;
if (TYPES_COUNT == type) //all types reached and solved - end recursion
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_stopSearch)
{
m_stopSearch = true;
return true; //return true to collect the solution and unwind
}
return false; //make "late" solving thread return false to skip solution collection
}
...
我知道在这样一个松散的、“非完整上下文”的问题中,可能没有人真正遇到过这方面的问题,抱歉给您带来麻烦。为了保持validity/consistency我补充了我的发现