如何/何时在无等待算法中释放内存

时间:2014-03-08 01:38:49

标签: multithreading algorithm asynchronous data-structures

我在找出无等待算法设计的关键点时遇到了麻烦。假设一个数据结构有一个指向另一个数据结构的指针(例如链表,树等),如何才能正确地释放数据结构?

问题在于,有一些单独的操作无法在没有锁的情况下以原子方式执行。例如,一个线程读取指向某个内存的指针,并增加该内存的使用计数以防止此线程使用数据时可能需要很长时间,即使它没有,也就是竞争条件。什么阻止另一个线程读取指针,减少使用计数并确定它不再使用并在第一个线程递增使用计数之前释放它?

主要问题是当前的CPU只有一个单词CAS(比较和交换)。或者问题是,我对无等待算法和数据结构一无所知,在阅读了一些论文后,我仍然没有看到光。

恕我直言垃圾收集不能成为答案,因为如果任何单个线程在一个原子块内,它将要么阻止GC运行(这意味着无法保证GC将运行再次)或问题只是推送到GC,在这种情况下,请解释GC如何判断数据是否处于愚蠢状态(指针被读取[例如存储在局部变量]但是使用计数没有增加)。

PS,欢迎参考有关蠢货等待算法的高级教程。

编辑:你应该假设问题是用非托管语言解决的,比如C或C ++。毕竟如果它是Java,我们就不必担心释放内存了。进一步假设编译器可以生成代码,该代码将在使用计数器递增之前存储对寄存器中的对象的临时引用(对于其他线程不可见),并且可以在加载对象地址和递增计数器之间中断线程。这当然并不意味着解决方案必须限于C或C ++,而是解决方案应该提供一组允许在链接数据结构上实现无等待算法的原语。我对基元以及如何解决设计无等待算法的问题感兴趣。有了这样的原语,可以在C ++和Java中同样很好地实现无等待算法。

3 个答案:

答案 0 :(得分:1)

经过一些研究,我学到了这一点。

解决这个问题并非易事,而且有几个解决方案各有优缺点。复杂性的原因来自CPU间同步问题。如果做得不对,它可能看起来在99.9%的时间内正常工作,这是不够的,或者它可能在负载下失败。

我找到的三个解决方案是1)危险指针,2)基于静止期的回收(由RCU实现中的Linux内核使用)3)引用计数技术。 4)其他5)组合

危险指针通过将当前活动的引用保存在众所周知的每个线程位置来工作,因此任何决定释放内存的线程(当计数器看起来为零时)都可以检查内存是否仍被任何人使用。一个有趣的改进是缓冲请求释放小阵列中的内存并在阵列满时批量释放它们。使用危险指针的优点是它实际上可以保证未回收内存的上限。缺点是它给读者带来了额外的负担。

基于静默期的回收通过延迟内存的实际释放来工作,直到知道每个线程都有机会完成可能需要释放的任何数据。知道满足此条件的方法是在删除对象后检查每个线程是否通过静止期(不在临界区)。在Linux内核中,这意味着每个任务都会进行自愿任务切换。在用户空间应用程序中,它将是关键部分的结束。这可以通过一个简单的计数器实现,每次计数器均匀时线程不在临界区(读取共享数据),每次计数器为奇数时,线程在临界区内,从临界区移动或返回所有线程需要做的是以原子方式递增数字。基于此,“垃圾收集器”可以确定每个线程是否有机会完成。有几种方法,一种简单的方法是将请求排队到空闲内存(例如在链表或数组中),每种方法都有当前一代(由GC管理),当GC运行时它检查状态线程(它们的状态计数器)看看是否每个都传递给下一代(它们的计数器高于上一次或者是相同甚至),任何内存都可以在被释放后回收一代。这种方法的优点是可以减轻读取线程的负担。缺点是它无法保证等待释放的内存的上限(例如,一个线程在关键部分花费5分钟,而数据不断变化并且内存未被释放),但实际上它可以解决好的。

有许多引用计数解决方案,其中许多需要双重比较和交换,有些CPU不支持,所以不能依赖。然而,关键问题仍然存在,在更新计数器之前需要参考。我没有找到足够的信息来解释如何简单可靠地完成这项工作。所以......

当然有许多“其他”解决方案,这是一个非常重要的研究课题,有大量的论文。我没有检查所有这些。我只需要一个。

当然可以组合各种方法,例如危险指针可以解决引用计数的问题。但是有几乎无限的组合,并且在某些情况下,自旋锁在理论上可能会破坏等待自由,但在实践中不会损害性能。有点像我在研究中发现的另一个小问题,理论上不可能使用比较和交换来实现无等待算法,这是因为理论上(纯理论上)基于CAS的更新可能会因非确定性过度时间而失败(想象一百万个核心上的一百万个线程,每个核心试图使用CAS增加和减少相同的计数器。实际上它很少会失败几次(我怀疑这是因为CPU比CAS有更多的时钟远离CAS,但我认为如果算法每隔50个时钟在相同的位置返回相同的CAS,那么64核可能有一个重大问题的可能性,然后再次,谁知道,我没有一百个核心机器试试这个)。我的研究的另一个结果是设计和实现无等待算法和数据结构是非常具有挑战性的(即使一些繁重的工作外包,例如到垃圾收集器[例如Java]),并且可能表现不如类似的算法与精心放置的锁。

所以,是的,即使没有延误也可以释放记忆。这很棘手。如果你忘记让正确的操作成为原子,或者放置正确的记忆障碍,哦,好吧,你是敬酒。 :-)感谢大家的参与。

答案 1 :(得分:0)

我知道这不是最好的方式,但它对我有用:

对于共享动态数据结构列表,我使用每个项目的使用计数器

  • 例如:

    struct _data
     {
     DWORD usage;
     bool  delete;
     // here add your data
     _data() { usage=0; deleted=true; }
     };
    const int MAX = 1024; 
    _data data[MAX];
    
  • 现在当项目开始在somwhere使用时

    // start use of data[i]
    data[i].cnt++;
    
  • 之后不再使用

    // stop use of data[i]
    data[i].cnt--;
    
  • 如果要添加新项目到列表

    // add item
    for (i=0;i<MAX;i++) // find first deleted item
     if (data[i].deleted)
      {
      data[i].deleted=false; 
      data[i].cnt=0;
      // copy/set your data
      break;
      }
    
  • 现在偶尔会出现在背景中(在计时器或其他任何时候)

  • 扫描数据[]一个所有未删除的项目,其中cnt == 0设置为已删除(如果有任何动态内存,则释放其动态内存)

[注]

  • 避免多线程访问问题实现每个数据列表的单个全局锁定
  • 并对其进行编程,以便在任何数据[i] .cnt正在更改时无法扫描数据
  • 如果您不想使用操作系统锁,
  • 一个bool和一个DWORD就足够了

    // globals
    bool data_cnt_locked=false;
    DWORD data_cnt=0;
    
  • 现在任何数据更改[i] .cnt修改如下:

    // start use of data[i]
    while (data_cnt_locked) Sleep(1);
    data_cnt++;
    data[i].cnt++;
    data_cnt--;
    
  • 并像这样修改删除扫描

    while (data_cnt) Sleep(1);
    data_cnt_locked=true;
    Sleep(1);
    if (data_cnt==0) // just to be sure
    for (i=0;i<MAX;i++) // here scan for items to delete ...
     if (!data[i].cnt)
      if (!data[i].deleted)
      {
      data[i].deleted=true; 
      data[i].cnt=0;
      // release your dynamic data ...
      }
    data_cnt_locked=false;
    

PS。

  • 不要忘记稍微满足您的需求,以适应您的需求
  • 无锁算法睡眠时间有时取决于OS任务/调度程序
  • 这不是一个真正无锁的实现
  • 因为当GC工作时,所有人都被锁定
  • 但是,如果多次访问没有阻止彼此
  • 所以,如果你不经常运行GC就可以了

答案 2 :(得分:0)

我认为增量/减量和比较和交换的原子操作可以解决这个问题。

点子:

  • 所有资源都有一个用原子操作修改的计数器。计数器最初为零。

  • 在使用资源之前:通过原子递增其计数器来“获取”它。当且仅当递增的值大于零时,才能使用该资源。

  • 使用资源后:通过原子递减计数器“释放”它。当且仅当递减的值等于零时,才应处置/释放资源。

  • 在处理之前:原子地将计数器值与最小(负)值进行比较和交换。如果并发线程“获取”其间的资源,则不会发生处理。

您尚未为问题指定语言。这是c#中的一个例子:

class MyResource
{
    // Counter is initially zero. Resource will not be disposed until it has
    // been acquired and released.
    private int _counter;

    public bool Acquire()
    {
        // Atomically increment counter.
        int c = Interlocked.Increment(ref _counter);

        // Resource is available if the resulting value is greater than zero.
        return c > 0;
    }

    public bool Release()
    {
        // Atomically decrement counter.
        int c = Interlocked.Decrement(ref _counter);

        // We should never reach a negative value
        Debug.Assert(c >= 0, "Resource was released without being acquired");

        // Dispose when we reach zero
        if (c == 0)
        {
            // Mark as disposed by setting counter its minimum value.
            // Only do this if the counter remain at zero. Atomic compare-and-swap operation.
            if (Interlocked.CompareExchange(ref _counter, int.MinValue, c) == c)
            {
                // TODO: Run dispose code (free stuff)
                return true; // tell caller that resource is disposed
            }
        }

        return false; // released but still in use
    }
}

用法:

// "r" is an instance of MyResource

bool acquired = false;

try
{
    if (acquired = r.Acquire())
    {
        // TODO: Use resource
    }
}
finally
{
    if (acquired)
    {
        if (r.Release())
        {
            // Resource was disposed.
            // TODO: Nullify variable or similar to let GC collect it.
        }
    }
}