Разделение пользовательского и серверного пространств подразумевает две горутины. В первой происходит вся внутренняя кухня, начиная с чтения из сокета, заканчивая http-специфичными штуками. Во второй - ждём сигнала из оповестительного канала. При получении, вызываем обработчик, при завершении - извещаем об этом ядро. И на этом строится вся синхронизация, от которой зависит половина кодовой базы. Это, помимо постоянных дедлоков при изменении логики работы данного механизма (причина нескольких переписываний чуть ли не с нуля), было очень удобным для имплементации потоковой обработки тела. Но была одна проблема...
...и заключалась эта проблема в чрезмерном количестве синхронизаций, основанных на межканальном взаимодействии. А происходили они действительно часто, особенно при работе с телом, и избежать их нельзя было никак (даже если с телом никто не взаимодействует). Это было медленно. Это создавало слишком частые переключения. Это приводило к повышению задержек. Это, в конце концов, приводило к нестабильным результатам бенчмарков - погрешность была больно уж велика.
Новая же модель подразумевает, что http-парсер будет парсить до тела. Само же тело он не трогает. Как только спарсились заголовки - вызывается обработчик, и уже внутри обработчика при потребности происходит чтение из сокета, и собственно обработка тела. Сам сокет был обёрнут в некий объект, который умел хранить "ненужные" данные, которые будут возвращены при следующем чтении - чтобы вычитав заголовки с кусочком тела, всё не сломалось к чертям. Это и позволило безболезненно разделить оба процесса парсинга друг от друга.
В результате, бенчмаркать стало немного труднее, однако это не есть проблема. Сами же бенчмарки стали более обширными, покрывая также tcp-сервер (однако не совсем точным образом; палки в колёса вставляют таймауты на чтение). Не смотря на это, прирост составил около трёхкратную разницу в случае с GET-запросом на 10 заголовков (3000ns -> 860ns).
Собственно, суть данного поста в донесении следующих мыслей:
- горутины сами по себе не дорогие; дорого их синхронизировать
- здесь сработал принцип упрощения алгоритма: несмотря на когнитивное усложнение архитектуры, отныне из алгоритма исключена целая куча синхронизаций. Это привело не только к ускорению кода, но и к тому, что работать с ним стало проще - меньше потенциальных мест, изменения в которых могут привести к отстреленной ноге