На собеседовании была задача.
C#.
В 3 разных потока будет передан один объект. В нем есть 3 метода. RunFirst, RunSecond, RunThird.
Нужно сделать так, что бы методы могли быть вызваны только по очереди. Хотя потоки могут вызывать их как угодно.
Ну сам класс то я реализовал через lock.
А потом меня попросили написать юнит-тест на это.
Как такое тестировать? Реально стартовать потоки в тесте? Но где гарантия, что потоки случайно не вызовут методы в нужном порядке.
Да и что проверять в тесте?
Здравствуйте, BlackEric, Вы писали:
BE>А потом меня попросили написать юнит-тест на это.
BE>Как такое тестировать? Реально стартовать потоки в тесте? Но где гарантия, что потоки случайно не вызовут методы в нужном порядке. BE>Да и что проверять в тесте?
Нужно синхронизовать потоки таким образом, чтобы методы гарантированно вызвались в неправильном порядке, и убедиться, что ваш код кинул эксепшны где ожидалось.
Ничего страшного нет в том, чтобы стартовать потоки в тестах. Есть "юнит" — минимально осмысленная подсистема, поведение которой можно протестировать. Есть "фиксча" — тестовый стенд для этой системы. В вашем случае "фиксча" — это будут вот эти вот потоки, синхронизованные определенным образом.
Здравствуйте, BlackEric, Вы писали:
BE>Нужно сделать так, что бы методы могли быть вызваны только по очереди. Хотя потоки могут вызывать их как угодно.
Мне решительно кажется, что нет способа не дать вызвать метод, когда есть ссылка на объект. Речь, вероятно, все-таки о "не дать выполнить метод". Т.к. можно ограничить выполнение, но нельзя ограничить вызов.
BE>Ну сам класс то я реализовал через lock.
BE>А потом меня попросили написать юнит-тест на это.
BE>Как такое тестировать? Реально стартовать потоки в тесте? Но где гарантия, что потоки случайно не вызовут методы в нужном порядке.
Да, стартовать потоки и с помощью синхронизации гарантировать определенную тестом последовательность вызовов. BE>Да и что проверять в тесте?
То, что правильная последовательность позволяет выполнить все методы, а неправильная приводит к неудаче.
Здравствуйте, samius, Вы писали:
S>Мне решительно кажется, что нет способа не дать вызвать метод, когда есть ссылка на объект. Речь, вероятно, все-таки о "не дать выполнить метод". Т.к. можно ограничить выполнение, но нельзя ограничить вызов.
Да вроде есть — прокси-объект который владеет реальным объектом, реализующим RunFirst, RunSecond, RunThird. Я не большой специалист в C#, но мне думается что до кучи это будет единственный способ покрыть нетагивными тестами подобный сценарий поведения. То есть реализовать мок для реального объекта и проверить что прокси гарантирует очерёдность вызовов.
Здравствуйте, kaa.python, Вы писали:
KP>Да вроде есть — прокси-объект который владеет реальным объектом, реализующим RunFirst, RunSecond, RunThird. Я не большой специалист в C#, но мне думается что до кучи это будет единственный способ покрыть нетагивными тестами подобный сценарий поведения. То есть реализовать мок для реального объекта и проверить что прокси гарантирует очерёдность вызовов.
Если бы ссылки на объект у вызывающего не было, то да, прокси не позволит вызвать метод объекта. Но по условию у вызывающего есть ссылка на сам объект.
Здравствуйте, BlackEric, Вы писали:
BE>На собеседовании была задача. BE>C#. BE>В 3 разных потока будет передан один объект. В нем есть 3 метода. RunFirst, RunSecond, RunThird. BE>Нужно сделать так, что бы методы могли быть вызваны только по очереди. Хотя потоки могут вызывать их как угодно. BE>Ну сам класс то я реализовал через lock.
BE>А потом меня попросили написать юнит-тест на это.
BE>Как такое тестировать? Реально стартовать потоки в тесте? Но где гарантия, что потоки случайно не вызовут методы в нужном порядке. BE>Да и что проверять в тесте?
Имхо, поскольку многопоточка — это случайный процесс, то нужны специальные средства тестирования, которые бы гарантированно проверили все взаимные варианты, чтобы отловить рейс кондишны и прочие многопоточыне глюки.
В джаве для таких целей есть несколько библиотек, например https://github.com/openjdk/jcstress
Но я на практике её не применял, т.к. она очень долго выполняет проверки. Есть коммерческие варианты, вроде как более удобные для реального применения.
Здравствуйте, samius, Вы писали:
S>Если бы ссылки на объект у вызывающего не было, то да, прокси не позволит вызвать метод объекта. Но по условию у вызывающего есть ссылка на сам объект.
Что-то похожее никак не противоречит условиям задачи (это C++, но на C# будет похоже кмк)
Здравствуйте, BlackEric, Вы писали:
BE>На собеседовании была задача. BE>C#. BE>В 3 разных потока будет передан один объект. В нем есть 3 метода. RunFirst, RunSecond, RunThird. BE>Нужно сделать так, что бы методы могли быть вызваны только по очереди. Хотя потоки могут вызывать их как угодно. BE>Ну сам класс то я реализовал через lock.
Ну да, конечный автомат (т.е. некое общее состояние, возможно enum) + lock. Хотя можно было бы и через waithandl'ы
как-то сделать -- т.е. runfirst будит handle для runsecond и т.д. Но тут реально можно больше проблем огрести,
чем через lock. Хотя логика возможно будет более прямолинейная.
BE>А потом меня попросили написать юнит-тест на это. BE>Как такое тестировать? Реально стартовать потоки в тесте? Но где гарантия, что потоки случайно не вызовут методы в нужном порядке. BE>Да и что проверять в тесте?
Да тут вообще можно перебором (3!) все варианты запустить через соотв. thread.sleep. Т.е. завести массив целых
типа sleep = int[] {1000,2000,3000} и все перестановки для thread.sleep для каждого потока.
Т.е. генерируйте тройки перестановок и засыпайте потоки на соотв. время перед запуском соотв. метода.
Здравствуйте, kaa.python, Вы писали:
KP>Здравствуйте, samius, Вы писали:
S>>Если бы ссылки на объект у вызывающего не было, то да, прокси не позволит вызвать метод объекта. Но по условию у вызывающего есть ссылка на сам объект.
KP>Что-то похожее никак не противоречит условиям задачи (это C++, но на C# будет похоже кмк)
Если внимательно почитать условие, то у клиента есть ссылка на объект, чьи методы он вызывает. И если этот объект будет Proxy, то придотвратить вызов Proxy::RunThird() мы не можем. А вызывать Impl::RunThird() клиент не должен, т.к. у него нет на него ссылки.
Если же слово "вызвать" заменить на "выполнить", то да, прокси подойдет, безусловно. Так же как и lock внутри реализации методов объекта.
Я не утверждаю, что прокси — плохое решение. Я утверждаю, что прокси — это решение задачи с формулировкой "выполнить" методы.
KP>А потом просто для Impl делаешь мок и тестируешь поведение оболочки.
Здравствуйте, Sharov, Вы писали:
S>Да тут вообще можно перебором (3!) все варианты запустить через соотв. thread.sleep. Т.е. завести массив целых S>типа sleep = int[] {1000,2000,3000} и все перестановки для thread.sleep для каждого потока. S>Т.е. генерируйте тройки перестановок и засыпайте потоки на соотв. время перед запуском соотв. метода.
Достаточно будет просто из теста запускать поток с соответствующим ThreadStart-ом.
факториал тут явно избыточен, т.к. проверить, что методы 2 и 3 не запускаются без предварительного вызова метода 1 можно и не создавая дополнительных потоков вовсе.
т.е.
1) в потоке теста вызываем методы 2 и 3, что бы убедиться, что они не отработают.
2) в левом потоке вызываем метод 1, после Join пытаемся в потоке теста вызвать 3 (должен сфейлиться) и 2 (должен выполниться).
3) в левом потоке вызваем 1, после Join в другом левом 2, после Join в потоке теста вызываем 3 и проверяем, что он выполнился.
Так мы проверим то, что методы не вызываются в неверной последовательности. Такая совокупность тестов не потребует синхронизации внутри реализации. По хорошему нужно еще проверить на щели в реализации, что бы исключить потенциальные гонки, но условие напрямую не требует этим заниматься. В том виде, как я его понял. Но что бы проверять потенциальные гонки, надо знать дополнительно, считается ли корректным вызов метода 2 более одного раза, если 1 уже выполнен, если 3 уже выполнен, и т.п.? Без ответа на эти вопросы организовать исчерпывающее тестирование не удастся.
Здравствуйте, samius, Вы писали:
S>Здравствуйте, Sharov, Вы писали:
S>>Да тут вообще можно перебором (3!) все варианты запустить через соотв. thread.sleep. Т.е. завести массив целых S>>типа sleep = int[] {1000,2000,3000} и все перестановки для thread.sleep для каждого потока. S>>Т.е. генерируйте тройки перестановок и засыпайте потоки на соотв. время перед запуском соотв. метода.
S>Достаточно будет просто из теста запускать поток с соответствующим ThreadStart-ом. S>факториал тут явно избыточен, т.к. проверить, что методы 2 и 3 не запускаются без предварительного вызова метода 1 можно и не создавая дополнительных потоков вовсе. S>т.е. S>1) в потоке теста вызываем методы 2 и 3, что бы убедиться, что они не отработают. S>2) в левом потоке вызываем метод 1, после Join пытаемся в потоке теста вызвать 3 (должен сфейлиться) и 2 (должен выполниться). S>3) в левом потоке вызваем 1, после Join в другом левом 2, после Join в потоке теста вызываем 3 и проверяем, что он выполнился.
Я думал одним тестом протестировать все возможные манипуляции с объектом, благо вариантов немного.
Т.е. по результатам каждой возможной посл-ти вызовов объект будет в корректном состоянии. Возможно это не совсем то.
Во варианте выше каждый случай 1)2)3) я бы сделал в виде отдельного теста, раз уж мы явно проверяем некоторые
условия. Что может быть и неплохо.
S>Так мы проверим то, что методы не вызываются в неверной последовательности. Такая совокупность тестов не потребует синхронизации внутри реализации.
Синхронизация где, в тесте или в реализации объетка? В объекте по любому нужна, т.к. до окончания метода1 может начать
работу метод2 и поломать все инварианты.
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, samius, Вы писали:
S>Я думал одним тестом протестировать все возможные манипуляции с объектом, благо вариантов немного. S>Т.е. по результатам каждой возможной посл-ти вызовов объект будет в корректном состоянии. Возможно это не совсем то.
Вероятно, нам ведь надо проверить невозможность вызова/выполнения, а не состояние объекта.
S>Во варианте выше каждый случай 1)2)3) я бы сделал в виде отдельного теста, раз уж мы явно проверяем некоторые S>условия. Что может быть и неплохо.
Это уже детали оформления.
S>>Так мы проверим то, что методы не вызываются в неверной последовательности. Такая совокупность тестов не потребует синхронизации внутри реализации.
S>Синхронизация где, в тесте или в реализации объетка? В объекте по любому нужна, т.к. до окончания метода1 может начать S>работу метод2 и поломать все инварианты.
В объекте синхронизация нужна, я о том, что предложенный мной подход с потоками и Join-ами не выявит отсутствия синхронизации. Подход со слипами по несколько секунд — тоже. Тест, который потребует синхронизации, должен кэшировать состояние объекта, например, один поток в цикле пытается вызвать метод 2, но фейлится (видимо, ловит исключение) и продолжает цикл. Второй поток параллельно вызыват 1. При наличии синхронизации первый поток должен выйти из цикла после успешного вызова 2. Но этот подход хорош в теории, ведь методы объекта могут быть достаточно противные, что бы состояние объекта выпадало из кэша. Тогда и этот подход не потребует синхронизации.
Здравствуйте, Sharov, Вы писали:
S>Да тут вообще можно перебором (3!) все варианты запустить через соотв. thread.sleep. Т.е. завести массив целых S>типа sleep = int[] {1000,2000,3000} и все перестановки для thread.sleep для каждого потока. S>Т.е. генерируйте тройки перестановок и засыпайте потоки на соотв. время перед запуском соотв. метода.
За thread.sleep в авто-тестах я бы сразу no hire делал. Оно не только работает ужасно долго делая билд тормозным солидным, но и иногда ломается, когда вдруг где-то что-то засвопит при очередном тестовом прогоне и завалит билд.
Надо просто блокировать|разблокировать треды в нужном порядке. Это я исхожу из моего понимания задания — мол, second должен лочиться до тех пор пока не вызван first. Иначе не очень ясно что должен делать second в момент когда first уже работает.
Если же фейлы нужны, то непонятно зачем с тредами тестировать, всё гораздо проще. Использовать какой-нибудь потокобезопасный примитив для изменения state, да и всё:
Здравствуйте, ·, Вы писали:
S>>Да тут вообще можно перебором (3!) все варианты запустить через соотв. thread.sleep. Т.е. завести массив целых S>>типа sleep = int[] {1000,2000,3000} и все перестановки для thread.sleep для каждого потока. S>>Т.е. генерируйте тройки перестановок и засыпайте потоки на соотв. время перед запуском соотв. метода. ·>За thread.sleep в авто-тестах я бы сразу no hire делал. Оно не только работает ужасно долго делая билд тормозным солидным, но и иногда ломается, когда вдруг где-то что-то засвопит при очередном тестовом прогоне и завалит билд.
Справедливо, ибо недетерминированность конечно присутствует, т.е. зависит от окружения -- что там еще на билд машине может быть.
Насчет времени выполнения -- можно уменьшить в 10 или даже 100 раз. Главное, чтобы по очереди.
Также не ясно как своп может что-то поломать в этом сценарии ?
·>Надо просто блокировать|разблокировать треды в нужном порядке. Это я исхожу из моего понимания задания — мол, second должен лочиться до тех пор пока не вызван first. Иначе не очень ясно что должен делать second в момент когда first уже работает.
Тогда не понятно, зачем вообще потоки нужны, тем более если исп. lock? Последовательно вызываем методы и проверяем
ожидаемый результат.
·>Честно говоря, я себе слабо представяю как написать _надёжный_ тест, который тестирует что переменная помечена volatile.
S>>Синхронизация где, в тесте или в реализации объетка? В объекте по любому нужна, т.к. до окончания метода1 может начать S>>работу метод2 и поломать все инварианты. S>В объекте синхронизация нужна, я о том, что предложенный мной подход с потоками и Join-ами не выявит отсутствия синхронизации. Подход со слипами по несколько секунд — тоже. Тест, который потребует синхронизации, должен кэшировать состояние объекта, например, один поток в цикле пытается вызвать метод 2, но фейлится (видимо, ловит исключение) и продолжает цикл. Второй поток параллельно вызыват 1. При наличии синхронизации первый поток должен выйти из цикла после успешного вызова 2. Но этот подход хорош в теории, ведь методы объекта могут быть достаточно противные, что бы состояние объекта выпадало из кэша. Тогда и этот подход не потребует синхронизации.
Не понял зачем парится с кэшированием, если у нас есть lock?
А как на практике тестируют lock-free структуры данных и соотв. код? Просто в данном случае, если ТС выбрал lock,
то про потоки можно забыть, т.е. просто тестировать соотв. состояние. А если volatile -- то
Здравствуйте, BlackEric, Вы писали:
BE>На собеседовании была задача. BE>C#. BE>В 3 разных потока будет передан один объект. В нем есть 3 метода. RunFirst, RunSecond, RunThird. BE>Нужно сделать так, что бы методы могли быть вызваны только по очереди. Хотя потоки могут вызывать их как угодно. BE>Ну сам класс то я реализовал через lock.
Тут по заданию не ясно, что должно произойти если вызываем неверный метод ?
Кидаем исключение, зависаем пока не вызовем нужный метод, программа падает или что-нибудь другое ?
Ну а после этого и будет ясно какой тест писать.
BE>А потом меня попросили написать юнит-тест на это.
BE>Как такое тестировать? Реально стартовать потоки в тесте? Но где гарантия, что потоки случайно не вызовут методы в нужном порядке. BE>Да и что проверять в тесте?
Тут зависит от того, что мы решили делать.
Здравствуйте, Sharov, Вы писали:
S> Справедливо, ибо недетерминированность конечно присутствует, т.е. зависит от окружения -- что там еще на билд машине может быть. S> Насчет времени выполнения -- можно уменьшить в 10 или даже 100 раз. Главное, чтобы по очереди.
А кто тебе обещал очередность sleep-ов?
S> Также не ясно как своп может что-то поломать в этом сценарии ?
Тред может просыпаться позднее указанного времени. И по большому счёту в любом порядке. Особенно, если ты уменьшишь в 100 раз.
S> ·>Надо просто блокировать|разблокировать треды в нужном порядке. Это я исхожу из моего понимания задания — мол, second должен лочиться до тех пор пока не вызван first. Иначе не очень ясно что должен делать second в момент когда first уже работает. S> Тогда не понятно, зачем вообще потоки нужны, тем более если исп. lock? Последовательно вызываем методы и проверяем ожидаемый результат.
Ну я вижу такой сценарий. first это некий init, который записывает какие-то нужные данные, а second — это запрос, которому нужны результаты init и он их может ожидать из другого треда.
S> Тут едва ли тест поможет, скорее cr.
"cr"?
S>А как на практике тестируют lock-free структуры данных и соотв. код? Просто в данном случае, если ТС выбрал lock, S>то про потоки можно забыть, т.е. просто тестировать соотв. состояние. А если volatile -- то
Это если все гарантированно правильно работает, но если все основано на вере, то и тест не нужен, если не ставиться цель написать тест который всегда зеленый.
А так- пустить 100 групп потоков по 3 с рандомным слипом, если до финиша добралась хотя бы одна группа с неправильной последовательностью срабатывания, не добралась хотя бы одна группа с правильной, и выскочили любые исключения кроме специально добавленных, то тест провален.
Ах да, еще это надо сделать на x86,x64,ARM , в разных средах.
Многопоточность.
Здравствуйте, ·, Вы писали:
·>Здравствуйте, Sharov, Вы писали:
S>> Справедливо, ибо недетерминированность конечно присутствует, т.е. зависит от окружения -- что там еще на билд машине может быть. S>> Насчет времени выполнения -- можно уменьшить в 10 или даже 100 раз. Главное, чтобы по очереди. ·>А кто тебе обещал очередность sleep-ов?
Формально -- никто, но с большой вероятность, тот что 1000мс спал проснется раньше, чем тот что 3000мс. Что и нужно.
S>> Также не ясно как своп может что-то поломать в этом сценарии ? ·>Тред может просыпаться позднее указанного времени. И по большому счёту в любом порядке. Особенно, если ты уменьшишь в 100 раз.
Скорее всего они все проснутся позднее, т.е. будет сдвиг у всех.
Я согласен, что идея так себе, но для наколенного теста сойдет. А для посерьезнее надо думать.