【问题标题】:How to synchronize access to many objects如何同步访问多个对象
【发布时间】:2011-02-12 13:08:20
【问题描述】:

我有一个线程池,其中包含一些线程(例如,与内核数量一样多)可以处理许多对象,比如数千个对象。通常我会给每个对象一个互斥锁来保护对其内部的访问,当我工作时锁定它,然后释放它。当两个线程试图访问同一个对象时,其中一个线程必须等待。

现在我想节省一些资源并具有可扩展性,因为可能有数千个对象,但仍然只有一个满手的线程。我正在考虑一种类设计,其中线程具有某种互斥锁或锁定对象,并在应该访问对象时将锁定分配给对象。这将节省资源,因为我拥有的锁对象数量与线程数量一样多。

现在是编程部分,我想将这个设计转换为代码,但不知道从哪里开始。我正在用 C++ 编程,并希望尽可能使用 Boost 类,但处理这些特殊要求的自写类是可以的。我将如何实现它?

我的第一个想法是每个线程都有一个 boost::mutex 对象,每个对象都有一个 boost::shared_ptr 最初是未设置的(或 NULL)。现在,当我想访问该对象时,我通过创建一个 scoped_lock 对象并将其分配给 shared_ptr 来锁定它。当 shared_ptr 已经设置时,我等待当前的锁。这个想法听起来像是一堆竞争条件,所以我有点放弃了它。有没有另一种方法来完成这个设计?完全不同的方式?

编辑: 上面的描述有点抽象,所以我添加一个具体的例子。想象一个有许多对象的虚拟世界(想想> 100.000)。在世界中移动的用户可以在世界中移动并修改对象(例如向怪物射箭)。当只使用一个线程时,我很适合对对象的修改进行排队的工作队列。不过,我想要一个更具可扩展性的设计。如果有 128 个核心处理器可用,我想使用全部 128 个,所以使用那个数量的线程,每个线程都有工作队列。一种解决方案是使用空间分离,例如对一个区域使用锁。这可以减少使用的锁的数量,但如果有一种设计可以节省尽可能多的锁,我会更感兴趣。

【问题讨论】:

    标签: c++ multithreading boost synchronization


    【解决方案1】:

    您可以使用互斥池而不是为每个资源分配一个互斥锁或为每个线程分配一个互斥锁。当请求互斥体时,首先检查有问题的对象。如果它已经标记了一个互斥锁,则阻止该互斥锁。如果不是,则为该对象分配一个互斥锁并发出信号,将互斥锁从池中取出。一旦互斥体未发出信号,清除插槽并将互斥体返回到池中。

    【讨论】:

    • 听起来可以解决问题。由于对象具有唯一标识符,互斥池有一种方法来识别对象。我正在尝试。谢谢!
    • 这不是把问题提升了一个层次吗?现在我们需要对象上的互斥锁,以确保将互斥锁分配给对象是安全的。
    • 不,您只需要一个互斥锁来保护您的互斥锁池(对象 ID 到正在使用的互斥锁的映射)。
    • @vividos。您将需要一个互斥锁来保护您的互斥锁池,但只要有一个可用的互斥锁,从池中取出一个互斥锁的操作应该相对较快。如果 OP 的目的是对需要锁定一段时间的项目执行长时间操作,那么您只想将这种“长期持有”锁定应用于一个对象。
    • 这也适用于关键部分(添加以便搜索关键部分显示这一点)。
    【解决方案2】:

    在不知不觉中,您正在寻找的是软件事务内存 (STM)。

    STM 系统在内部使用所需的锁进行管理,以确保 ACI 属性(原子、​​一致、隔离)。这是一项研究活动。你可以找到很多STM库;特别是我正在研究Boost.STM(该库尚未进行beta测试,文档也不是最新的,但您可以使用)。还有一些编译器正在引入 TM(如 Intel、IBM 和 SUN 编译器)。您可以从here 获取规范草案

    这个想法是识别关键区域如下

    transaction {
      // transactional block
    }
    

    并让 STM 系统在确保 ACI 属性的情况下使用所需的锁进行管理。

    Boost.STM 方法让您可以编写类似

    的内容
    int inc_and_ret(stm::object<int>& i) {
      BOOST_STM_TRANSACTION {
        return ++i;
      } BOOST_STM_END_TRANSACTION 
    }
    

    您可以将这对 BOOST_STM_TRANSACTION/BOOST_STM_END_TRANSACTION 视为确定作用域隐式锁的一种方式。

    这种伪透明的代价是每个 stm::object 需要 4 个元数据字节。

    即使这与您的初始设计相去甚远,我真的认为这是您的目标和初始设计背后的原因。

    【讨论】:

    • Boost.STM 听起来像是一个有趣的库。我要去看看它。谢谢!
    • 请注意,分支 vbe 比树干更新。 :)
    • @vividos Boost.STM 能满足您的需求吗?
    • 我还没有试用这个库。我假设所有应该通过来自两个事务的访问来保护的对象都应该存储为 stm::object 某处。我认为STM也可以用来解决我的问题。
    • @Vicente 我看过 Boost.STM 和各种研究文章,因为我自己对这个问题很感兴趣,即允许锁定单个项目而无需为每个项目创建互斥锁的容器一。每个项目上的事务应该是“悲观的”,但你应该被允许在集合的其他部分进行并发事务。我没有看到任何关于你的类的文档以及它如何应用于这个特定的模型。
    【解决方案3】:

    我怀疑是否有任何干净的方法可以完成您的设计。将互斥锁分配给对象的问题看起来会修改对象的内容 - 因此您需要一个互斥锁来保护对象免受多个线程试图同时为其分配互斥锁,因此要保留您的第一个互斥锁分配安全,你需要另一个互斥锁来保护第一个。

    就我个人而言,我认为您要解决的问题可能一开始就不是问题。在我花很多时间试图修复它之前,我会做一些测试,看看你会失去什么(如果有的话),只需在每个对象中包含一个 Mutex 并完成它。我怀疑你是否需要走得更远。

    如果您需要做的不仅仅是我想拥有一个线程安全的对象池,并且任何时候线程想要对一个对象进行操作,它都必须从该池中获得所有权。获取所有权的调用将释放当前由请求线程拥有的任何对象(以避免死锁),然后赋予它所请求对象的所有权(如果该对象当前由另一个线程拥有,则阻塞)。对象池管理器可能会自己在一个线程中运行,自动序列化对池管理的所有访问,因此池管理代码可以避免锁定对变量的访问,告诉它谁当前拥有什么对象等等。

    【讨论】:

    • 您可以使用 boost::once 将互斥锁设置为对象。成本是 once_flag 变量,您可以在随后将互斥锁释放回池时将其重置为 init。那里的解决方案可能涉及一些“全局”锁定和解锁,但我认为目的是他可能希望在很长一段时间内保持对对象的锁定,同时允许系统的其余部分可供使用。跨度>
    【解决方案4】:

    就个人而言,这就是我会做的。你有许多对象,都可能有某种键,比如名字。因此,请使用以下人员姓名列表:

     Bill Clinton
     Bill Cosby 
     John Doe
     Abraham Lincoln 
     Jon  Stewart 
    

    因此,现在您将创建多个列表:例如,每个字母一个。比尔和比尔会单独进入一个名单,约翰、乔恩亚伯拉罕。

    每个列表都将被分配给一个特定的线程——访问必须通过那个线程(你必须将一个对象的操作编组到那个线程上——很好地使用函子)。那么你只有两个地方可以锁定:

     thread() { 
          loop { 
             scoped_lock lock(list.mutex); 
             list.objectAccess(); 
          }
     } 
    
     list_add() { 
           scoped_lock lock(list.mutex); 
           list.add(..); 
     } 
    

    将锁定保持在最低限度,如果您仍在进行大量锁定,您可以优化对列表中的对象执行的迭代次数,从 1 到 5,以最大限度地减少获取所花费的时间锁。如果您的数据集增长或按数字键控,您可以执行任意数量的隔离数据以将锁定保持在最低限度。

    【讨论】:

      【解决方案5】:

      在我看来,您需要一个工作队列。如果工作队列上的锁成为瓶颈,您可以切换它,以便每个线程都有自己的工作队列,然后某种调度程序会将传入对象提供给工作量最少的线程。下一个级别是工作窃取,其中已用完工作的线程查看其他线程的工作队列。(请参阅英特尔的线程构建块库。)

      【讨论】:

      • 我已经有一个每个线程的工作队列,问题是关于在处理对象时锁定对象。
      【解决方案6】:

      如果我没听错的话......

      struct table_entry {
          void *   pObject;     // substitute with your object
          sem_t    sem;         // init to empty
          int      nPenders;    // init to zero
      };
      
      struct table_entry *  table;
      
      object_lock (void * pObject) {
          goto label;                   // yes it is an evil goto
      
          do {
              pEntry->nPenders++;
              unlock (mutex);
              sem_wait (sem);
      label:
              lock (mutex);
              found = search (table, pObject, &pEntry);
          } while (found);
      
          add_object_to_table (table, pObject);
          unlock (mutex);
      }
      
      object_unlock (void * pObject) {
          lock (mutex);
          pEntry = remove (table, pObject);   // assuming it is in the table
          if (nPenders != 0) {
              nPenders--;
              sem_post (pEntry->sem);
          }
          unlock (mutex);
      }
      

      上面的方法应该可行,但它确实有一些潜在的缺点,例如...

      1. 搜索中可能存在瓶颈。
      2. 线程饥饿。无法保证任何给定线程都会退出 object_lock() 中的 do-while 循环。

      但是,根据您的设置,这些潜在的缺点可能并不重要。

      希望这会有所帮助。

      【讨论】:

      • 这似乎实现了 John Dibling 描述的互斥池背后的想法。感谢您分享这个想法!
      【解决方案7】:

      我们在这里对类似的模型感兴趣。我们考虑的一个解决方案是拥有一个全局(或共享)锁,但使用方式如下:

      • 可以在对象上自动设置的标志。如果您设置了标志,那么您就拥有该对象。
      • 您执行您的操作,然后重置变量并发送信号(广播)一个条件变量。
      • 如果获取失败,则等待条件变量。当它被广播时,您检查它的状态以查看它是否可用。

      看起来我们每次更改这个变量的值时都需要锁定互斥锁。所以有很多锁定和解锁,但您不需要长时间保持锁定。

      使用“共享”锁,您可以将一个锁应用于多个项目。您将使用某种“哈希”函数来确定哪个互斥体/条件变量适用于该特定条目。

      【讨论】:

      • 在等待条件变量的多个线程之间不会存在竞争吗?还是只唤醒了一个等待线程?
      【解决方案8】:

      在@JohnDibling 的帖子下回答以下问题。

      您实施了这个解决方案吗?我有一个类似的问题,我想知道你是如何解决将互斥锁释放回池的。我的意思是,你怎么知道,当你释放互斥体时,如果你不知道另一个线程是否持有它,它可以安全地放回队列中?

      @LeonardoBernardini


      我目前正在尝试解决同样的问题。我的方法是使用计数器字段和真正的资源互斥体字段创建自己的互斥体结构(称为 counterMutex)。因此,每次尝试锁定 counterMutex 时,首先增加计数器,然后锁定底层互斥锁。完成后,减少 coutner 并解锁互斥锁,然后检查计数器是否为零,这意味着没有其他线程正在尝试获取 lock 。如果是这样,请将 counterMutex 放回池中。操作计数器时是否存在竞争条件?你可能会问。答案是不。请记住,您有一个全局互斥锁,以确保一次只有一个线程可以访问 coutnerMutex。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-04-11
        • 1970-01-01
        • 1970-01-01
        • 2016-10-04
        • 1970-01-01
        • 2016-11-24
        • 1970-01-01
        • 2012-06-13
        相关资源
        最近更新 更多