如何正确销毁 pthread 互斥锁

Posted

技术标签:

【中文标题】如何正确销毁 pthread 互斥锁【英文标题】:How to correctly destroy pthread mutex 【发布时间】:2013-06-14 17:11:03 【问题描述】:

我如何才能销毁 pthread 互斥变量?

这就是我想要做的。 我想缓存对象(结构变量),通过键查找。 我想在这里有最小的锁粒度。所以我想为每个人都有一把锁 对象可能嵌入在结构中,以便我可以进行对象级锁定。

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

我想从缓存中释放对象。 现在如何正确销毁/释放互斥锁? pthread_mutex_destroy 文档说我们不应该在互斥锁被锁定时使用 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 显示那些线程正在等待锁。

【问题讨论】:

在您的示例中,所有线程都尝试从缓存中进行独占 fetch-use-destroy,而第一个计划线程阻塞所有其他线程。这是您希望缓存在实际应用程序中的行为方式吗?如果两个线程尝试从缓存中获取相同的对象,一个会被阻塞,直到……另一个销毁该对象? @pilcrow 不,在实际应用中它不会那样工作。真正的用途将是查找对象并获得该对象的读或写锁并使用和释放锁。我只想测试一个线程已经用完缓存空间的部分,它需要销毁一些对象,以便将一些其他对象带入缓存。这是我有疑问的场景,所以只是模拟了这个场景 sleep(2) 是为了确保其他线程有足够的时间等待锁定,以便在其他线程等待时模拟销毁。 【参考方案1】:

您遇到的基本问题是从缓存中删除对象需要在缓存级别而不是对象级别进行同步。

实现这一点的一种方法是为整个缓存设置一个全局锁,该锁仅在查找期间持有,并在获取对象锁后删除。此锁可以是读写锁,仅当线程要删除对象时才为写入而持有。因此,希望使用缓存对象的线程会这样做:

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() 函数无法返回它。

【讨论】:

我认为这会奏效。我确实想到了这一点,现在不记得为什么我认为它不起作用。一段时间后会接受答案。【参考方案2】:

在这点上我完全同意 caf 的看法。我们在某些实现中做了类似的事情(例如,参考ifMIB.c 中的 ifData_createReference 和 ifData_removeReference 例程)。基本思想是保持一个全局锁来保护整个对象列表和一个对象级锁来保护列表中的各个条目。

当我们必须在列表中创建一个新条目时,对列表进行 WRITE 锁定并添加一个新条目,以便将该条目一致地添加到列表中的所有用户。并释放列表锁。

当我们必须从列表中查找/访问一个条目时,在列表上获取一个 READ 锁并搜索该条目。一旦我们找到条目,在 READ 模式下获取对象锁定以进行只读操作/在 WRITE 模式下获取对象锁定以修改对象条目。现在,释放列表锁。现在,一旦我们完成了对象条目的处理,也释放对象锁。

当必须从列表中删除对象条目时,在列表上获取一个 WRITE 锁。在列表中搜索并找到对象条目。对对象条目进行 WRITE 锁定,这将确保您是该对象的唯一当前用户。现在从列表中删除该条目,因为没有人可以在列表中再搜索它。并立即释放对象锁。然后,释放列表锁。现在销毁对象并释放对象资源。

【讨论】:

【参考方案3】:

(a) 尝试销毁锁定的互斥锁或 (b) 引用已销毁的互斥锁而不是调用 pthread_mutex_init 重新创建它是未定义的行为(请参阅 documentation)。这意味着销毁共享互斥锁的线程将与锁定它的其他线程竞争,并且(1)首先发生销毁,其他线程调用未定义的行为试图锁定,因为(b)或(2)锁定在另一个线程中由于(a),首先发生并且销毁线程会调用未定义的行为。

您需要更改您的设计,以使处于活动争用状态的互斥锁永远不会被破坏。对于您的示例,您可以在加入所有线程后销毁main 中的共享互斥锁。对于您描述的程序,您可能需要在对象中插入引用计数。

【讨论】:

是的,我知道问题的原因。在 main 中销毁它不是一种选择。正如我所提到的,这是用于**缓存**,因此新对象将被添加到缓存中,而旧对象需要被驱逐。实际上,如果您永远无法安全地销毁互斥锁​​,那么在什么条件下我可以使用该 api 的其他可能用途。我想到了需要再次保护的引用计数,这意味着一个锁需要 2 个锁,原子变量可以提供帮助。这似乎太复杂了,可能这是唯一的选择。【参考方案4】:

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

好吧,我希望你的意图是正确的,我遇到了完全相同的问题。无论如何,后来我意识到我很愚蠢:抱怨pthread_mutex_* 函数之后 pthread_mutex_destroy() 的未定义行为就像在访问free() 之后的指针时抱怨SEGFAULTS

大多数 C 程序都是围绕这样的范式建模的,即每个程序都必须确保在某种破坏后不访问内存。好的 C 程序的设计会防止指针到处散布,因此破坏只发生在定义明确的地方,当没有其他变量不再包含指针时。这在垃圾收集语言中根本不是问题。

解决方案 1:像内存分配一样使用引用计数。引用计数器是通过原子函数访问的。 (使用 glib,它包含很棒的便携的东西)

解决方案 1b:像在内存分配中那样使用引用计数,将重要的工作进程与不重要的工作进程分开,并在后面使用弱引用,这样它们就不会阻止对象破坏。

解决方案 2:不要破坏互斥体。为什么要为节省 RAM 而烦恼?只需制作一个包含 128k 个对象的全局静态数组即可。添加一个结构成员,它指示对象的状态。 而不是破坏只是原子比较并设置状态变量,并在访问处于“禁用”状态的对象的线程中打印错误。

解决方案 3 - 困难的方法:不要进行共享内存并发。结合系统上与 CPU 数量相匹配的线程池,使用非阻塞 IO、消息对象和状态机设计。为每个任务建立消息队列,并让任务仅通过在另一个队列中排队的消息进行通信。将队列放在包含套接字/文件描述符的同一“选择”或“轮询”集中。要在状态机之间混洗大数据(3d 游戏),请使用具有原子引用计数器的结构并在写入时复制语义。 在大多数情况下,这将是最高效、稳定和可维护的解决方案。

如果您所做的事情与性能有关,请三思而后行使用原子操作。它们可能比互斥锁更昂贵。

【讨论】:

以上是关于如何正确销毁 pthread 互斥锁的主要内容,如果未能解决你的问题,请参考以下文章

互斥锁的示例

线程同步

pthread锁之pthread_mutexattr_t

多线程的互斥锁的运用

linux中的互斥锁几行代码备份

如何在c中正确同步多线程与互斥锁?