为什么我们想在具有互斥锁的情况下使函数递归?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么我们想在具有互斥锁的情况下使函数递归?相关的知识,希望对你有一定的参考价值。

https://stackoverflow.com/a/5524120/462608

如果要递归调用函数(锁定相同的互斥锁),则它们必须使用一个递归互斥锁,或者 必须一次又一次地解锁和锁定相同的非递归互斥锁(谨防并发线程!),或 必须以某种方式注释他们已经锁定的互斥锁(模拟递归所有权/互斥锁)。

在任何情况下,这都是一个“明智的”设计决策,使函数递归已经有一个互斥锁吗?

答案

例如,当您具有递归调用它的函数,并且您希望获得对它的同步访问:

void foo() {
   ... mutex_acquire();
   ... foo();
   ... mutex_release();
}

如果没有递归互斥锁,则必须首先创建一个“入口点”函数,当您拥有一组相互递归的函数时,这会变得很麻烦。没有递归互斥:

void foo_entry() {
   mutex_acquire(); foo(); mutex_release(); }

void foo() { ... foo(); ... }
另一答案

递归和非递归互斥锁具有不同的用例。没有互斥类型可以轻松替换另一个。非递归互斥体具有较少的开销,并且递归互斥体在某些情况下具有有用或甚至需要的语义,并且在其他情况下具有危险甚至破坏的语义。在大多数情况下,有人可以使用递归互斥体替换任何策略,使用基于非递归互斥体的不同更安全和更有效的策略。

  • 如果您只想排除其他线程使用您的互斥锁保护资源,那么您可以使用任何互斥锁类型,但由于其较小的开销,可能希望使用非递归互斥锁。
  • 如果你想以递归方式调用函数,它们锁定相同的互斥锁,那么它们也是 必须使用一个递归互斥,或 必须一次又一次地解锁并锁定相同的非递归互斥锁(注意并发线程!)(假设这在语义上是合理的,它仍然可能是性能问题),或者 必须以某种方式注释他们已经锁定的互斥锁(模拟递归所有权/互斥锁)。
  • 如果要从一组此类对象中锁定多个受互斥锁保护的对象,可以通过合并构建这些对象,则可以选择 使用每个对象恰好一个互斥,允许更多的线程并行工作,或 使用每个对象对任何可能共享的递归互斥锁的引用,以降低未能将所有互斥锁锁定在一起的可能性,或者 为每个对象使用一个与任何可能共享的非递归互斥锁相当的引用,绕过多次锁定的意图。
  • 如果要在与锁定不同的线程中释放锁,则必须使用非递归锁(或明确允许此而不是抛出异常的递归锁)。
  • 如果要使用同步变量,则需要能够在等待任何同步变量时显式解锁互斥锁,以便允许在其他线程中使用该资源。对于非递归互斥锁,这是非常可能的,因为递归互斥锁可能已被当前函数的调用者锁定。
另一答案

我今天遇到了递归互斥锁的需要,我认为这可能是到目前为止发布的答案中最简单的例子:这是一个暴露两个API函数的类,Process(...)和reset()。

public void Process(...)
{
  acquire_mutex(mMutex);
  // Heavy processing
  ...
  reset();
  ...
  release_mutex(mMutex);
}

public void reset()
{
  acquire_mutex(mMutex);
  // Reset
  ...
  release_mutex(mMutex);
}

这两个函数不能同时运行,因为它们修改了类的内部,所以我想使用互斥锁。问题是,Process()在内部调用reset(),并且它会产生死锁,因为已经获得了mMutex。使用递归锁定它们可以解决问题。

另一答案

如果您想查看使用递归互斥锁的代码示例,请查看Linux / Unix的“Electric Fence”的源代码。 '在Valgrind出现之前,Twas是一种常见的Unix工具,用于查找“边界检查”读/写溢出和欠载以及使用已释放的内存。

只需使用源代码编译和链接电栅栏(选项-g with gcc / g ++),然后使用链接选项-lefence将其链接到您的软件,然后开始逐步调用malloc / free。 http://elinux.org/Electric_Fence

另一答案

如果线程阻止尝试获取(已再次)已拥有的互斥锁,那肯定会有问题......

是否有理由不允许同一个线程多次获取互斥锁?

另一答案

除了paxdiablo的exmaple使用实际的递归函数之外,不要忘记递归地使用互斥锁并不一定意味着所涉及的函数是递归的。我发现用于递归互斥体来处理你需要在某些数据结构方面需要原子的复杂操作的情况,这些复杂的操作依赖于更基本的操作,因为基本操作仍然需要使用互斥锁也可以单独使用。示例可能类似于以下内容(请注意,代码仅是说明性的 - 它不使用在处理帐户和日志时可能真正需要的正确错误处理或事务技术):

struct account
{
    mutex mux;

    int balance;

    // other important stuff...

    FILE* transaction_log;
};

void write_timestamp( FILE*);


// "fundamental" operation to write to transaction log
void log_info( struct account* account, char* logmsg)
{
    mutex_acquire( &account->mux);

    write_timestamp( account->transaction_log);
    fputs( logmsg, account->transaction_log);

    mutex_release( &account->mux);
}


// "composed" operation that uses the fundamental operation.
//  This relies on the mutex being recursive
void update_balance( struct account* account, int amount)
{
    mutex_acquire( &account->mux);

    int new_balance = account->balance + amount;

    char msg[MAX_MSG_LEN];
    snprintf( msg, sizeof(msg), "update_balance: %d, %d, %d", account->balance, amount, new_balance);

    // the following call will acquire the mutex recursively
    log_info( account, msg);

    account->balance = new_balance;

    mutex_release( &account->mux);
}

在没有递归互斥锁的情况下执行或多或少等效的操作意味着代码需要注意不要重新获取互斥锁(如果它已经持有它)。一种选择是向数据结构添加某种标志(或线程ID)以指示互斥锁是否已被保留。在这种情况下,你实际上是在实现递归互斥体 - 这是一个比起初可能看起来更难的工作。另一种方法是传递一个标志,表明你已经获得了互斥体作为参数(更容易实现和正确),或者只是有更多的基本操作,假设已经获取了互斥锁,并从更高级别的函数中调用那些关于获取互斥锁的责任:

// "fundamental" operation to write to transaction log
//  this version assumes that the lock is already held
static
void log_info_nolock( struct account* account, char* log msg)
{
    write_timestamp( account->transaction_log);
    fputs( logmsg, account->transaction_log);
}


// "public" version of the log_info() function that
//      acquires the  mutex
void log_info( struct account* account, char* logmsg)
{
    mutex_acquire( &account->mux);
    log_info_nolock( account, logmsg);    
    mutex_release( &account->mux);
}


// "composed operation that uses the fundamental operation
//      since this function acquires the mutex, it much call the
//      "nolock" version of the log_info() function
void update_balance( int amount)
{
    mutex_acquire( &account->mux);

    int new_balance = account->balance + amount;

    char msg[MAX_MSG_LEN];
    snprintf( msg, sizeof(msg), "update_balance: %d, %d, %d", account->balance, amount, new_balance);

    // the following call assumes the lock is already acquired
    log_info_nolock( account, msg);

    account->balance = new_balance;

    mutex_release( &account->mux);
}
另一答案

嗯,一种可能性是你正在使用的资源自然适用于递归算法。

考虑搜索二叉树,同时防止其他人使用互斥锁使用(尤其是修改)树。

如果使用递归互斥锁,则只需将一个函数search()传递给根节点即可。然后它按照正常的二叉树搜索递归调用自己,但它在该函数中做的第一件事是锁定互斥锁(虽然这看起来像Python,这只是因为Python是伪代码的理想基础):

def search (haystack, mutex, needle):
    lock mutex recursively

    if haystack == NULL:
        unlock mutex and return NULL

    if haystack.payload == needle:
        unlock mutex and return haystack

    if haystack.payload > needle:
        found = search (haystack.left, mutex, needle)
    else:
        found = search (haystack.right, mutex, needle)

    unlock mutex and return found

另一种方法是将互斥锁和搜索分成两个独立的函数,如search()(public)和search_while_locked()(很可能是私有的):

def private search_while_locked (haystack, needle):
    if haystack == NULL:
        return NULL

    if haystack.payload == needle:
        return haystack

    if haystack.payload > needle:
        return search_while_locked (haystack.left, needle)

    return search_while_locked (haystack.right, needle)

def search (haystack, mutex, needle):
    lock mutex non-recursively

    found = search_while_locked (haystack.right, needle)

    unlock mutex and return found

虽然这种方式打败了递归解决方案的优雅,但实际上我更喜欢它,因为它减少了需要完成的工作量(无论工作量多么小,它仍然有效)。

而且很容易将自己放到公共/私有函数中的语言可以很好地封装细节。您的班级用户对您在班级中的工作方式不了解(或需要知识),他们只需要调用公共API。

但是,您自己的功能可以访问所有非公共内容,并且可以完全了解某些操作需要使用哪些锁。


另一种可能性与此非常相关,但没有递归。

想想您可能希望用户对您的数据执行的任何有用操作,这需要在此期间没有其他人使用它。到目前为止,您只有非递归互斥锁的经典案例。例如,清除队列中的所有条目:

def clearQueue():
    lock mutex
    while myQueue.first <> null:
        myQueue.pop()
    unlock mutex

现在让我们说你发现它非常有用,并且想要从你的析构函数中调用它,它已经锁定了互斥锁:

def destructor():
    lock mutex
    clearQueue()
    doSomethingElseNeedingLock()
    unlock mutex

显然,使用非递归互斥锁,在析构函数调用后,它将锁定clearQueue的第一行,这可能是您需要递归互斥锁的一个原因。

您可以使用上述提供锁定公共功能和非锁定私有功能的方法:

def clearQueueLocked():
    while myQueue.first <> null:
        myQueue.pop()

def clearQueue():
    lock mutex
    clearQueueLocked():
    unlock mutex

def destructor():
    lock mutex
    clearQueueLocked():
    doSomethingElseNeedingLock()
    unlock mutex and return

但是,如果有大量的这些公共/私人功能对,它可能会有点混乱。

以上是关于为什么我们想在具有互斥锁的情况下使函数递归?的主要内容,如果未能解决你的问题,请参考以下文章

悲观锁和乐观锁

ReactiveSwift源码解析 Atomic的代码实现以及其中的Defer延迟Posix互斥锁递归锁

noexcept 交换和移动具有互斥锁的类

我想在不使用 slug 的情况下使 PHP seo 的博客 URL 变得友好

TencentOS tiny深度源码分析—— 互斥锁

为啥我不能将带有互斥锁的仿函数传递给线程?