线程同步问题:可能的竞争,滥用volatile,缓存一致性?

时间:2013-01-28 19:38:54

标签: c synchronization pthreads

我熟悉 pthreads 编程;以下代码是一个简单的生产者/消费者设计,其中数据是从全局列表中提取/获取的。问题是:尝试消费者函数中的数据指针被释放两次。此外,如果我在循环开始时添加printf(),一切似乎都没问题......我做错了什么?我怀疑是否滥用volatile关键字,或缓存隐藏的内容......除非它只是一个设计问题(可能是:p)。

感谢您的见解。

注意:malloc()/free()在我的系统上是线程安全的。我正在使用$ gcc -pthread -O0进行编译,我认为这应该减少因误用volatile而导致的设计错误。最后,我们不关心此代码片段内存不足(如果生产者多于消费者)。

修改:将代码更改为单个列表头。

#include <pthread.h>
#include <stdlib.h>
#include <string.h>

pthread_mutex_t lock;
pthread_cond_t new_data;

struct data {
  int i;
  struct data *next;
};

struct data *list_head = NULL;

void *consume(void *arg)
{
  struct data *data;

  while (1) {
    pthread_mutex_lock(&lock);
    while (list_head == NULL) {
      pthread_cond_wait(&new_data, &lock);
    }
    data = list_head;
    list_head = list_head->next;
    pthread_mutex_unlock(&lock);
    free(data);
  }

  return NULL;
}

void *produce(void *arg)
{
  struct data *data;

  while (1) {
    data = malloc(sizeof(struct data));
    pthread_mutex_lock(&lock);
    data->next = list_head;
    list_head = data;
    pthread_mutex_unlock(&lock);
    pthread_cond_signal(&new_data);
  }

  return NULL;
}

int main()
{
  pthread_t tid[2];
  int i;

  pthread_mutex_init(&lock, NULL);
  pthread_cond_init(&new_data, NULL);
  pthread_create(&tid[0], NULL, consume, NULL);
  pthread_create(&tid[1], NULL, produce, NULL);
  for (i = 0; i < 2; i++) {
    pthread_join(tid[i], NULL);
  }
}

输出:

$ ./a.out 
*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x00007f5870109000 ***

4 个答案:

答案 0 :(得分:4)

考虑以下情况:

  1. 产生 - &gt;获得锁
  2. 消费 - &gt;等待锁
  3. 产生 - &gt;分配d0,write_ptr = d0,read_ptr = d0,信号,解锁
  4. 消费 - &gt;获得锁
  5. 产生 - &gt;等待锁
  6. 消费 - &gt;满意的条件
  7. 消费 - &gt; data = d0,read_ptr = NULL,解锁
  8. 消费 - &gt;免费d0
  9. 产生 - &gt;获得锁定,分配d1
  10. 消费 - &gt;等待锁
  11. 产生 - &gt; write_ptr != null所以write_ptr->next = d1
  12. 产生 - &gt; read_ptr == null所以read_ptr = d1
  13. 检查步骤11. write_ptr仍然是d0,即使它是独立于生产者线程的。您需要确保consume不会使write_ptr无效。

    双重链接列表可以让你避免一些这些困难(因为读者和作者从不同的角度工作)。

    主:

    • 创建定点节点HEADTAIL,链接HEADTAIL
    • Spawn producer
    • 产生消费者

    制片:

    • 锁定
    • 创建节点
    • HEAD->next->prev链接到node
    • node->next链接到HEAD->next
    • HEAD->next链接到node
    • node->prev链接到HEAD
    • 解锁
    • 信号

    消费者:

    • 锁定
    • 等待TAIL->prev != HEADdo { pthread_cond_wait } while (condition);
    • data = TAIL->prev
    • TAIL->prev链接到data->prev
    • TAIL->prev->next链接到TAIL
    • 解锁
    • 使用data

答案 1 :(得分:4)

我相信您的问题在以下几行:

if (NULL != write_ptr)
  write_ptr->next = data;
write_ptr = data;
if (NULL == read_ptr)
  read_ptr = data;

我认为您没有正确构建列表。事实上,我不明白为什么你有两个列表。但无论如何......

我假设您要将新数据添加到列表的开头。否则你需要一个尾指针,或者每次都需要追逐到列表的末尾。

要执行此操作,您需要将当前列表头添加为数据的下一项。它应该看起来像:

data->next = write_ptr;
write_ptr = data;

不需要对write_ptr进行NULL检查。

答案 2 :(得分:1)

更新:正如Billy ONeal指出的那样,pthread函数提供了所需的内存屏障,因此只要受到pthread锁的保护,就没有必要声明任何volatile。 (有关详细信息,请参阅此问题:Does guarding a variable with a pthread mutex guarantee it's also not cached?

但是我对一些奇怪的行为得到了另一个解释:生成器创建的链表被破坏:假设write_ptr不是NULL并观察行为:

/* 1 */ if (NULL != write_ptr)
/* 2 */   write_ptr->next = data;
/* 3 */ write_ptr = data;

假设write_ptr指向先前分配的实例A. A.next为NULL。我们新分配一个实例B并将所有内容设置为NULL(因此:B.next为NULL)。

第1行计算为true,因此执行第2行。 A.next现在指向B. 执行第3行后,write_ptr指向B B.next是NULL =&gt; A丢失了导致内存泄漏的原因。

但是现在我看不出为什么libc抱怨双重免费。

答案 3 :(得分:1)

错误在行

if (NULL != write_ptr)
   write_ptr->next = data;
write_ptr = data;

这应该是:     if(NULL!= write_ptr)        data-&gt; next = write_ptr;     write_ptr = data;

出于调试目的,还要确保从队列中获取预期值。

此外,不需要volatile个变量。由于您的队列受互斥锁保护,操作系统将确保队列访问是原子的。仅在访问内存映射的硬件资源时才需要volatile,并且永远不应将其用于同步。它所做的就是不必要地将数据强制存储在内存中。

还有一个问题。如果您的生产者比您的消费者更快,那么除非限制队列的大小并强制生产者等待消费者,否则最终会耗尽内存。较小的队列响应速度较快,寻找更大队列的唯一理由是消除生产者的扰动。