заметки на полях (фильтрации)
May. 8th, 2013 01:19 amНа языках программирования можно ввести отношение частичного порядка "А < Б", когда с языка А на язык Б пересесть можно, и это ощущается как прогресс, а вот обратно возвращаться очень неохота и мучительно. Если не ошибаюсь,
lionet в свое время заметил, что в этом отношении заметны две вершины - хаскель и лисп, с их высоты все остальные языки кажутся недостаточно хорошими. Но занятно другое: у этого отношения есть также два дна, по сравнению с которыми все другие языки выглядят превосходными, - это PHP и C++. :)
После ряда других языков мне заставить себя писать что-то на С++ очень сложно, но иногда такая необходимость возникает. Помогает сгладить моральные мучения лишь возможность найти в языке крупицы чего-то хорошего. Нынче вот взялся за новую реализацию своего фирменного super resolution движка, и что меня сейчас радует и выручает, это присутствующие в языке элементы зависимых типов. У меня код оперирует блоками разных размеров и векторами разной точности: это могут быть целые координаты в кадре низкого разрешения, в кадре высокого разрешения, а также координаты с полупиксельной и четвертьпиксельной точностью. Плюсовые шаблоны позволили описать эти вещи как семейства типов, индексированные целочисленными значениями, т.е. натурально зависимые типы получились:
В результате блоки разного размера - это разные типы, и векторы разной точности - разные типы, реально очень помогает не запутаться. Плюс компилятору подспорье: у многих циклов число итераций теперь известно статически, можно хорошо оптимизировать. В иных языках для таких вещей можно использовать phantom types, но там может быть сложнее сделать функцию refine, переводящую вектор на следующий уровень точности, в соседний слой семейства. Все-таки очень удобно, когда с параметром типа можно делать всякую арифметику и использовать его сразу на двух уровнях: типов и выражений.
К слову о зависимых типах. Одна тривиальная мысль о них мне лично оказалась весьма полезной для понимания. Мы привыкли в функциональных языках обозначать тип функции из А в В как А -> B, где А и В какие-то конкретные типы вроде Bool и Int, элементами которых служат значения вроде true и 2. Теперь добавим в систему типов еще один тип, назовем его Type, элементами которого являются типы. Тогда A -> Type будет просто типом функции, которая каждому элементу А сопоставляет какой-то тип. Это и получится зависимый тип, и именно так он и обозначается в соответствующих языках. Например, пишут B : A -> Type и говорят, что В - это зависимый от А тип, или семейство типов, индексированное значениями из А. Но эту же запись можно воспринимать и буквально - как обычную функцию, просто кодомен у нее не совсем обычный.
После ряда других языков мне заставить себя писать что-то на С++ очень сложно, но иногда такая необходимость возникает. Помогает сгладить моральные мучения лишь возможность найти в языке крупицы чего-то хорошего. Нынче вот взялся за новую реализацию своего фирменного super resolution движка, и что меня сейчас радует и выручает, это присутствующие в языке элементы зависимых типов. У меня код оперирует блоками разных размеров и векторами разной точности: это могут быть целые координаты в кадре низкого разрешения, в кадре высокого разрешения, а также координаты с полупиксельной и четвертьпиксельной точностью. Плюсовые шаблоны позволили описать эти вещи как семейства типов, индексированные целочисленными значениями, т.е. натурально зависимые типы получились:
#define VP_LOWRES 1
#define VP_HIRES 2
#define VP_HALF 3
#define VP_QUARTER 4
template <int Prec>
class Vec
{
public:
int x, y;
Vec(int vx, int vy) : x(vx), y(vy) {}
Vec operator+(Vec<int Prec> &a) { return Vec<Prec>(x + a.x, y + a.y); }
Vec operator-(Vec<int Prec> &a) { return Vec<Prec>(x - a.x, y - a.y); }
Vec<Prec + 1> refine() { return Vec<Prec + 1>(x*2, y*2); }
...
};
class Plane
{
...
template<int W> void readBlockQP(Vec<VP_QUARTER> v, MonoBlock<W> &block);
...
}
В результате блоки разного размера - это разные типы, и векторы разной точности - разные типы, реально очень помогает не запутаться. Плюс компилятору подспорье: у многих циклов число итераций теперь известно статически, можно хорошо оптимизировать. В иных языках для таких вещей можно использовать phantom types, но там может быть сложнее сделать функцию refine, переводящую вектор на следующий уровень точности, в соседний слой семейства. Все-таки очень удобно, когда с параметром типа можно делать всякую арифметику и использовать его сразу на двух уровнях: типов и выражений.
К слову о зависимых типах. Одна тривиальная мысль о них мне лично оказалась весьма полезной для понимания. Мы привыкли в функциональных языках обозначать тип функции из А в В как А -> B, где А и В какие-то конкретные типы вроде Bool и Int, элементами которых служат значения вроде true и 2. Теперь добавим в систему типов еще один тип, назовем его Type, элементами которого являются типы. Тогда A -> Type будет просто типом функции, которая каждому элементу А сопоставляет какой-то тип. Это и получится зависимый тип, и именно так он и обозначается в соответствующих языках. Например, пишут B : A -> Type и говорят, что В - это зависимый от А тип, или семейство типов, индексированное значениями из А. Но эту же запись можно воспринимать и буквально - как обычную функцию, просто кодомен у нее не совсем обычный.
no subject
Date: 2013-05-09 08:48 am (UTC)int blah = async compute();
В месте вызова compute() кто-то должен сохранить состояние функции-вызыватора. Куда? Без async всё складывается в железный стек. С async всё, что лежит на стеке, должно уйти куда-то ещё - в мягкий стек; мы же хотим, чтобы железный поток не простаивал, а занимался делом.
1. Поскольку мы хотим смешивать async и не async вызовы, стек не может быть железным и в случае не async вызовов. Поток должен вести мягкий стек. Совокупность мягких стеков и есть совокупность зелёных ниток. Речь идёт о том, что якобы зелёные нитки вести дешевле, чем железные? Выгоды преувеличены.
2. Раньше было:
compute();
finish();
Теперь кто-то эти методы может вызвать async. Раньше у вас были головняки со state transition гарантированным program order, теперь у вас головняки с concurrency in practice, где ничего такого не было. Как пример, запись в поток в цикле - речь не обязательно о файловом потоке, подумайте о шифровании или сжатии. Если функция-вызыватор сама не позаботится о гигиене использования потока, вместо нормального цикла получится асинхронный ад с out of order заполнением потока.
3. async Task<blah> oops() { async oops() } - интересно, что переполнится в этом случае и какова диагностика? Тут ещё вариант, что я не понял, как правильно выразить эту мысль - но должно же быть возможно выразить бесконечную рекурсию. Диагностика этого не то, чтобы невозможна, но хотелось бы услышать, как это решается в данном езыке.
no subject
Date: 2013-05-09 09:19 am (UTC)В кучу уходит, не в стек.
>кто-то эти методы может вызвать async
Этот кто-то .NET framework.
>теперь у вас головняки с concurrency in practice
Я написал довольно много async-await кода (начинал году в 2010 с Async CTP). Этот подход как раз решает эти ваши "головняки с concurrency in practice", а не создаёт. Очень часто удаётся написать даже очень нетривиальный параллельный код без единой блокировки, только с использованием гарантий, которые дают компилятор и рантайм.
>Если функция-вызыватор сама не позаботится о гигиене использования потока
Ничего не нужно делать.
>вместо нормального цикла получится асинхронный ад с out of order заполнением потока.
Не получится, за вас framework это сделает. Framework гарантирует, что statements в async методе будут исполняться строго последовательно, хотя и не обязательно одним потоком конечно.
А может быть и одним потоком, поведение зависит от того есть ли synchronization context, framework сам умеет при необходимости маршалить вызовы в тот поток, из которого вызвали await.
>async Task oops() { async oops() }
Имеете в виду видимо
async Task oops() { await oops(); }?
Получите обычный stack overflow exception в рантайме.
>должно же быть возможно выразить бесконечную рекурсию
Элементарно.
async Task oops() { await Task.Yield(); await oops(); } - ничего не будет падать, и получите бесконечную рекурсию, только зачем?
no subject
Date: 2013-05-09 12:12 pm (UTC)Вот это и есть мягкий стек. В кучу должны складывать все - и вызыватор, и вызываемый.
"Этот подход как раз решает эти ваши "головняки с concurrency in practice", а не создаёт."
я думаю, моя мысль ускользнула. Пример на гугле показывает как раз головняки с concurrency in practice, а ваш пример с await s.readByte() ничего интересного не делает. Интересное как раз вот в чём (по памяти пример из гугла):
Task<string> getString = connection.getStringAsync("http://..."); -- здесь, я так понимаю, не только async таск создаётся, но и запускается. это fork
DoSomethingIndependent(); -- Вот это вот и есть concurrency in practice
return await getString; -- это join
Вызыватор должен быть в курсе, где там внутри методов есть async, иначе getStringAsync и DoSomethingIndependent напорются на конкурентность, где её не было. Проблемы нет, если SomethingIndependent действительно independent.
"Получите обычный stack overflow exception в рантайме."
Прекрасно. А теперь внутри getStringAsync вызывается await функции, вызывающей кого-то ещё и т.д. Как диагностировать, что вызвало stack overflow exception?
no subject
Date: 2013-05-09 12:33 pm (UTC)Нет, вызываемый тупо возвращает экземпляр Task. Он может быть даже выполнен синхронно уже. Таск вовсе не обязан быть реализован через async метод, он может работать как угодно ещё.
Сам async метод - да, складывает в кучу конечно, но это автомагически делает для вас компилятор.
>создаётся, но и запускается. это fork
Нет никаких форков. Все Winapi давным-давно асинхронные, в WinRT вон вообще убрали API блокирующий IO, шоб неповадно было.
>Вызыватор должен быть в курсе, где там внутри методов есть async
Тот кто зовёт вообще не в курсе как именно реализовано API. Для него это самый обычный метод, возвращающий объект Task.
>иначе getStringAsync и DoSomethingIndependent напорются на конкурентность, где её не было
Ещё раз повторяю. Фреймворк by design гарантирует исполнение вашего async-метода не более чем одним потоком одновременно (если вы конечно его только один раз позвали).
>Как диагностировать, что вызвало stack overflow exception?
Точно так же как до появления асинков, посмотреть в дебаггере на call stack.
no subject
Date: 2013-05-09 06:57 pm (UTC)И вон в параллельной ветке упоминают pool, который async таски подбирает.
Про fork - это термин.
no subject
Date: 2013-05-09 09:33 pm (UTC)>в каком порядке выполнятся getStringAsync и DoSomethingIndependent?
Если в каком-то методе написаното doSomethingIndependent() будет исполнено только после того как getStringAsync() вернуло результат, и вообще не будет исполнено если getStringAsync() упадёт с исключением.
>в параллельной ветке упоминают pool, который async таски подбирает.
Эти и другие детали раализации вовсе не обязательно знать, шоб писать корректный код.
no subject
Date: 2013-05-09 09:54 pm (UTC)вот: http://code.msdn.microsoft.com/Async-Sample-Example-from-9b9f505c/sourcecode?fileId=63457&pathId=166422599
(если не проглотит как спам)
GetStringAsync написан так же, как и AccessTheWebAsync, т.е. на входе что-то делается, потом await сокета, потом на выходе что-то делается с результатом. В момент await внутри GetStringAsync мы возвращаемся на строку, где GetStringAsync вызван. DoSomethingIndependent не зависит от строки, но нас должно волновать взаимодействие его с остальным кодом в GetStringAsync.
Если мой вопрос всё ещё не понятен, то бог с ним.
no subject
Date: 2013-05-09 10:11 pm (UTC)Да, в нём асинхронная порция функциональности GetStringAsync вполне может работать параллельно с исполнением DoIndependentWork().
Это вам не подходит? Хотите сначала закончить GetStringAsync потом DoIndependentWork?
Замените "Task<string> getStringTask =" на "string urlContents = await ".
no subject
Date: 2013-05-10 07:24 am (UTC)Подходит ли async - это ещё зависит, не могут ли суб-классы переопределить поведение таким образом, что оно неожиданно станет асинхронным адом. Вещи перестают быть чёрными ящиками.
no subject
Date: 2013-05-10 02:30 pm (UTC)Не имеет. GetStringAsync как и подавляющее большинство async методов имеет всего 2 порции кода: синхронная первая, и callback когда получен результат.
>могут ли суб-классы переопределить поведение таким образом, что оно неожиданно станет асинхронным адом
Если убрать слово "неожиданно" — ответ "конечно могут".
Например достаточно добавить в последнюю строчку реализации GetStringAsync "Task.Run( шонибудь )", шонибудь запустиццо и продолжит работать параллельно даже после того как GetStringAsync вернёт результат.
Неожиданно такого не происходит.