Потокобезопасность передачи параметра по ссылке

Рейтинг: 2Ответов: 1Опубликовано: 06.02.2015

Здравствуйте.

Интересует, является ли потокобезопасной передача параметра по ссылке. Например, есть такой код:

void func(SomeClass & v)
{
    // ... что-то делаем с v
}

Возможна ли такая ситуация, что объект, переданный в функцию func по ссылке (v), каким-либо образом выйдет из своей области видимости (в другом потоке) и будет уничтожен, а в данной функции func ссылка на этот объект станет недействительной? Я такой ситуации себе представить не могу, но, может быть, такое возможно?

Думаю, вопрос можно перефразировать так: а возможно ли, вообще, передать ссылку на объект, созданную в одном потоке, в другой поток?

По мотивам программы от VladD'a накропал такую программку для эксперимента:

class A
{
public:
    int m_a;
    A() {
        m_a = 10;
    }
    void print(string s) {
        cerr << "A::print(): " << s.c_str() << "A::m_a == " << m_a << "\n";
    }
    ~A() {
        m_a = 50;
        cerr << "Объект A уничтожен\n";
    }
};

class Container
{
public:
    A& ref;
    Container(A& v) : ref(v) {}
    ~Container() {
        cerr << "Объект Container уничтожен\n";
    }
};

void ThreadProc(Container* c)
{
    cerr << "Мы в порожденном потоке, щас воспользуемся ссылкой";
    sleep(5);
    // пользуемся, и ... не вылетаем, однако!:
    c->ref.print("воспользовались: ");
    c->ref.m_a = 100;
    c->ref.print("еще раз воспользовались: ");
}

int main (int argc, char *argv[])
{
    {
        A a;
        Container c(a);
        thread t (ThreadProc, &c);
        t.detach();
        cerr << "Порожденный поток отделен.\n";
    }
    sleep(10);
}

Вывод программы:

Порожденный поток отделен.
Объект Container уничтожен
Объект A уничтожен
Мы в порожденном потоке, щас воспользуемся ссылкой
A::print(): воспользовались: A::m_a == 50
A::print(): еще раз воспользовались: A::m_a == 100

И никаких exception. Т.е. получается, что объект по ссылке передать в другой поток возможно. И даже, когда этот объект уничтожается (выходит из области видимости - выполняется его деструктор) в основном потоке, то другой поток все равно может им как-то пользоваться. Как это возможно? Вопросов становится все больше.

Ответы

▲ 2

Ещё как да. C++ — не memory safe, в нём легко выстрелить себе в ногу^W^W^W^W закодировать undefined behaviour.

Пример:

SomeClass& ProduceDeadReference()
{
    SomeClass sc;
    return sc;
}

...
SomeClass& v = ProduceDeadReference();
// тут ссылка уже мёртвая
std::thread t(ThreadProc, v);

void ThreadProc(SomeClass& v)
{
    // а тут тем более
}

Классическое дополнительное чтение по теме: Can a local variable's memory be accessed outside its scope? (очень рекомендую).


Уточнение по коду. Вот этот код компилируется:

#include <thread>

class SomeClass {};

SomeClass& ProduceDeadReference()
{
    SomeClass sc;
    SomeClass* p = &sc; // подавим warning заодно
    return *p;
}

class Container
{
    SomeClass& ref;
public:
    Container(SomeClass& v) : ref(v) {}
};

void ThreadProc(Container* c)
{
    // пользуемся, вылетаем
}

int main()
{
    Container c(ProduceDeadReference());
    std::thread t(ThreadProc, &c);
    return 0;
}

По поводу того, что ваш код не вылетает. Память, оставшаяся после мёртвого объекта, может в любой момент быть выделена другому объекту. Ссылка фактически ссылается именно на эту память. Таким образом, работать с объектом по невалидной ссылке — всё равно что стрелять с закрытыми глазами, «на кого бог пошлёт». Можете попасть в стенку, а можете и в человека.