我有一个非常简单的队列实现,它包装了一个固定的数组。它包含peek,enqueue和dequeue。如果peek返回一个引用,我发现它最终将返回冲突的结果(冲突的结果意味着它将返回2个不同的值而没有任何插入的出列或排队)。显然,如果该引用被保留并修改,可能会发生这种情况,但据我所知,事实并非如此。事实上,再次调用peek会得到预期的结果。
以下是使用Windows线程和互斥锁的代码。我也在Linux上使用pthreads尝试了相同的结果。我显然不明白的东西......我已经转储了可执行文件并找到了返回引用之间的唯一区别,或者是在取消引用内存位置时的唯一区别。例如:
如果返回引用,则peek包含:
lea eax,[edx+ecx*4+8]
然后在消费者线程中:
cmp dword ptr [eax],1
但是,如果返回一个值,则peek包含:
mov eax,dword ptr [edx+ecx*4+8]
然后在消费者线程中:
cmp eax,1
谢谢!
#include <iostream>
#include <windows.h>
typedef void *(thread_func_type)(void *);
void start_thread(HANDLE &thread, thread_func_type *thread_func, void *arg)
{
DWORD id;
thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread_func, arg, 0, &id);
if (thread == NULL) {
std::cerr << "ERROR: failed to create thread\n";
::exit(1);
}
}
void join_thread(HANDLE &thread)
{
WaitForSingleObject(thread, INFINITE);
}
class ScopedMutex
{
HANDLE &mutex;
public:
ScopedMutex(HANDLE &mutex_) : mutex(mutex_)
{
WORD result = WaitForSingleObject(mutex, INFINITE);
if (result != WAIT_OBJECT_0) {
std::cerr << "ERROR: failed to lock mutex\n";
::exit(1);
}
};
~ScopedMutex()
{
ReleaseMutex(mutex);
};
};
template <typename T, unsigned depth>
class Queue
{
unsigned head, tail;
bool full;
T data[depth];
HANDLE mutex;
public:
Queue() : head(0), tail(0), full(false)
{
mutex = CreateMutex(NULL, 0, NULL);
if (mutex == NULL) {
std::cerr << "ERROR: could not create mutex.\n";
::exit(1);
}
};
T &peek()
{
while (true) {
{
ScopedMutex local_lock(mutex);
if (full || (head != tail))
return data[tail];
}
Sleep(0);
}
};
void enqueue(const T &t)
{
while (true) {
{
ScopedMutex local_lock(mutex);
if (!full) {
data[head++] = t;
head %= depth;
full = (head == tail);
return;
}
}
Sleep(0);
}
};
void dequeue()
{
while (true) {
{
ScopedMutex local_lock(mutex);
if (full || (head != tail)) {
++tail;
tail %= depth;
full = false;
return;
}
}
Sleep(0);
}
};
};
template <unsigned num_vals, int val, unsigned depth>
void *
producer(void *arg)
{
Queue<int, depth> &queue = *static_cast<Queue<int, depth> *>(arg);
for (unsigned i = 0; i < num_vals; ++i) {
queue.enqueue(val);
}
std::cerr << "producer " << val << " exiting.\n";
return NULL;
}
template <unsigned num_vals, int val, unsigned depth>
void *
consumer(void *arg)
{
Queue<int, depth> &queue = *static_cast<Queue<int, depth> *>(arg);
for (unsigned i = 0; i < num_vals; ++i) {
while (queue.peek() != val)
Sleep(0);
if (queue.peek() != val) {
std::cerr << "ERROR: (" << val << ", " << queue.peek() << ")" << std::endl;
std::cerr << "But peeking again gives the right value " << queue.peek() << std::endl;
::exit(1);
}
queue.dequeue();
}
return NULL;
}
int
main(int argc, char *argv[])
{
const unsigned depth = 10;
const unsigned num_vals = 100000;
Queue<int, depth> queue;
HANDLE p1, p2, c1, c2;
start_thread(p1, producer<num_vals, 1, depth>, &queue);
start_thread(p2, producer<num_vals, 2, depth>, &queue);
start_thread(c1, consumer<num_vals, 1, depth>, &queue);
start_thread(c2, consumer<num_vals, 2, depth>, &queue);
join_thread(p1);
join_thread(p2);
join_thread(c1);
join_thread(c2);
}
答案 0 :(得分:3)
Peek将引用返回到数组的中间,而其他线程正在主动修改该内存。从该引用中读取任何属性将是未定义的行为。你不能偷看里面,你的出队应该删除元素并返回副本。
答案 1 :(得分:2)
也许就是这样:
队列已满。由于一些怪癖的调度,生产者#2连续两次运行,因此队列中接下来的两个插槽包含这些值:2,2
peek()
。它返回对第一个槽的引用。但是在lea
指令和cmp
指令之间的时间内:dequeue()
。这释放了消费者#1的peek()
刚刚返回的插槽,允许生产者线程继续。第二个值2现在位于队列的头部。生产者线程#1现在用值1覆盖第一个槽。因为队列是循环的,所以第一个槽现在是队列的尾部。
这两个插槽现在包含以下值:1,2
回到消费者话题#1中,cmp
指令发生,您看到所需的值并退出while循环
peek()
,并看到错误的值。
当peek()
返回副本时,您没有看到此竞争条件,因为消费者#1在检索值时持有互斥锁。当peek()
返回引用时,您正在检索该值而不保留Mutex,因此受CPU指令调度程序和OS的线程调度程序的支配。
答案 2 :(得分:0)
您的主题可能轮流轮流,这会让您的消费者每次都能使用正确的号码。
相反,让一个消费者抓住对数字的引用,并将其与以下副本进行比较:
int& valRef(queue.peek());
int valCopy = valRef;
while( valRef == valCopy){}
printf("Aha! It IS unsafe!\n");
最终,其中一个制作人将在您进行比较时覆盖您引用的内存。