如果peek返回引用,即使从未使用引用(使用程序集!),线程安全队列也不是线程安全的

时间:2010-02-22 23:18:58

标签: c++ multithreading

我有一个非常简单的队列实现,它包装了一个固定的数组。它包含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);
}

3 个答案:

答案 0 :(得分:3)

Peek将引用返回到数组的中间,而其他线程正在主动修改该内存。从该引用中读取任何属性将是未定义的行为。你不能偷看里面,你的出队应该删除元素并返回副本。

答案 1 :(得分:2)

也许就是这样:

队列已满。由于一些怪癖的调度,生产者#2连续两次运行,因此队列中接下来的两个插槽包含这些值:2,2

  1. 消费者线程#1处于其自旋循环中,刚刚调用peek()。它返回对第一个槽的引用。但是在lea指令和cmp指令之间的时间内:
  2. 消费者话题#2调用dequeue()。这释放了消费者#1的peek()刚刚返回的插槽,允许生产者线程继续。第二个值2现在位于队列的头部。
  3. 生产者线程#1现在用值1覆盖第一个槽。因为队列是循环的,所以第一个槽现在是队列的尾部。

    这两个插槽现在包含以下值:1,2

  4. 回到消费者话题#1中,cmp指令发生,您看到所需的值并退出while循环

  5. 消费者#1再次调用peek(),并看到错误的值。
  6. peek()返回副本时,您没有看到此竞争条件,因为消费者#1在检索值时持有互斥锁。当peek()返回引用时,您正在检索该值而不保留Mutex,因此受CPU指令调度程序和OS的线程调度程序的支配。

答案 2 :(得分:0)

您的主题可能轮流轮流,这会让您的消费者每次都能使用正确的号码。

相反,让一个消费者抓住对数字的引用,并将其与以下副本进行比较:

int& valRef(queue.peek());
int valCopy = valRef;
while( valRef == valCopy){}
printf("Aha! It IS unsafe!\n");

最终,其中一个制作人将在您进行比较时覆盖您引用的内存。