我熟悉 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 ***
答案 0 :(得分:4)
考虑以下情况:
write_ptr != null
所以write_ptr->next = d1
read_ptr == null
所以read_ptr = d1
检查步骤11. write_ptr
仍然是d0,即使它是独立于生产者线程的。您需要确保consume
不会使write_ptr
无效。
双重链接列表可以让你避免一些这些困难(因为读者和作者从不同的角度工作)。
主:
HEAD
和TAIL
,链接HEAD
和TAIL
制片:
HEAD->next->prev
链接到node
node->next
链接到HEAD->next
HEAD->next
链接到node
node->prev
链接到HEAD
消费者:
TAIL->prev != HEAD
(do { 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
,并且永远不应将其用于同步。它所做的就是不必要地将数据强制存储在内存中。
还有一个问题。如果您的生产者比您的消费者更快,那么除非限制队列的大小并强制生产者等待消费者,否则最终会耗尽内存。较小的队列响应速度较快,寻找更大队列的唯一理由是消除生产者的扰动。