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

时间:2012-05-11 07:00:38

标签: multithreading pthreads mutex

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

  

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

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

2 个答案:

答案 0 :(得分:2)

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

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

如果使用递归互斥锁,则只需将一个函数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()(公共)和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

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

可以轻松地将公共/私人功能融入其中的语言可以很好地封装细节。你班级的用户对你如何在你的班级中做事情没有任何知识(或需要知识),他们只是称之为公共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

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

答案 1 :(得分:1)

除了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);
}