Каналы или примитивы синхронизации (мьютексы и др.)? Что делать?

У меня мало опыта в Го. Итак, недавно, когда компания, в которую я собираюсь устроиться, дала мне домашнее задание на собеседовании, я воспользовался возможностью, чтобы попрактиковаться в Го. Это один из основных языков в их наборе инструментов (по крайней мере, так кажется со стороны), а домашнее задание на собеседовании, как правило, скучное занятие, так почему бы не сделать его интересным?

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

В Go это, очевидно, достигается с помощью нескольких горутин. Подпрограмма «печать через равные промежутки времени», очевидно, должна планироваться отдельно от подпрограммы «чтения из файла». По крайней мере, в этот файл нельзя будет записывать больше байтов в течение длительного времени, и процедура "печать через равные промежутки времени" должна по-прежнему срабатывать, пока процедура "чтения из файла" заблокирована.

Так как координировать? Потому что было бы странно, если бы во время определенного цикла печати ключевая статистика менялась из-за считывания байтов. Представьте себе печать трех статистических данных: общее количество записей в файле, количество неразборчивых записей и количество действительных записей. Если бы сначала выводилось итоговое значение, а затем два последних, но статистика изменилась в середине, возникла бы ситуация, когда неразборчиво + действительно != итог. Это несовместимо. И действительно, это термин, который мы бы использовали: непоследовательные чтения. Мы хотим последовательности, даже в этой тривиальной и надуманной ситуации. Как мы этого достигаем?

Что ж, проблема сводится к одному из разделяемых состояний — две подпрограммы обращаются к ключевой статистике и могут делать это одновременно. Это не просто теоретический сценарий в однопроцессорной системе, где упреждающее планирование приостанавливает поток, который явно не уступает. Большинство систем в настоящее время являются многопроцессорными, и программы Go используют все преимущества. Две описанные выше подпрограммы, реализованные как горутины, будут сопоставлены с двумя потоками ОС, каждый из которых читает и записывает в одну и ту же общую память.

Go предлагает два решения:

Мьютексы требуют, чтобы пользователь общего ресурса получил блокировку (атомарно) перед доступом к этому ресурсу и разблокировал его, когда пользователь сделал это. Если каждый пользователь играет хорошо, это работает хорошо. Проблема в том, что какой-нибудь бедный инженер-программист забывает правила участия. В конце концов, ничто не мешает инженеру писать код, который просто не блокирует и не разблокирует. Это проблема, потому что замки не компонуемые. Например, блокировку и разблокировку нельзя просто поместить в метод прочитать общее число ключевой статистики. Да, это гарантирует, что инженер-пользователь не сможет забыть. Но если требуется чтение общего прочитанного числа и неразборного числа как части одной и той же атомарной операции, для этого случая необходимо написать специальный метод. (Да, в этом вырожденном случае сгодится реентерабельная блокировка, но композиция блокировок вообще невозможна. И, кроме того, в Go нет реентерабельных блокировок.)

Так как насчет ЦСП? Go предлагает каналы — координирующие горутины через передачу сообщений. Как это будет работать здесь? Вместо совместного использования состояния одна горутина в цикле будет получать сообщения от других горутин. Одним из них может быть запрос на атомарное чтение трех упомянутых выше статистических данных. Другим может быть запрос на атомарное добавление новой информации в ключевую статистику. Совместного использования нет — только одна процедура затрагивает память за раз; действительно, только одна процедура затрагивает память. Таким образом, это полностью безопасно — инженер-пользователь такого интерфейса вынужден общаться по каналу, поэтому он не может забыть о параллелизме. Здорово!

Либо это? Причина, по которой я написал этот пост, заключается в том, что мне было очень трудно решить, какой метод использовать. Есть и существенные недостатки у варианта CSP/channel:

  • Производительность. Обычно я бы сказал, что это отвлекающий маневр, но если кто-то использует Go, производительность, вероятно, имеет значение. Мьютексы на порядок быстрее.
  • Комбинируемость. Поскольку каждое сообщение должно рассматриваться как сигнал для выполнения атомарной операции (если вы хотите оставаться в здравом уме…), возникает та же проблема, что и с мьютексами. В сложных ситуациях единственным решением является написание DSL-запроса. Правильно. Что приводит нас к:
  • Сильная связь. Вместо того, чтобы доверять инженерам пользователей блокировать и разблокировать и делать все, что они хотят, в середине, в модели канала любой набор операций, которые должны быть объединены в атом, должен быть реализован. в назначенном коде горутины ресурса. Это достаточно плохо, когда весь код находится в одном проекте. Но как можно использовать библиотеки таким образом?

Пожалуйста, имейте в виду, что у меня минимальный опыт работы с Go, а CSP для меня новичок. Так что, возможно, моя оценка неверна. Пожалуйста, свяжитесь со мной и скажите мне об этом. (@alyssackwan в Твиттере.)

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