Все секреты многопоточности

Disclaimer. Данная работа не повлияла на мои взгляды на программирование, но повлекла достаточно радикальные изменения в подходах к проектированию автоматных объектов.  Теперь, если процесс  автономен и/или не предъявляет требований  к синхронизации, он может использовать другие механизмы параллелизма, не переставая при этом быть автоматным по сути. А почему так случилось, что этому предшествовало  и что это за изменения, вы прочтете далее.

Это не самая большая моя статья (так я думал, начиная ее), над которой я работал, пожалуй, дольше и больше, чем над другими. Но это точно первая моя статья, в которой автоматы и ВКПа не будут главной темой. Тема потоков для меня достаточно необычна, т.к. я совсем не фанат многопоточного программирования. Но,  тем не менее, занимаясь параллельным программированием, время от времени возвращаюсь к теме многопоточности. И вот, чтобы добыть какие-то аргументы для критики и одновременно попробовать найти практическое применение потокам, я решил в этот очередной раз более плотно заняться потоками. А что из этого получилось,   читайте далее.

Создание потоков

На текущий момент создание потока выглядит совсем просто. Но создать — даже не полдела. Это его лишь мизерная часть. Далее следует нетривиальное погружение в детали работы потоков. По большей части потому, что тема достаточно туманная. Ситуация резко усложняется в случае множества потоков. Их необходимо синхронизировать, разруливать тупики, гонки, организовывать обмен данными и т.д. и т.п.

Сети автоматов многие проблемы параллелизма решают «на раз», чего у потоков и близко нет. Они, к сожалению, проблемы только множат. В то же время потоки поддержаны аппаратно и внедрены в языки программирования. Аппаратная поддержка делает их эффективными, а языковая — упрощает применение. Но даже все хорошее, что у них есть, мое отношение к ним не меняет. Для этого просто нет предпосылок. Тем не менее, использовать потоки вполне можно и как это делать применительно к среде ВКПа, что это дает, далее и будет показано. 

Для порождения потока в Qt нужно создать потомок класса QThread и перегрузить его метод run (). Но дальше начинаются проблемы. Одна из них — корректное завершение самих потоков и приложения в целом. Чтобы не иметь подобных проблем, было сделано следующее. Во-первых, введен флаг работы потока, названный далее bIfRun. Его установка запускает цикл работы потока, а сброс инициирует выход из цикла. В том числе, если необходимо срочно прервать работу. Так работа потока взята под контроль.

Еще один флаг — bIfExecuteStep учитывает особенности работы ВКПа. Он позволяет синхронизировать работу потока с автоматным пространством среды ВКПа, породившем поток (напомним, что в данном случае мы имеем дело не просто с автоматом, а с потомком класса потока Qt). Данный флаг устанавливается при вызове метода ExecuteThreadStep () средой ВКПа, а сбрасывается в цикле потока. Так осуществляется внешняя синхронизация потока на каждом дискретном такте среды. Отключает подобную синхронизацию локальная переменная класса bIfLiberty. Данный флаг, будучи установленным, запускает поток на максимальной скорости.

Перегруженный метод WaitForThreadToFinish () автоматного класса  содержит код корректного завершения потока. Этот метод вызывается средой перед удалением автоматного процесса. Так работает механизм корректного завершения потока. Если же класс не автоматный, т.е. не является потомком класса LFsaAppl), то этот код нужно включить в деструктор класса.

Нужно не забыть и про вызов метода start (n), где параметр n задает приоритет потока. Без этого вызова поток просто не будет создан. Но место такого вызова определяется уже в рамках функциональности автоматного процесса.

Все описанное схематично ниже демонстрирует код на С++ (см. листинг 1).

Листинг 1. Схема реализации потока автоматного объекта на С++ в ВКПа

FThCounter::~FThCounter(void)
{
    bIfRun = false;
...
    quit();
    wait();
}

void FThCounter::WaitForThreadToFinish() {
    // завершить поток базового автомата
    bIfRun = false;
    quit();
    wait();     // неограниченное ожидание завершения потока
}

void FThCounter::ExecuteThreadStep() { bIfExecuteStep = true; }

void FThCounter::run() {
...
    while (bIfRun) {
        if (bIfExecuteStep || pVarIfLiberty->GetDataSrc()) {
...
                    if (!bIfRun) break;
                }
            }
        }
    }
    bIfRun = false;
}

Описанного выше в большинстве случаев достаточно, чтобы создать поток и управлять им в среде ВКПа (да, скорее всего, и в любом другом месте). Однако, если бы только этим все и заканчивалось… Управлять и работать — разные вещи. А с чем нам еще предстоит столкнуться мы сможем узнать, только тестируя потоки. И, пожалуй, это единственный случай в моей практике, когда ВКПа выполняет чисто служебные функции — запуская, удаляя и отображая результаты работы потока/потоков. Но одновременно это и случай, когда, казалось бы, независимые друг от друга вещи дополняют друг друга, существенно умножая свои возможности.

Автоматные классы тестирования потоков

Создадим автоматные классы, которые будут потомками класса потока библиотеки Qt. Первый класс, назовем его генератор, будет с периодичностью устанавливать/сбрасывать переменную булевского типа. Второй, который назовем счетчиком, будет наращивать в цикле  значение целой переменной до заданного значения. Он же поможет нам оценить скоростные характеристики процессов и имитировать одновременный доступ к общему ресурсу.

Значение переменных, которые используют данные классы, будем отображать, используя графические возможности среды ВКПа — ее осциллограф. При этом визуально генератор должен порождать периодический сигнал прямоугольной формы, а счетчик — линейный график изменения переменной, линейность и наклон которого будут отражать качество и скорость работы процесса. Счетчик также будет создавать пул потоков, каждый из потоков которого будет в свою очередь счетчиком. Потоки будут обращаться к переменной, имитирующей общий ресурс. Будет организована возможность использования примитивов синхронизации — мютекса или семафора.

Счетчик, кроме порождения параллельных процессов, представленных пулом потоков, будет создавать собственно автоматный процесс среды ВКПа, а также параллельные ему — процесс управляемый событиями таймера и еще один — собственный поток.  Создание последних, назовем их так, двух типов процессов — таймерного и поточного возможно потому, что автоматные класс наследует подобные возможности, являясь потомком класса потока библиотеки Qt (а на нижнем уровне иерархии наследования — потомком класса QObject).

Код потока генератора представлен на листинге ниже (листинг 2):

Листинг 2. Модель генератора на потоке

void FThread::run() {
    nState = 3; // начальное состояние модели потока
    while(bIfRun)  {
        beg:
        if (bIfExecuteStep || pVarIfLiberty->GetDataSrc()) {
            if (bIfExecuteStep) { bIfExecuteStep = false; }
            switch(nState) {
            case 3: y3(); nState = 4; break;
            case 4: y4(); nState = 3; break;
            }
        }
        // задержка потока (в мксек)
        if (pVarStrSleep->strGetDataSrc().length()>0)
            usleep(QString(pVarStrSleep->strGetDataSrc().c_str()).toInt());
        if (bIfExecuteStep) goto beg;
    }
}
// получить текущее состояние потока
string FThread::FGetState(int nNum) {
    if (bIfRun) {
        switch(nState) {
        case 3: return "t_s3"; break;
        case 4: return "t_s4"; break;
        default: return "error state";
        }
    }
    else return LFsaAppl::FGetState(nNum);
};

Код генератора демонстрирует «автоматную реализацию» функций в потоке. В нее также введено внутренне состояние, которое можно в реальном времени контролировать с помощью перегруженного метода FGetState () автоматного класса.  Скорость потока и, соответственно,  период выходного сигнала генератора  можно регулировать, погружая поток на какое-то время в «сон» с помощью метода usllep (n), параметр которого задает время «сна» в микросекундах (если просто sleep (n), то в миллисекундах).  Весьма желательно — вот он первый добытый и достаточно неожиданный секретик! — иметь возможность вызов данного метода исключать, т.к., запущенный даже с нулевым значением параметра, он весьма заметно влияет на скорость работы потока.

Код потока счетчика представлен в листинге 3. Здесь, а это уже наш секретик, нулевое значение длины строковой переменной, задающей числовое значение параметра «сна», позволяет отключить вызов метода usleep ().

Листинг 3. Модель счетчика на потоке

void FThCounter::run() {
    int n=0;
    pVarTimeThrFSA->SetDataSrc(nullptr, 0.0);
    while (bIfRun) {
        if (bIfExecuteStep || pVarIfLiberty->GetDataSrc()) {
            if (pVarCounterThrFSA) {
                timeThr.start();
                while (nGetDataSrc()) {
                    if (bIfExecuteStep || pVarIfLiberty->GetDataSrc()) {
                        if (bIfExecuteStep) { bIfExecuteStep = false; }
                        int nCntr = pVarCounterThrFSA->GetDataSrc();
                        pVarCounterThrFSA->SetDataSrc(nullptr, ++nCntr);
                        pVarCounterThrFSA->UpdateVariable();
                        if (pVarStrSleepThrFSA->strGetDataSrc().length()>0) {
                            usleep(QString(pVarStrSleepThrFSA->strGetDataSrc().c_str()).toInt());
                        }
                        n++;
                    }
                    if (!bIfRun) break;
                }
            }
        }
        pVarTimeThrFSA->SetDataSrc(nullptr, timeThr.elapsed());
        break;
    }
    bIfRun = false;
}

В соответствии с выбранным режимом работы поток  может быть или синхронизирован со средой ВКПа, или работать в свободном режиме со скоростью, определяемой значением метода usleep (), или с максимальной скоростью, если такой вызов отсутствует.

Несколько отличается код счетчика из пула потоков. Его демонстрирует листинг 4. Отметим одну важную особенность данного класса. Поскольку он не является потомком автоматного класса, то код корректного завершения потока (см. листинг 1) включен в его деструктор.

Листинг 4. Модель счетчика с синхронизаций потоков

void ThCounter::run() {
    QMutex m_mutex;
    int n=0;
    while (npIfSemaphoreLotThreads->GetDataSrc();
        bool bMx = pFThCounter->pIfMutexLotThreads->GetDataSrc();
        if (bSm || bMx) {
            if (bSm) pFThCounter->AddCounterSem();
            else pFThCounter->AddCounterMx();
        }
        else pFThCounter->AddCounter();

        if (!pFThCounter->pIfTimerLotThreads->GetDataSrc()) {
            QString qstr = pFThCounter->pVarStrSleepLotThreads->strGetDataSrc().c_str();
            if (qstr.length()>0) {
                int nSleep = qstr.toInt();
                usleep(nSleep);
            }
        }
        n++;
    }
    pFThCounter->DecrementActive();
}

Поток из пула потоков может синхронизировать работу с другими потоками, используя примитивы синхронизации — мютекс или семафор, или работать в асинхронном режиме. Метод DecrementActive () вызывается при завершении работы потока для уменьшения счетчика активных потоков. Его нулевое значение информирует о завершении работы пула.

Коды методов автоматного класса (ссылка на него есть в каждом потоке), которые изменяют значение счетчика, представлены в листинге 5. Методы — AddCounterMx (), AddCounterSem () и AddCounter () вызываются в зависимости  от выбранного режима синхронизации. При этом, если установлена ссылка на объект общего ресурса, изменение общего счетчика происходит через него. В такой ситуации именно этот объект содержит ссылку на общий ресурс. Когда ссылка на объект общего ресурса не установлена, то общий счетчик принадлежит текущему автоматному объекту и недоступен напрямую другим объектам.

Листинг 5. Методы изменения счетчика в различных режимах тестирования

void FThCounter::DecrementActive() {
    m_mutex.lock();
    pCSetVarThread->nActive--;
    m_mutex.unlock();
}

void FThCounter::AddCounter() {
    if (!pFSharedResource) {
        int nVal = pVarExtrCounter->GetDataSrc();
        pVarExtrCounter->SetDataSrc(nullptr, ++nVal);
    }
    else {
        if(pFSharedResource->pVarExtrCounter) {
            pFSharedResource->AddCounter();
        }
    }
    int nMyCnt = pVarMyCounter->GetDataSrc();
    pVarMyCounter->SetDataSrc(nullptr, ++nMyCnt);
    pVarMyCounter->UpdateVariable();

}

void FThCounter::AddCounterMx() {
    string str = FGetNameVarFSA();
    if (!pFSharedResource) {
        m_mutex.lock();
        int nVal = pVarExtrCounter->GetDataSrc();
        pVarExtrCounter->SetDataSrc(nullptr, ++nVal);
        m_mutex.unlock();
    }
    else {
        if(pFSharedResource->pVarExtrCounter) {
            pFSharedResource->AddCounterMx();
        }
    }
    int nMyCnt = pVarMyCounter->GetDataSrc();
    pVarMyCounter->SetDataSrc(nullptr, ++nMyCnt);
    pVarMyCounter->UpdateVariable();
}

void FThCounter::AddCounterSem() {
    if (!pFSharedResource) {
        int n = m_semaphore.available();
        m_semaphore.acquire(1);
        n = m_semaphore.available();
        int nVal = pVarExtrCounter->GetDataSrc();
        pVarExtrCounter->SetDataSrc(nullptr, ++nVal);
        m_semaphore.release(1);
        n = m_semaphore.available();
    }
    else {
        if(pFSharedResource->pVarExtrCounter) {
            pFSharedResource->AddCounterSem();
        }
    }
    int nMyCnt = pVarMyCounter->GetDataSrc();
    pVarMyCounter->SetDataSrc(nullptr, ++nMyCnt);
    pVarMyCounter->UpdateVariable();
}

Вызов в коде метода UpdateVariable ()   необходим для «мгновенной» фиксации нового значения счетчика.  И это, кстати, один из секретиков среды ВКПа. Без его вызова переменная будет изменяться синхронно дискретному времени среды, которое в общем случае будет существенно больше дискретного времени потока. Только так последующее чтение переменной средствами среды (см. в коде вызов метода GetDataSrc ()), как и ее визуализация в среде,   будет отражать ее истинное значение.

Локальная переменная класса с именем  «nMyCouner» (ссылка — pVarMyCounter) содержит число обращений на изменение счетчика. Она введена, т.к. в общем случае значение общего счетчика, который изменяется множеством потоков, не обязательно может быть равно числу обращений на его изменение  через текущий класс.

Тестирование потоков

Графики на рис. 1 отражают работу пула потоков, порождаемых одним объектом класса FThCounter при максимальном значения счетчика — 1000. Цифры, которыми помечены графики, отражают число потоков в пуле: 1- 1000 потоков, 2 — 2000, 3 — 3000, 4 — 4000, 5 — 5000. Время работы пула потоков до достижения максимального значения общего счетчика можно оценить числом клеток до «полки» (одна клетка — 10 сек). В эксперименте потоки для синхронизации используют мютекс, а их дискретное время — 10 мсек (каждый из потоков вызывает функцию «засыпания» usleep (n), где n = 10000).

Оценим реальную эффективность пулов потоков в сравнении с идеальной. В идеале скорость любого числа параллельных потоков для 1000 циклов при дискретном времени 0.01 сек равна 10-ти секундам. Или, другими словами, она равна времени работы одного потока (ведь, потоки одинаковы и параллельны). Наш тест показывает примерно 11–12 сек. В какой-то мере, это терпимо. Можно предположить, что примерно такое же время будет при числе потоков до 1000. Для 2000 потоков имеем уже 17,7 сек, 3000 потоков — 30.8 сек, 5000 — более 70-ти сек и 10000 потоков сделают работу за 125.486 сек. Таким образом, оптимальный «потолок» потоков, отражающих в разумных пределах идеальную скорость задачи, находится в пределах 1000 потоков. Далее ситуация, с точки зрения скорости работы, конечно, будет только ухудшаться. В конечном счете все будет зависеть от используемой аппаратной конфигурации.

С помощью описанных выше экспериментов мы установили оптимальное число потоков, которое может поддерживать достаточно эффективно та или иная конкретная вычислительная среда (имеется в виду —  процессор, объем памяти, операционная система). Как показали эксперименты, на более мощной конфигурации «идеальное» время стало меньше 11сек (здесь, отметим, больше 11), а «потолок» сдвинулся до 3000 потоков. Вот такой «секрет» — серьезная зависимость многопоточности от мощности вычислительной среды. Хотя секрета тут-то и нет. И достаточно понятно почему.

Рис. 1. Тестирование различного числа потоков

Рис. 1. Тестирование различного числа потоков

Другая картина (см. рис. 2) наглядно отражает не только подмеченную выше тенденцию, но и другие стороны работы потоков. С одной стороны, здесь демонстрируется замедление работы потоков при увеличении их числа, а, с другой стороны, — явная нестабильность их работы. Последнее представлено нелинейностью графиков. Все большие или малые «полочки» на графиках — результат замедления работы потоков в отдельные моменты времени, определяемые текущей загрузкой системы.

Рис. 2. Тесты пулов потоков: 1 - до 1000 потоков, 2-2000 потоков, 3 - 3000, 4 - 5000, 5 - 10000 потоков

Рис. 2. Тесты пулов потоков: 1 — до 1000 потоков, 2–2000 потоков, 3 — 3000, 4 — 5000, 5 — 10000 потоков

Кстати, поскольку современные приложения активно используют графику, то небезынтересно было бы оценить эффективность графической подсистемы, запуская тест с выводом графиков и без него. Можно также оценить работу приложения в фоновом режиме и т.д. и т.п.

В результате такой достаточно простой тест позволяет получить много интересной информации о вычислительной системе в дополнение к той оценке, которую содержит свойства системы. По крайней мере, сравнивая свои две системы,   я теперь имею о них и другую и, как я полагаю,  достаточно объективную оценку. Просто потому, что рассмотренный тест работает в «бульоне», в котором «варюсь» и я, запуская данный тест, просматривая попутно YouTube и занимаясь написанием данной статьи…

Рис. 3. Тест на стабильность работы потоков

Рис. 3. Тест на стабильность работы потоков

Но вернем к статье. Оценим стабильность работы потоков. На рис. 3 показаны результаты тестирования нескольких последовательных экспериментов для пулов потоков из 2000 и 5000 экземпляров. Две тысячи потоков отработали достаточно стабильно. Это можно было бы сказать и про пул из пяти тысяч, если бы не один выпадающий эксперимент со временем почти 129 сек (более чем в два раза больше ожидаемого). Но подобные задержки, как показывает опыт,   могут возникнуть в любой момент и при любом числе потоков. Этим процессом «рулит» система. Можно лишь сказать, что вероятность «тормозов» увеличивается при увеличении числа потоков.  Теперь понятны причины, по которым Windows не относят к системам реального времени: потоки могут подвести в самый неподходящий момент времени. Не потому ли мы не так давно лихо врезались в Луну? ;)

Довольно неожиданной (по аналогии с функцией sleep ())  оказалась и разница в работе с объектами синхронизации: мютексами — QMutex и семафорами — QSemaphore. Рис. 4 демонстрирует результаты для вариантов из 100, 200 и 300 чистых (без вызова usleep ()) потоков и максимальном значении счетчика — 1000. Я настолько впечатлен результатом, что сомневаюсь в правильном использовании семафоров (все же я начинающий «многопоточник»), а потому привожу код функций с мютексом и семафором (листинг 6). Так что, может, пока не будем спешить с объявлением еще одного обнаруженного секрета потоков? Ну, а вдруг?

Рис. 4. Тест объектов синхронизации.

Рис. 4. Тест объектов синхронизации.

Листинг 6. Методы счетчика, использующие объекты синхронизации мютекс и семафор.

void FSharedResource::AddCounterMx() {
    m_mutex.lock();
    int n = pVarExtrCounter->GetDataSrc();
    pVarExtrCounter->SetDataSrc(nullptr, ++n);
    pVarExtrCounter->UpdateVariable();
    if (pVarMyCounter) {
        int nMyCnt = pVarMyCounter->GetDataSrc();
        pVarMyCounter->SetDataSrc(nullptr, ++nMyCnt);
        pVarMyCounter->UpdateVariable();
    }
    m_mutex.unlock();
}

void FSharedResource::AddCounterSem() {
    m_semaphore.acquire(1);
    int nVal = pVarExtrCounter->GetDataSrc();
    pVarExtrCounter->SetDataSrc(nullptr, ++nVal);
    pVarExtrCounter->UpdateVariable();
    if (pVarMyCounter) {
        int nMyCnt = pVarMyCounter->GetDataSrc();
        pVarMyCounter->SetDataSrc(nullptr, ++nMyCnt);
        pVarMyCounter->UpdateVariable();
    }
    m_semaphore.release(1);
}

Рассмотрим теперь, как на работу потоков влияет синхронизация. В контексте этого мне не дает покоя статья [1], в которой для экспериментов взяты десять потоков и единая переменная-счетчик. При этом максимальное число циклов отдельного потока — 1000000. Если убрать синхронизацию, то при таких же исходных данных наш тест выполнит задачу за время примерно равное 1,8 сек. Правда, значение счетчика при этом будет непостоянным в диапазоне от 7e+006 до 8,5e+006 (усредненные результаты нескольких экспериментов). Т.е. неконтролируемый доступ к общему ресурсу порождает и непредсказуемый результат. И куда спешим?

Возьмем для синхронизации процессов мютекс. Время будет уже много больше — 46,893сек, но при этом получим вполне ожидаемый правильный результат. Но, если это будет семафор, то время увеличится до неприличных 302,316сек при опять же правильном результате.

А, к слову, какие характеристики будут у процессов автоматного класса? Напомним, что кроме потоков из пула, в роли счетчика выступает сам автоматный класс, в рамках которого создается также циклический процесс, управляемый событиями от таймера (далее — таймерный процесс). А поскольку автомат является наследником класса потока, то он может иметь и свой поток.

Результаты экспериментов сведены в таблицу табл. 1. Заданное максимальное значение счетчика — 10000. Столбцы соответствуют разным типам процессов — автоматному, таймерному и на потоке. Строки — разным значениям дискретного времени. Первая строка таблицы — эксперимент, когда дискретное время не задано. В этом случае автомат работает в асинхронном режиме, значение таймера у процесса на его базе — 0, у потока нет вызова функции usleep (). 

Наиболее стабильные результаты демонстрирует автомат, у таймерного процесса погрешность доходит до 10%, у потока — от 20% и может быть даже много больше (вспомним про «тормоза»). Наиболее примечательны результаты первой строки, где поток показывает недостижимую для других процессов скорость, которая при использовании любой синхронизации или даже просто вызова функции «сна» с нулевым параметром стремительно падает.

Тип процесса

(v=10000)

                Время такта (dt)

FSA

timer event

Thread

-

2344

8265

1

1 msec

9793

10586

24322

2 msec

19847

20056

31553

10 msec

99845

100028

120991

100 msec

999980

1092093

1032146

Выводы

Читаю: «Горутины виснут непонятно почему, случайная запись в закрытый канал вызывает panic, нормально протестировать приложение вообще невозможно… написать мало-мальски серьезную программу, которая конкурентно что-то делает, внезапно оказывается не так-то просто.» [2]. Ну, прямо, как говорится, в строку. Думаю, учитывая результаты выше проведенных экспериментов, описанное поведение горутин вполне объяснимо…

Рис. 5. Тестирование объектов синхронизаци

Рис. 5. Тестирование объектов синхронизаци

По мне использование потоков для реализации параллельных процессов ограничено определенными рамками. При этом первые кандидаты на поточную реализацию — процессы, не требующие синхронизации. И уж совсем хорошо, если их скорость, точнее, ее равномерность,   не будет критичным фактором. Для пояснения этого на рис. 5 приведена еще одна визуализация экспериментов с синхронизацией потоков. Картина представлена для 10-ти потоков, без функции «засыпания» и максимальном значении счетчика — 50000 (ср. с рис. 4). Может это случайно, но видно насколько «криво» отрабатывают семафоры. А в контексте данной статьи это только крайний, но, скорее всего, не последний секрет потоков.

Литература:

1.    Многопоточность в Python: очевидное и невероятное. [Электронный ресурс], Режим  доступа:  https://habr.com/ru/articles/764420/ свободный. Яз. рус. (дата обращения 01.06.2024).

2.    Sructured concurency в языке Go. [Электронный ресурс], Режим  доступа:  https://habr.com/ru/hubs/parallel_programming/articles/ свободный. Яз. рус. (дата обращения 01.06.2024).

PS

Увлекшись счетчиками, мы как-то упустили генераторы, хотя именно с них начинали (см. листинг 1). Чтобы не трогать основной текст статьи, поговорим о них здесь. На рис. 6 приведен результат тестирования двух параллельно работающих генераторов, на работу которых наложен исполняемый параллельно же тест счетчика.

Генераторы хорошо демонстрируют проблемы работы потоков в реальном времени. Два нижних графика — это генераторы на автоматах. Созданные, как процессы в среде ВКПа, они сохраняют синхронную работу на все время своей работы. Генераторы на потоках, созданные этими же автоматами (два верхних графика), демонстрируют проблемы работы потоков.

На рис. 6 такие проблемные области выделены и помечены одинаковыми метками.  Области, помеченные 1, демонстрируют, как достаточно быстро генераторы на потоках вошли в противофазу. Заметим, что автоматные же генераторы — стабильны.  Области, помеченные как 2 и 3, показывают, как не столь уж заметные проблемы в работе потоков (их отражает нелинейность графика счетчика) влияют на работу соответствующих им моделей генераторов. Причем, заметим, визуально в один и тот момент времени, но по-разному на разные генераторы. Но генераторы на автоматах, что примечательно, по-прежнему сохраняют свою стабильность!

Рис. 6. Тест генераторов

Рис. 6. Тест генераторов

Вот кратко, пожалуй, и все по генераторы. Главное, что они хорошо демонстрируют стабильную и качественную работу параллельных процессов на автоматах  и наличие проблем в работе  потоков.

Вспомним еще раз цитату про проблемы языка Go. Но прибегать нынче к структурному программированию — это же, как сейчас сказали бы, «полный трэш». Уж тогда просмотрите хотя бы сорокалетней давности книгу Э.Йодана (см. [3] ниже), в которой упомянут метод Ашкрофта и Манны — метод введения переменной состояния (выше в коде любого потока его аналог). А рекомендациям, которые высказаны на ст. 277 его же книги,   я следую, пожалуй, уже не один десяток лет. И, честное слово, повозившись нынче с потоками, ни чуть об этом не жалею. 

И если когда-то давно Дейкстра призвал к отказу от оператора goto, то в ВКПа на уровне модели автомата не используется вообще какой-либо из операторов передачи управления современных языков.  Другими словами, откажитесь от операторов управления современных языков и вам будет счастье! Правда, с одной оговоркой: на уровне предикатов/действий автоматов их применение вполне обосновано.  Но только из чисто практических соображений (с точки зрения теории в них нужды совсем нет). И, если вы обратили внимание, применение операторов управления ограничено узкими рамками, как правило, весьма небольших функций, а потому легко и надежно контролируется.

Так сказать, — структурное программирование с своей идеальной автоматной форме!

Литература:

3. Йодан Э. Структурное проектирование и конструирование программ. М.: Мир,1979 — 415с.

© Habrahabr.ru