
Содержание статьи
В заключительной статье о конкурентности в Go разберем инструкцию select , сигнальные каналы и инструменты пакета context . С их помощью удобно обмениваться сообщениями, управлять дедлайнами и отменять запущенные операции. В качестве практического примера реализуем простой механизм крекинга хеша MD5: будем подбирать пароль из словаря, пока его хеш не совпадет с нужным.
Словари можно взять, например, в репозитории SecLists . Заранее выбери любое слово не из первых строк словаря и запиши его MD5-хеш. Например, для пароля netadmin он будет таким:
Это пригодится для экспериментов с кодом.
Создай новый модуль:
Вставь в main. go такой код:
Здесь мы определили горутины, из которых соберем конвейер обработки. Чтобы не передавать в функции кучу аргументов, используем структуру PipelineConfig : в ней храним все параметры конвейера. Канал JobCh нужен для передачи сообщений, которые надо обработать, то есть слов, считанных из словаря. В ResultCh отправим слово, чей хеш совпал с искомым, если такое слово найдется.
В ErrorCh отправляем сообщения об ошибках. DoneCh — сигнальный канал: через него передадим всем горутинам команду сразу завершить работу. Строковая переменная SrcFileName хранит имя файла словаря, из которого мы читаем данные, а TargetHash — указатель на хеш, с которым ищем совпадение.
Функция produce( ) генерирует задания для дальнейшей обработки: читает строки из списка, валидирует их и отправляет в канал. Сама обработка — вычисление хешей — в задачи этой функции не входит и на ее код не влияет.
Функция worker( ) обрабатывает задания: вычисляет хеш, сравнивает его с целевым значением и, если они совпадают, отправляет слово в канал результатов и завершается. Обрати внимание: когда ты параллельно запускаешь несколько воркеров, совпадение найдет один из них и завершится, но остальные об этом не узнают и продолжат гриндить данные, пока те не закончатся. Поэтому нам нужен отдельный сигнал завершения, чтобы передать всем горутинам «стоп».
Функция collect( ) читает результаты и выводит их в консоль. Здесь мы ждем только один результат: так устроена задача. Поэтому цикла нет — просто ждем первое сообщение или закрытие канала, смотря что случится раньше.
О чтении из канала мы говорили в предыдущей статье , но вкратце напомню, если ты вдруг забыл: val, ok : = < -ch блокирует текущую горутину, пока из канала ch не придет значение val . Если канал закроют, val получит zero value для типа канала, а ok станет false . Мы используем проверку if ! ok , чтобы разделить две ситуации — получение сообщения и закрытие канала — и выполнить нужные действия.
Как и в предыдущей статье, собираем конвейер обработки из отдельных горутин: одна генерирует задания, пул горутин параллельно обрабатывает их, а еще одна собирает результаты:
Чтобы сэкономить место, я не буду заново разбирать, как работает конвейер. Если нужно — вернись к предыдущей статье . Кратко напомню последовательность запуска: мы инициализировали нужные каналы и структуру с настройками.
Горутина produce (для наглядности я буду называть горутины по именам соответствующих функций) запускается и работает, пока не прочитает файл srcFileName .
Горутина collect запускается и работает, пока не закроется канал resultCh или пока она не получит результат вычислений.
Горутины worker запускаются и работают, пока не закроется канал jobCh .
В отдельной горутине ждем, когда завершатся все воркеры, и только потом закрываем канал с результатами. Так воркер, который еще работает, не попытается записать в уже закрытый канал.
Еще в одной отдельной горутине ждем завершения конвейера и закрываем канал ошибок.
Тем временем поток выполнения идет дальше: main( ) заходит в бесконечный цикл чтения из канала ошибок errCh и остается там, пока канал не закроется. После этого по значению флага hadError выбирается код возврата.
И последовательность остановки:
После чтения файла produce завершается, а канал jobCh закрывается.
Цикл чтения из jobCh внутри worker завершается, когда канал закрывается. После этого worker( ) возвращает управление. В отдельной горутине мы ждем, пока завершатся все воркеры, и закрываем канал resultCh .
Чтение из resultCh внутри collect тоже завершается: либо мы получили искомое значение, либо все воркеры отработали и канал resultCh закрылся. После возврата из collect( ) закрываем канал doneCh — это самый важный момент, но о нем чуть позже.
В отдельной горутине ждем, пока остановится конвейер, то есть завершатся produce и collect , а затем закрываем канал errCh .
После закрытия errCh главная горутина выходит из цикла чтения.
Сигнальный канал и select
Разберем код функции worker( ) подробнее:
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
9990 рублей 5000 р.
950 р.
12 января 2027 года в Microsoft официально прекратят расширенную поддержку Windows Server …