Skip to content

Lazy#21

Open
Palezehvat wants to merge 11 commits intomasterfrom
Lazy
Open

Lazy#21
Palezehvat wants to merge 11 commits intomasterfrom
Lazy

Conversation

@Palezehvat
Copy link
Owner

No description provided.

Copy link

@YuriUfimtsev YuriUfimtsev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

И еще одно замечание: условие про отсутствие дублирования тестов на общее поведение для двух реализаций. Можно воспользоваться атрибутом TestCaseSource

/// </summary>
public static class FunctionsForTests
{
public static volatile int howMutchFunctionBeenCalled = 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mutch?)

public interface ILazy<T>
{
/// <summary>
/// Getting the created object

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тогда уж
The class implements, Gets the created object


public class MultiThreadLazy<T> : ILazy<T>
{
private Func<T> functionForLazy;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лучше просто supplier

private Object objectLock = new ();
private volatile bool initialized = false;
private T resultSuppiler;
private Exception exceptionSuppiler = default;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

К предупреждению компилятора стоит прислушаться, exceptionSupplier при инициализации класса null-ом становится

/// Constructor for storing the object creation function
/// </summary>
public MultiThreadLazy(Func<T> function)
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тоже важное замечание компилятора, которое также отсылает к условию "supplier вправе вернуть null"

{
[Test]
public void SimpleExample()
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тест с тремя+ Assert-ами не есть хорошо, особенно, если они все друг с другом не связаны. Надо разбить на более мелкие тесты с говорящими названиями

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я понял, но тогда будут отдельные тесты для многопоточности и одно поточности, я не очень понимаю, как применить TestCaseSource, если запуск многопоточных тест и однопоточных сильно отличается

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Речь больше про то, чтобы разделить тесты на два типа:

  1. Тест(тесты) на корректную работу многопоточного Lazy и отсутствие гонок
  2. Тест(тесты) на корректную работу Lazy, когда к нему обращается всего один поток. И такого рода тесты надо запускать на двух вариантах Lazy, используя TestCaseSource для передачи объектов разных типов

Assert.That(FunctionsForTests.FunctionForLazyWithCounter(), Is.Not.EqualTo(singleThreadLazy.Get()));
FunctionsForTests.howMutchFunctionBeenCalled = 0;
var multiThreadLazy = new MultiThreadLazy<int>(() => FunctionsForTests.FunctionForLazyWithCounter());
var arrayThreads = new Thread[Environment.ProcessorCount];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вдруг Environment.ProcessorCount вернет 1? :)
На такой случай лучше вручную потоков 10 создать, в крайнем случае будет симуляция многопоточности

}
catch (InvalidOperationException)
{
Assert.True(true);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Какая-то максимально бесполезная строчка :)
Никакой проверки в Assert не происходит, и в то же время если исключение не бросится, тест не упадет. Надо исправить

[Test]
public void ThreadRaceTest()
{
var multiThreadLazy = new SingleThreadLazy<int>(() => FunctionsForTests.FunctionForLazyWithCounter());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Что-то здесь не так

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это проверка на гонку, проверяю, что при реализации в однопоточном режим использование многопоточности приводит к искажению результата, как по другому проверить, что нету гонок я не знаю

Copy link

@YuriUfimtsev YuriUfimtsev Sep 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Проверка на отсутствие гонок предполагает запуск многопоточного Lazy несколькими потоками и проверку, что все потоки получили один и тот же результат. Кстати, можно использовать ManualResetEvent для увеличения шанса одновременного запрашивания потоками значения Lazy.
В однопоточном режиме должно приводить к искажению, да, но это не часть функциональности системы, которую мы реализуем и хотим поддерживать, проверять тестами

value = multiThreadLazy.Get();
lock (value)
{
if ((int)value != 1)
Copy link

@YuriUfimtsev YuriUfimtsev Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

К этому тесту тоже много вопросов :)

  1. Неочевидно, чему должно быть равно value. Порядок тестов иметь значения не должен, а static-поле меняется в нескольких тестах. Поэтому, кстати, лучше его static-ом вообще не делать, а для каждого теста создавать свой объект с полем определенного значения. А то запустятся два теста и упадут из-за неупорядоченных проверок одного и того же поля на равенство разным значениям.
  2. Этот тест обречен на вечный успех
  3. Сравнение лучше делать более определенным. Если мы конкретное число ждем, то его и писать

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут скорее попытка поймать, что в однопоточной реализации при использовании многопоточности появляется гонка потоков, а сказать конкретно в какой момент value будет не равно 1 и чему именно
невозможно. Понимаю, что тест не самый хороший, но по-другому, как это проверить я не знаю
.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Выше написал, но главное, что мы хотим проверить не наличие гонки в однопоточном режиме, а отсутствие гонок в многопоточном

namespace Lazy;

/// <summary>
/// The class implements, Gets the created object

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// The class implements, Gets the created object
/// The class implements an object whose function is called only once.

public interface ILazy<T>
{
/// <summary>
/// Getting the created object

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Getting the created object
/// Gets the created object.

{
resultSuppiler = supplier();
supplier = null;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Компилятор остался неуслышанным :(

Comment on lines 36 to 39
if (exceptionSuppiler != default)
{
throw exceptionSuppiler;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Копипаст остался. Может ли exceptionSupplier != default и пройти мимо catch? Если нет, то нужна ли эта проверка, или можно кидать исключение в другом блоке?

@@ -0,0 +1,46 @@
namespace Lazy;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В этот раз аналогично

/// </summary>
public class FunctionsForTests
{
public volatile int howMuchFunctionBeenCalled = 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public-поле -- это не про .NET

Comment on lines 46 to 55
bool isExceptionWasThrown = false;
try
{
value = multiThreadLazy.Get();
}
catch (InvalidOperationException)
{
isExceptionWasThrown = true;
}
Assert.True(isExceptionWasThrown);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool isExceptionWasThrown = false;
try
{
value = multiThreadLazy.Get();
}
catch (InvalidOperationException)
{
isExceptionWasThrown = true;
}
Assert.True(isExceptionWasThrown);
Assert.Throws<InvalidOperationException>(() => multiThreadLazy.Get());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тогда и value не нужно


[Test]
public void ExampleWithExceptionWithMultiThreadLazy()
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

На самом деле, от многопоточного теста нужна только проверка на отсутствие гонок, а проверка на корректность работы произойдет и в однопоточных тестах. Так что будет вполне достаточно трех тестов ниже

{
arrayThreads[i] = new Thread(() =>
{
Assert.That(correctResult, Is.EqualTo(multiThreadLazy.Get()));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert.That в разных потоках -- не лучший вариант, как минимум из-за сложностей отладки. Лучше собрать значения потоков в массив и по нему пробежаться в конце в главном потоке, сравнивая значения с помощью Assert.That

/// Function that throws InvalidOperationException
/// </summary>
/// <returns></returns>
public int FunctionWithInvalidOperationException()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Функция себя не оправдывает, int и не собирается возвращать :)
Лучше вернуть int в конце, но чтобы компилятор не ругался, добавить строчку, выполнение которой вызовет исключение (типо деления на ноль)

{
throw exceptionSuppiler;
supplier = null;
GC.Collect();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Помнить про существование и работу сборщика мусора, конечно, важно, но время его вызова стоит оставлять на усмотрение рантайма

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Как?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Просто явно через GC.Collect(); его не вызывать, среда выполнения .NET сама это сделает, когда посчитает нужным

supplier = null;
if (supplier == null)
{
resultSuppiler = default;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нагляднее будет просто null написать. default для nullable типа в точности null и вернет

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

И resultSupplier при инициализации и так null-ом станет, нет смысла его еще раз обнулять

arrayThreads[i] = new Thread(() =>
{
Assert.That(result, Is.EqualTo(multiThreadLazy.Get()));
Assert.That(Equals(multiThreadLazy.Get(), 1));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Есть Assert.That и Assert.Equals. Надо что-то одно выбрать.
Например, Assert.That(multiThreadLazy.Get(), Is.EqualTo(1));

arrayThreads[i] = new Thread(() =>
{
Assert.That(correctResult, Is.EqualTo(multiThreadLazy.Get()));
Assert.That(Equals(multiThreadLazy.Get(), 1));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Аналогично

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert.That в разных потоках -- не исправлено.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хорошо, а как получить значения из потоков и класть их в массив?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Пример с презентации

@@ -22,6 +22,7 @@ public int FunctionForLazyWithCounter()
/// <returns></returns>
public int FunctionWithInvalidOperationException()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Функция теперь свое название не оправдывает, exception другой.
И точно ли нужна отдельная переменная для единицы?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Переменная нужна, т.к. при её отсутствии появляется error и сборка невозможна

Copy link

@YuriUfimtsev YuriUfimtsev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В первом коммите "Часть ревью" исправили Assert.That(Equals(...)), а в следующих двух обратно вернули :)
И имя функции надо сменить. Либо указать DivideByZeroException, либо просто Exception. Но никак не InvalidOperationException

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments