如何正确销毁pthread互斥锁

9

如何精确地销毁pthread mutex变量?

这是我想做的事情。 我想要缓存对象(结构体变量),这些对象由键查找。 我希望在此处拥有尽可能小的锁颗粒度。因此,我想要每个对象一个锁,可能嵌入在结构中,以便我可以进行对象级别的锁定。

现在问题是如何安全地销毁这些对象? 看起来第一步是从查找表中删除对象,以使对象在未来不可访问,这很好。

我想从缓存中释放对象。 现在如何正确销毁/释放mutex? pthread_mutex_destroy文档说,我们不应在mutex被锁定时使用pthread_mutex_destroy。假设一个线程决定销毁对象,则需要销毁锁,以释放锁并执行pthread_mutex_destroy。其他正在等待对象锁的线程会发生什么?

以下是用于模拟上述情况的代码,请注意,我使用sleep(2)来放大竞争的影响。


#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

typedef struct exampleObj {
   pthread_mutex_t mutex;
   int key;
   int value1;
   int value2;
}exampleObj;

exampleObj sharedObj = {PTHREAD_MUTEX_INITIALIZER,0,0,0};

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 

exampleObj* Lookup(int key) {
   return &sharedObj;
}

void* thrFunc(void* id) {
   int i = (*((int*)id));
   char errBuf[1024];
   exampleObj * obj = Lookup(0);

   if (pthread_mutex_lock(&obj->mutex)) {
      printf("Locking failed %d \n",i);
      return NULL;
   }
   // Do something
   printf("My id %d will do some work for 2 seconds.\n",i);
   sleep(2);
   pthread_mutex_unlock(&obj->mutex);
   int errNum = pthread_mutex_destroy(&obj->mutex);
   strerror_r(errNum,errBuf,1024);
   printf("Destroying mutex from thread %d : %s\n ",errNum,errBuf);
   return NULL;
}

int main() {
   pthread_t thrds[10];
   int i;
   int args[10];
   char errBuf[1024];
   int errNum = 1;

   for (i=0;i<10;i++){
      args[i] = i;
      pthread_create(&thrds[i],NULL,thrFunc,args+i);
   }

   for (i=0;i<10;i++){
      pthread_join(thrds[i],NULL);
   }
   return 0;
}

多个线程成功销毁了互斥锁。剩余的线程永久挂起。

Gdb显示这些线程正在等待锁。


在您的示例中,所有线程都尝试从缓存中进行独占式的获取-使用-销毁操作,第一个被调度的线程会阻塞其他所有线程。这是您希望缓存在实际应用中表现出来的行为吗?如果两个线程尝试从缓存中获取相同的对象,其中一个线程会被阻塞直到...另一个线程销毁该对象? - pilcrow
@pilcrow 在实际应用中,它不会像那样工作。真正的用法将是查找对象并获取该对象的读取或写入锁,并使用和释放该锁。我想测试的只是一个线程例如已经用完了缓存空间,它需要销毁一些对象以便将其他对象带入缓存。这就是我有疑问的情况,所以只是模拟了这种情况,sleep(2)是为了确保有足够的时间让其他线程等待锁,以便在其他线程等待时模拟销毁。 - Sridhar V
4个回答

4
您面临的基本问题是,从缓存中删除对象需要在缓存级别而不是对象级别进行同步。
一种实现方式是,在整个缓存中拥有全局锁,仅在查找期间保持该锁,并在获取对象锁后释放该锁。如果线程要删除对象,则此锁可以是读写锁,仅用于写入。因此,希望使用缓存对象的线程将执行以下操作:
pthread_rwlock_rdlock(&cache_lock);
exampleObj * obj = Lookup(key);
pthread_mutex_lock(&obj->mutex);
pthread_rwlock_unlock(&cache_lock);

/* Do some work on obj */

pthread_mutex_unlock(&obj->mutex);

如果一个线程希望销毁缓存对象,那么它应该执行以下操作:

pthread_rwlock_wrlock(&cache_lock);
exampleObj * obj = Lookup(key);
pthread_mutex_lock(&obj->mutex);
Remove(key);
pthread_rwlock_unlock(&cache_lock);

/* Do some cleanup work on obj */
pthread_mutex_unlock(&obj->mutex);
pthread_mutex_destroy(&obj->mutex);

(其中Remove()函数从缓存中删除该函数,使得后续的Lookup()函数无法返回它。)

我认为这会起作用。我曾经想到过这个,现在却不记得为什么认为它不会起作用。一段时间后会接受答案。 - Sridhar V

1
我完全同意caf的观点。我们在某些实现中做了类似的事情(例如,参见ifMIB.c中的ifData_createReference和ifData_removeReference例程)。基本思想是保持全局锁来保护整个对象列表和对象级锁来保护列表中的单个条目。
当我们需要在列表中创建新条目时,请在列表上获取写锁并添加新条目,以便将条目一致地添加到列表的所有用户中。然后释放列表锁。
当我们需要查找/访问列表中的条目时,请在列表上获取读锁并搜索条目。一旦找到条目,请以读模式获取对象锁以进行只读操作/以写模式获取对象锁以修改对象条目。现在释放列表锁。现在,一旦我们完成了对象条目的处理,请释放对象锁。
当需要从列表中删除对象条目时,请在列表上获取写锁。在列表中搜索并找到对象条目。获取对象条目的写锁,这将确保您是该对象的唯一当前用户。现在从列表中删除该条目,因为没有人可以再在列表中搜索它了。立即释放对象锁。然后释放列表锁。现在销毁对象并释放对象资源。

0
我想从缓存中释放对象。现在如何正确地销毁/释放互斥锁?pthread_mutex_destroy文档说,我们不应该在互斥锁被锁定时使用pthread_mutex_destroy。假设一个线程决定销毁对象,它需要销毁锁以释放锁并执行pthread_mutex_destroy。那些等待对象锁的其他线程会发生什么?
嗯,我希望我理解了你的意图,我也遇到了完全相同的问题。无论如何,后来我意识到我很愚蠢:抱怨pthread_mutex_*函数在pthread_mutex_destroy()之后的未定义行为就像抱怨在free()之后访问指针时出现SEGFAULTS
大多数C程序都是围绕着这样一种范式建模的:每个程序必须确保在某种破坏之后不再访问内存。好的C程序将具有防止指针传播到各处的设计,因此只有在不再包含指针的其他变量的情况下,在明确定义的位置进行破坏。这在垃圾收集语言中根本不是问题。
解决方案1:像内存分配一样使用引用计数。引用计数器通过原子函数访问。(使用glib,它包含了很棒的、可移植的东西)
解决方案1b:使用引用计数,就像内存分配一样,将重要的工作人员与不重要的工作人员分开,并在后者中使用弱引用,以便它们不会阻止对象的销毁。
解决方案2:不要销毁互斥锁。何必费心节省RAM呢?只需要创建一个全局静态数组,大约128k个对象。添加一个结构体成员,指示对象的状态。不是销毁,只需原子比较和设置状态变量,并在访问“DISABLED”状态对象的线程中打印错误。
解决方案3-困难的方法:不要进行共享内存并发操作。将线程池与系统上的CPU数量匹配,使用非阻塞IO,消息对象和状态机设计相结合。为每个任务创建消息队列,让任务仅通过在其他任务队列中排队的消息通信。将队列放入包含套接字/文件描述符的“select”或“pollfd”集合中。要在状态机之间移动大数据(3D游戏),请使用具有原子引用计数器和写时复制语义的结构体。在大多数情况下,这将是最高效,最稳定和最可维护的解决方案。
如果您所做的事涉及性能,请三思而后行,不要使用原子操作。它们可能比互斥锁更昂贵。

0

试图销毁已锁定的互斥量或引用已销毁的互斥量(除了调用 pthread_mutex_init 重新创建它之外)都是未定义行为(请参见文档)。这意味着销毁共享互斥量的线程将与其他锁定它的线程竞争,要么(1)销毁先发生,其他线程因(b)而尝试锁定导致未定义行为,要么(2)在另一个线程中锁定先发生,销毁线程因(a)而导致未定义行为。

您需要更改设计,以便正在争用的互斥量永远不会被销毁。对于您的示例,您可以在所有线程加入后在main中销毁共享互斥量。对于您描述的程序,您可能需要在对象中插入引用计数。


是的,我意识到这些问题,这也是我提出问题的原因。在主函数中摧毁它不是一个选项。正如我所提到的,这是为了 缓存,所以新对象将被添加到缓存中,旧对象需要被清除。实际上,如果你永远不能安全地销毁互斥量,那么在什么其他情况下我会能够使用它呢?我想到了引用计数,但这需要受保护,这意味着需要两个锁来锁定,原子变量可以帮助解决这个问题。但似乎过于复杂,也许这是唯一的选择。 - Sridhar V

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接