Boost Threads - Гонки - PullRequest
       2

Boost Threads - Гонки

1 голос
/ 31 января 2011

Я играю с некоторыми потоками Boost, и я написал следующий код:

#include <boost/thread.hpp>
#include <iostream>

volatile int threads = 0;

const int iterations = 200;

volatile char output[iterations*4];
volatile int k = 0;

void w() {
    threads++;
    for (int i = 0; i < iterations; i++) {
        k++;
        output[k] = '0';
    }
}
void y() {
    threads++;
    for (int i = 0; i < iterations; i++) {
        k++;
        output[k] = '1';
    }
}

int main(int argc, char* argv[]) {
    boost::thread a(&w);
    boost::thread b(&w);
    boost::thread c(&y);
    boost::thread d(&y);

    while (k <= iterations * 4) {}

    for (int i=0; i < k; i++) {
        std::cout << output[i];
    }
    return 0;
}

Чего я не понимаю, так это того, почему завершение этих потоков стало ждать друг друга, это показано в выходных данных:

000000000000000000000000000000000000000011111111

У меня сложилось впечатление, что выходные данные будут в случайном порядке, а операторы будут выглядеть примерно так (ожидаемый результат):

00100000111001000000010001111100000000011111111

Вместо них следуют все 0, затем все 1 (или наоборот).

Почему это так?На данный момент кажется, что потоки расположены в случайном порядке, но все еще объединены;т.е. ждем друг друга перед выполнением.Я действительно ожидал, что код потоков ++, возможно, будет запускаться одновременно в некоторых потоках, оставляя их с некоторым значением thisthread.

Отказ от ответственности: я очень новичок в Boost: потоки, просто играя, я понимаю расуусловия очень опасны (могут привести к неопределенному поведению), я просто кладу руку в огонь, чтобы я мог это понять.

Ответы [ 2 ]

4 голосов
/ 31 января 2011

Здесь происходит несколько вещей, которые мешают вам увидеть ожидаемое вами поведение переключения контекста:

  1. Нити в основном сидят в узких петлях. Маловероятно, что переключение контекста произойдет в середине.
  2. В вашем примере было странное переключение контекста, когда w(2) печатает 6, а затем 47. Переключается с 3 на 2 и обратно.
  3. Доступ к глобальной переменной, вероятно, оптимизирован компилятором C ++. Создание переменной volatile может помочь. Обратите внимание, что volatile на самом деле не является решением. Например, приращения могут не быть атомарной операцией (иначе, выборкой, приращением, сохранением), а также кэши ЦП могут иногда вызывать странные артефакты, потому что хранилища в памяти не видны всем процессорам во время их выполнения.
  4. Вы не используете endl. Обычно я думаю, что endl - абсолютно ужасная идея. Но в этом случае это означает, что все записи буферизируются, что может вызвать некоторые интересные артефакты.
  5. Кроме того, cout будет поточно-ориентированным, и блокировки, необходимые для этого, могут также влиять на то, как контексты потоков переключаются друг с другом.

Ряд этих проблем можно решить, вызвав системный вызов write напрямую. Это заставит системный вызов, который может дать возможность другому потоку работать. Это также заставит вывод произойти именно тогда, когда будет выполнен код для него.

Вот программа, которая будет демонстрировать поведение, которое вы искали на многоядерном Unix-компьютере. Он также покажет вам окончательное значение k, чтобы продемонстрировать, почему volatile на самом деле не является большим решением, и как он создал гонку на значение k. Первоначально я просто предложил это как хак, чтобы заставить вашу программу работать немного лучше для создания поведения, которое вы искали, потому что в вашей оригинальной программе это не имело значения:

#include <boost/thread.hpp>
#include <iostream>
#include <unistd.h>

volatile int threads = 0;

const int iterations = 200;

volatile int k = 0;

void w(char thid) {
    threads++;
    for (int i = 0; i < iterations; i++) {
        k++;
        ::write(1, &thid, 1);
    }
}

int main(int argc, char* argv[]) {
    boost::thread a(&w, '0');
    boost::thread b(&w, '1');
    boost::thread c(&w, '2');
    boost::thread d(&w, '3');

    a.join();
    b.join();
    c.join();
    d.join();
    ::write(1, "\n", 1);
    // In general, do not mix write to stdout and cout. In this case
    // it will work, but this is a very limited and specific case.
    ::std::cout << "k == " << k << "\n";
    return 0;
}
2 голосов
/ 31 января 2011

Основная причина наблюдения за последовательным заполнением выходного буфера в том, что вы завершаете потоки слишком рано.200 итераций недостаточно для прерывания рабочих потоков.Вместо вывода на печать я добавил это к вашему main():

int switches = 0;
for (int i=1; i<iterations*4; ++i)
    if (output[i] != output[i-1])
        ++switches;
std::cout << "Switches = " << switches << "\n";

и начал увеличивать iterations.Действительно, в какой-то момент (около 10 000 000 на моем компьютере) переключатели потоков стали очевидными.Это сразу выявило еще одну проблему с вашей программой: приращения k не являются атомарными.Вы объявили k как volatile, что означает, что компилятор не будет кэшировать свое значение в регистре при повторной проверке, как вы делаете это в пустом цикле в main.Однако, volatile совсем не гарантирует атомных приращений.На практике это означает, что когда происходит фактическое переключение потоков, k++ не всегда увеличивается, и цикл while (k<iterations*4) {} никогда не завершается.Чтобы увеличить k атомарно, вам нужна еще одна внешняя библиотека - , вот обсуждение темы на SO .Если вы оказались в Windows, может быть проще использовать InterlockedIncrement напрямую.Если вы внесете эти изменения в свою программу, вы увидите ожидаемый результат.


Существует также небольшое переполнение буфера вывода.Поскольку вы увеличиваете k перед записью в вывод, вы записываете в индексы от 1 до итераций * 4, включая.Вам нужно либо инициализировать k в -1, либо добавить еще один элемент в массив

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...