代码之家  ›  专栏  ›  技术社区  ›  vividos

如何同步访问多个对象

  •  10
  • vividos  · 技术社区  · 16 年前

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

    现在,我想节省一些资源,并且具有可伸缩性,因为可能有数千个对象,而且仍然只有一手线程。我在考虑一个类设计,线程具有某种互斥对象或锁对象,并在应该访问对象时将锁分配给对象。这将节省资源,因为我只有和线程一样多的锁对象。

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

    我的第一个想法是每个线程有一个boost::mutex对象,每个对象都有一个boost::shared_ptr,它最初是未设置的(或为空)。现在,当我想要访问这个对象时,我通过创建一个作用域锁对象来锁定它,并将它分配给共享的指针。当已设置共享指针时,我将等待当前的锁。这个想法听起来像一堆种族条件,所以我有点放弃了它。还有其他方法来完成这个设计吗?完全不同的方式?

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

    8 回复  |  直到 10 年前
        1
  •  4
  •   John Dibling    16 年前

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

        2
  •  3
  •   Vicente Botet Escriba    16 年前

    在不知道的情况下,您正在寻找的是软件事务性内存(STM)。

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

    其目的是确定以下关键区域

    transaction {
      // transactional block
    }
    

    并让STM系统使用所需的锁进行管理,以确保ACI属性。

    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::对象,这种伪透明的代价是4个元数据字节。

    即使这离你的初始设计还很远,我真的认为这是你的目标和初始设计背后的原因。

        3
  •  1
  •   Jerry Coffin    16 年前

    我怀疑有任何干净的方法来完成你的设计。将互斥体分配给对象的问题看起来会修改对象的内容——所以您需要一个互斥体来保护对象,使其免受多个线程同时向其分配互斥体的影响,因此为了保证第一个互斥体分配的安全,您需要另一个互斥体来保护第一个互斥体一个。

    就我个人而言,我认为你试图解决的问题一开始可能不是问题。在我花很多时间试图修复它之前,我会做一些测试,看看在每个对象中简单地包含一个互斥体并完成它会给您带来什么损失(如果有的话)。我怀疑你还需要做更多的事。

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

        4
  •  1
  •   Chris K    16 年前

    就我个人而言,这就是我要做的。你有很多对象,都可能有某种类型的键,比如说名字。下面列出了人们的名字:

     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
  •  1
  •   stonemetal    16 年前

    我觉得你需要一个工作队列。如果工作队列上的锁变成了瓶颈,您可以切换它,使每个线程都有自己的工作队列,那么某种调度程序会将传入的对象以最少的工作量提供给线程。下一个更高级别是工作窃取,在窃取过程中,已用完工作的线程会查看其他线程的工作队列(请参阅Intel的线程构建块库)。

        6
  •  0
  •   Sparky    16 年前

    如果我正确地跟随你……

    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. 线头饥饿。不能保证任何给定的线程都将退出对象_Lock()中的do-while循环。

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

    希望这有帮助。

        7
  •  0
  •   CashCow    15 年前

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

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

    不过,每次更改该变量的值时,我们都需要锁定互斥体。所以有很多锁和解锁,但你不需要长期保持锁。

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

        8
  •  0
  •   wenjun.yan    10 年前

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

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

    作者:@leonardobernardini


    我目前正试图解决同样的问题。我的方法是创建您自己的mutex结构(称为countermutex),使用counter字段和real resource mutex字段。因此,每次尝试锁定counter mutex时,首先递增计数器,然后锁定底层mutex。当你完成了它,你减少coutner和解锁mutex,然后检查计数器,看看它是否为零,这意味着没有其他线程试图获得锁。如果是这样,将countermutex放回池中。操纵柜台时是否有比赛条件?你可以问。答案是否定的。记住您有一个全局互斥体,以确保一次只有一个线程可以访问coutnermutex。