65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2012年12月18日

CPOL

5分钟阅读

viewsIcon

21488

downloadIcon

194

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) 提示和技巧

要注意这一点:请求升级是耗时的。因此,关键在于:我最晚可以在哪里请求升级。并尽可能避免多次进行。

无论如何,一旦线程获取了唯一的锁,在锁的作用域内,其他唯一的锁请求都非常快(对于该线程而言):互斥锁知道该线程拥有唯一的访问权限。我只会更新递归栈。

请注意:此解决方案提供了比标准递归锁更多的功能,但需要付出代价:它非常耗时。

示例项目

如果需要,我可以提供一个示例项目。

© . All rights reserved.