可变/可升级的递归互斥锁





4.00/5 (2投票s)
boost::upgrade_mutex 的扩展
头文件修复。
“知识是有限的,想象力包围着整个世界。”——阿尔伯特·爱因斯坦
引言
这是一个库。
我设计了一种新型的互斥锁:mutable_recursive_mutex
。
它是 boost::upgrade_mutex
及其相关锁的扩展。这段代码功能齐全并在 Windows 7 上进行了测试。在开发过程中,我发现我的软件性能大幅提升。它比使用标准互斥锁快了 10 倍。
这很容易解释:我有 12 个线程使用相同的共享资源。它们几乎 90% 的时间都花在读取数据上。如果使用标准锁定,所有访问都将被串行化... 所有这些线程都是偶尔的写入者:这是关键点。
让我向您介绍我的解决方案。
我的意思是访问是可变的:可升级和可降级。
例如,一个线程可以从共享访问开始,然后将其更改为独占访问,再改回共享访问,最后释放互斥锁。
特点
- 100% 基于 Boost 库,它是 boost::thread 的扩展
- 没有调用系统特定的函数。
- 完全递归。
- 可以暂停/恢复锁定。
- 公平锁定。
- 可选:定义一个主线程:从功能角度来看,这使其不可中断。
- 使用相关锁时允许所有转换:
mutable_xxxxx_lock
。 - 应在 Boost 库支持的所有平台上工作。
注意:我没有实现所有功能。只实现了(对我而言)需要的功能。但如果你喜欢这个,欢迎自由扩展它。
锁
- mutable_unique_lock
- mutable_shared_lock
- mutable_suspend_lock
- 非成员函数:suspend_lock
背景
读者必须熟悉 Boost 线程库和 C++。这是为高级用户准备的。我不会解释线程,也不会解释为什么锁定公共资源是强制性的。有很多很棒的教程可以解释这些。
使用代码
它是一个标准的递归互斥锁,带有一些额外的改进。
它的工作原理如下。
#include "mutable_recursive_mutex.hpp"
mutable_recursive_mutex my_mutex;
int _tmain(int argc, _TCHAR* argv[])
{
// defines current thread as main thread.
my_mutex.set_main_thread_id();
mutable_shared_lock read_lock(my_mutex);
// do some read-only work
mutable_unique_lock write_lock(my_mutex);
// Now ask to promote your access to exclusive.
// You can safely write data now.
return 0;
}
公平锁定
这仅仅意味着等待时间最长的线程将获得所有权
主线程
如果使用,此线程将绕过公平锁定,并始终获得所有权。
它被设计用于优先处理最重要的数据。
系统更能保证数据从共享状态转移到独占状态(再返回)时的完整性。
除非调用“suspend/resume”。
Mutable_suspend_lock
此锁的目的是无论互斥锁的状态和锁定递归级别如何,都能释放互斥锁。
它主要设计用于在不冒死锁风险的情况下停止应用程序。
就像其他状态一样,“suspended”状态也是递归的。
进入“suspended”状态时,所有其他锁定/解锁函数都会被停用。唯一工作的函数是:
- suspend
- resume
{
mutable_suspend_lock lock(my_mutex);
// current has released the mutex
}
//Or
my_mutex.suspend();
所有后续对
my_mutex.lock()
my_mutex.lock_shared()
my_mutex.unlock()
my_mutex.unlock_shared()
的调用都会被忽略。递归栈不会被修改,直到作用域结束和/或调用:my_mutex.resume()
。
工作原理
- 线程状态
我定义了 3 种线程状态,外加一个元状态:lst_suspended
。
typedef enum
{
lst_none,
lst_shared,
lst_unique,
lst_suspended
} lock_state_t;
这些状态存储在 std::dequeue
中
typedef std::deque<lock_state_t> thread_recursion_stack_t;
每个线程都有自己的堆栈
boost::thread_specific_ptr<thread_recursion_stack_t> m_thread_states;
当为线程创建堆栈时,推入堆栈的第一个元素始终是: lst_none
。这意味着线程对互斥锁没有任何访问权限。它类似于 lst_suspended
。
但是 lst_none
只使用一次,而且只使用一次。与 lst_suspended
不同。
- 升级
这是这项工作的核心。当线程想要获得独占访问时,内部会调用 promote 函数。
它首先会尝试获取所有权。如果被拒绝(由 upgrade_mutex
)。它将等待一个条件:m_promote_condition
。
每次线程解锁时,该条件都会被发出信号。然后所有其他线程都会被唤醒并再次尝试获取所有权。
现在我们来看看
void mutable_recursive_mutex::promote(thread_recursion_stack_t * stack)
{
boost::thread::id thread_id = boost::this_thread::get_id();
boost::mutex helper_mutex;
boost::unique_lock<boost::mutex> helper_lock(helper_mutex);
bool bnotify = (stack->front() == lst_shared);
boost::unique_lock<boost::mutex> promote_lock(m_promote_mutex);
if (m_is_main_thread_id_assigned && (thread_id == m_main_thread_id))
m_waiting_promote_threads.push_front(thread_id);
else
m_waiting_promote_threads.push_back(thread_id);
if (bnotify)
m_mutex.unlock_shared();
while (true)
{
if (m_waiting_promote_threads.front() == thread_id)
{
bnotify = false;
if (m_mutex.try_lock())
{
m_waiting_promote_threads.pop_front();
break;
}
}
if (bnotify)
m_promote_condition.notify_all();
bnotify = false;
promote_lock.unlock();
m_promote_condition.wait(helper_lock);
promote_lock.lock();
}
}
helper_lock 和 helper_mutex
的唯一目的是让 “m_promote_condition.wait(helper_lock);
” 工作。它们根本不锁定任何东西。
if (m_is_main_thread_id_assigned && (thread_id == m_main_thread_id))
m_waiting_promote_threads.push_front(thread_id);
else
m_waiting_promote_threads.push_back(thread_id);
这是带有主线程的公平锁定子系统。
如果查看整个代码,您会看到 promote 函数是从以下状态调用的
- lst_none
- lst_shared
- lst_suspended(在这种情况下类似于 lst_none)
当它是 lst_shared
时,不能说我们会立即获得锁。因此,共享访问被释放。规则是当访问被释放时,条件必须被发出信号。
- promote_lock 问题
=> 为了速度原因,我尽可能晚地进行。
=> 如果我把这项工作就这样留下。那就会有死锁的风险。让我解释一下问题在哪里以及我是如何解决的。问题只在于以下两行
promote_lock.unlock();
m_promote_condition.wait(helper_lock);
这两行可能导致死锁,因为它们不是原子执行的。此外,大多数 Boost 库都在用户空间完成,并调用执行原子操作的操作系统特定函数。
因此,如果您不想在内核级别开发并且没有外部帮助,这个问题就没有解决方案。
我通过使用一个定期发出条件信号的线程简单地解决了它:这就是外部帮助。请看:unlock_thread
void mutable_recursive_mutex::unlock_thread()
{
boost::chrono::milliseconds ms(100);
try
{
while (true)
{
boost::this_thread::sleep_for(ms);
boost::unique_lock<boost::mutex> promote_lock(m_promote_mutex);
if (!m_waiting_promote_threads.empty())
m_promote_condition.notify_all();
}
} catch (boost::thread_interrupted)
{
}
}
操作方法
如果您打算使用公平锁定和/或“主线程”,请定义:FAIR_LOCKING
1) 想要使用这些类的人必须能够回答以下问题
- 我有一个所有事物都基于的线程吗?
- 或者它们都等价吗?
答案会告诉您是否需要定义“主线程”。我不建议更改“主线程”,即使它是安全的。
我强烈建议定义一个“主线程”并一劳永逸!
2) 策略
从一般角度看,循环线程应该像这样。感谢 Espen Harlinn(他告诉我澄清这一点)
void std_thread()
{
boost::chrono::milliseconds ms(20);
// right place to define if needed
my_mutex.set_main_thread_id();
while (true)
{
boost::this_thread::sleep_for(ms);
mutable_shared_lock read_lock(my_mutex);
//
}
my_mutex.reset_main_thread_id();
}
这样,工作可以尽可能并行地开始。这可能会提高性能。
当需要时,要求将其提升为独占。这是通过以下方式完成的
mutable_unique_lock write_lock(my_mutex);
3) 提示和技巧
要注意这一点:请求升级是耗时的。因此,关键在于:我最晚可以在哪里请求升级。并尽可能避免多次进行。
无论如何,一旦线程获取了唯一的锁,在锁的作用域内,其他唯一的锁请求都非常快(对于该线程而言):互斥锁知道该线程拥有唯一的访问权限。我只会更新递归栈。
请注意:此解决方案提供了比标准递归锁更多的功能,但需要付出代价:它非常耗时。
示例项目
如果需要,我可以提供一个示例项目。