Транзакции / подсказка к 1 задачке

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

Для этого воспользуемся возможностью запускать функции в отдельном потоке с помощью Task.Run.

Возьмем, например, наш обработчик по добавлению котов

private void btnGenerateCats_Click(object sender, EventArgs e)
{
    using (var connection = GetConnection())
    {
        connection.Open();

        pbCatGenerator.Value = 0;
        pbCatGenerator.Maximum = (int)udCount.Value;

        var breeds = new Dictionary<string, TrackBar>
        {
            ["Сибирская"] = tbSiberian,
            ["Бенгальская"] = tbBengalskaya,
            ["Сиамская"] = tbSiamskaya,
            ["Мейн-кун"] = tbMainKun,
            ["Египетская"] = tbEgipetskaya,
            ["Персидская"] = tbPersidkaya,
            ["Манчкин"] = tbManchkin,
            ["Шоколадный йорк"] = tbShocoladniyYork,
        };

        int totalBreed = breeds.Values.Sum(tb => tb.Value);

        Dictionary<string, Range> breedsProbablity = new();
        double previousProbabilty = 0;
        foreach (var item in breeds)
        {
            double k = (double)breeds[item.Key].Value / totalBreed;
            if (k > 0)
            {
                breedsProbablity[item.Key] = new Range
                {
                    min = previousProbabilty,
                    max = previousProbabilty + k
                };

                previousProbabilty += k;
            }
        }

        Random rnd = new();
        for (var i = 0; i < udCount.Value; ++i)
        {
            string breed = null;
            var k = rnd.NextDouble(); // бросаем случайное число от 0 до 1

            foreach (var item in breedsProbablity)
            {
                if (item.Value.CheckContains(k))
                {
                    breed = item.Key; 
                    break;
                }
            }

            if (breed == null)
                continue;

            var isMale = ((double)tbCatMaleFemale.Value / 100) < rnd.NextDouble();
            var gender = isMale ? "муж" : "жен";
            var name = GenerateCatName(gender);

            SqlCommand command = new($@"
INSERT INTO Cats(name, breed) 
VALUES('{name}', '{breed}')"
, connection);
            command.ExecuteNonQuery();

            pbCatGenerator.Value++;
        }
    }
}

давайте вынесем его в функцию.

// создал функцию
private void GenerateCats()
{
    using (var connection = GetConnection())
    {
        connection.Open();

        pbCatGenerator.Value = 0;
        pbCatGenerator.Maximum = (int)udCount.Value;

        var breeds = new Dictionary<string, TrackBar>
        {
            ["Сибирская"] = tbSiberian,
            ["Бенгальская"] = tbBengalskaya,
            ["Сиамская"] = tbSiamskaya,
            ["Мейн-кун"] = tbMainKun,
            ["Египетская"] = tbEgipetskaya,
            ["Персидская"] = tbPersidkaya,
            ["Манчкин"] = tbManchkin,
            ["Шоколадный йорк"] = tbShocoladniyYork,
        };

        int totalBreed = breeds.Values.Sum(tb => tb.Value);

        Dictionary<string, Range> breedsProbablity = new();
        double previousProbabilty = 0;
        foreach (var item in breeds)
        {
            // ...
        }

        Random rnd = new();
        for (var i = 0; i < udCount.Value; ++i)
        {
            // ...

            pbCatGenerator.Value++;
        }
    }
}

// в клике на кнопку теперь вызываем функцию
private void btnGenerateCats_Click(object sender, EventArgs e)
{
    GenerateCats(); // теперь вызываем тут функцию
}

чтобы функция запустилась асинхронно ее надо обернуть в Task.Run, делается это так:

private void btnGenerateCats_Click(object sender, EventArgs e)
{
    Task.Run(() =>
    {
        GenerateCats();
    });
}

Правда, если ее теперь попробовать запустить, то получим ошибку

собственно, проблема, что мы внутри асинхронной функции пытаемся обратится к объекту на форме, обращение к которому всегда должно быть синхронным.

Соответственно надо все операции по взаимодействию с формой надо вынести из функции. То есть своего рода подготовить данные для генерации, а потом передать их уже функции GenerateCats в качестве параметров.

Сначала вынесем функцию GetConnection() из GenerateCats, вот так:

private void GenerateCats(SqlConnection connection) // добавил параметр
{
    using (connection) // тут теперь просто передаю переменную в using
    {
        connection.Open();

        // ...
    }
}

private void btnGenerateCats_Click(object sender, EventArgs e)
{
    var connection = GetConnection(); // создаю соединение перед асинхронным вызовом
    Task.Run(() =>
    {
        GenerateCats(connection); // передаю его в функцию
    });
}

попробуем еще раз запустить:

теперь получаем ошибку на обнуление прогрессбара. Вынесем обнуление как и создание конекшена перед Task.Run

private void GenerateCats(SqlConnection connection) 
{
    using (connection) 
    {
        connection.Open();

        /* УБРАЛ
        pbCatGenerator.Value = 0;
        pbCatGenerator.Maximum = (int)udCount.Value;
        */

        // ...
    }
}

private void btnGenerateCats_Click(object sender, EventArgs e)
{
    var connection = GetConnection();
    // добавил
    pbCatGenerator.Value = 0;
    pbCatGenerator.Maximum = (int)udCount.Value;

    Task.Run(() =>
    {
        GenerateCats(connection);
    });
}

запускаем еще раз:

теперь ошибка на расчёт вероятности выбора пароды. Нам по идее все эти подсчеты тоже надо вынести из функции и просто передавать словарик в качестве результата.

private void GenerateCats(SqlConnection connection, Dictionary<string, Range> breedsProbablity) // добавил параметр
{
    using (connection) 
    {
        connection.Open();

        /* УБИРАЕМ
        var breeds = new Dictionary<string, TrackBar>
        {
            ["Сибирская"] = tbSiberian,
            ["Бенгальская"] = tbBengalskaya,
            ["Сиамская"] = tbSiamskaya,
            ["Мейн-кун"] = tbMainKun,
            ["Египетская"] = tbEgipetskaya,
            ["Персидская"] = tbPersidkaya,
            ["Манчкин"] = tbManchkin,
            ["Шоколадный йорк"] = tbShocoladniyYork,
        };

        int totalBreed = breeds.Values.Sum(tb => tb.Value);

        Dictionary<string, Range> breedsProbablity = new();
        double previousProbabilty = 0;
        foreach (var item in breeds)
        {
            // ...
        }
        */

        Random rnd = new();
        for (var i = 0; i < udCount.Value; ++i)
        {
            // ...

            pbCatGenerator.Value++;
        }
    }
}

private void btnGenerateCats_Click(object sender, EventArgs e)
{
    var connection = GetConnection();
    pbCatGenerator.Value = 0;
    pbCatGenerator.Maximum = (int)udCount.Value;

    // все связанное с расчетом вероятности утащил сюда
    // НАЧАЛО расчета вероятности
    var breeds = new Dictionary<string, TrackBar>
    {
        ["Сибирская"] = tbSiberian,
        ["Бенгальская"] = tbBengalskaya,
        ["Сиамская"] = tbSiamskaya,
        ["Мейн-кун"] = tbMainKun,
        ["Египетская"] = tbEgipetskaya,
        ["Персидская"] = tbPersidkaya,
        ["Манчкин"] = tbManchkin,
        ["Шоколадный йорк"] = tbShocoladniyYork,
    };

    int totalBreed = breeds.Values.Sum(tb => tb.Value);

    Dictionary<string, Range> breedsProbablity = new();
    double previousProbabilty = 0;
    foreach (var item in breeds)
    {
        double k = (double)breeds[item.Key].Value / totalBreed;
        if (k > 0)
        {
            breedsProbablity[item.Key] = new Range
            {
                min = previousProbabilty,
                max = previousProbabilty + k
            };

            previousProbabilty += k;
        }
    }
    // КОНЕЦ расчета вероятности

    Task.Run(() =>
    {
        // передаю теперь сюда breedsProbablity
        GenerateCats(connection, breedsProbablity);
    });
}

пробуем запустить еще раз. Очередная ошибка

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

и если первые два с ними в принципе понятно, что делать, так как это просто значения, то с pbCatGenerator сложнее, так как это оповещение о прогрессе.

Но о нем позже. Как обычно создаем два новых параметра и подготавливаем значения в функции клика

private void GenerateCats(
    SqlConnection connection, 
    Dictionary<string, Range> breedsProbablity, 
    int count, // добавили параметр по количество
    int maleFemale // добавили параметр под долю котов/кошек
)
{
    using (connection) // тут теперь просто передаю переменную в using
    {
        connection.Open();

        Random rnd = new();

        for (var i = 0; i < count; ++i) // тут теперь count использую
        {
            // ...

            var isMale = ((double)maleFemale / 100) < rnd.NextDouble(); // тут теперь maleFemale использую
            var gender = isMale ? "муж" : "жен";
            var name = GenerateCatName(gender);

            SqlCommand command = new($@"
INSERT INTO Cats(name, breed) 
VALUES('{name}', '{breed}')"
, connection);
            command.ExecuteNonQuery();

            pbCatGenerator.Value++;
        }
    }
}

private void btnGenerateCats_Click(object sender, EventArgs e)
{
    var connection = GetConnection();
    pbCatGenerator.Value = 0;
    pbCatGenerator.Maximum = (int)udCount.Value;

    var breeds = new Dictionary<string, TrackBar>
    {
        // ...
    };

    int totalBreed = breeds.Values.Sum(tb => tb.Value);

    Dictionary<string, Range> breedsProbablity = new();
    double previousProbabilty = 0;
    foreach (var item in breeds)
    {
        // ...
    }

    // считываю значения с формы
    int count = (int)udCount.Value;
    int maleFemale = (int)tbCatMaleFemale.Value;

    Task.Run(() =>
    {
        // и передаю теперь сюда count и maleFemale
        GenerateCats(connection, breedsProbablity, count, maleFemale);
    });
}

запускаем:

Ура дошли до последней строки. Собственно оповещение о прогрессе.

Тут проблема в том, что мы из асинхронной функции должны обратится к интерфейсу. Специально для таких целей в C# есть специальный класс Progress, через экземпляр которого можно пробрасывать оповещения в другой поток (интерфейс всегда выполняется в главном потоке приложения). Работать с ним достаточно просто.

private void GenerateCats(
    SqlConnection connection, 
    Dictionary<string, Range> breedsProbablity, 
    int count, 
    int maleFemale,
    IProgress<int> progress // добавил поле для оповещения о прогрессе
)
{
    using (connection) // тут теперь просто передаю переменную в using
    {
        connection.Open();

        Random rnd = new();

        for (var i = 0; i < count; ++i)
        {
            // ...

            // УБИРАЕМ pbCatGenerator.Value++;
            // добавляем вызов функции с помощью которого оповестим об изменении
            // вот это самое i попадет в value лямбда-фунуции которая будет ниже
            progress?.Report(i + 1);
        }
    }
}

private void btnGenerateCats_Click(object sender, EventArgs e)
{
    // ...
    
    // создаем экземпляр оповещателя об изменении
    // передаем лямбда выражение в качестве параметра,
    // которое как раз будет использоваться в качестве окошка между 
    // асинхронной функцией и интерфейсом
    var progress = new Progress<int>(value =>
    {
        // меняем значение прогрессбара
        pbCatGenerator.Value = value;
    });

    Task.Run(() =>
    {
        // передаю теперь сюда еще и progress
        GenerateCats(connection, breedsProbablity, count, maleFemale, progress);
    });
}

проверяем

ура! =)

И кстати интерфейс теперь не блокируется. В принципе мы можем даже тыкнуть на кнопку запустить дважды.

Блокируем кнопку с помощью async/await

Давайте чтобы такого не случилось будем ее блокировать.

Но сначала для наглядности запихаем в асинхронную функцию, небольшую паузу между добавлениями записей. Ну типа добавил одну запись, приостановись на 300мс.

private void GenerateCats(
    SqlConnection connection, 
    Dictionary<string, Range> breedsProbablity, 
    int count, 
    int maleFemale,
    IProgress<int> progress
)
{
    using (connection) // тут теперь просто передаю переменную в using
    {
        connection.Open();

        Random rnd = new();

        for (var i = 0; i < count; ++i)
        {
            // ...

            progress?.Report(i + 1);

            Thread.Sleep(300); // добавил, не забудьте вверху добавить using System.Threading
        }
    }
}

проверяем:

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

Чтобы такого не происходило надо перед запуском будем блочить кнопку. У меня кнопка называется btnGenerateCats

private void btnGenerateCats_Click(object sender, EventArgs e)
{
    // ...

    // блочим кнопку
    btnGenerateCats.Enabled = false;

    Task.Run(() =>
    {
        GenerateCats(connection, breedsProbablity, count, maleFemale, progress);
    });
    
    // снимаем блок с кнопки
    btnGenerateCats.Enabled = true;
}

хм, чего-то не блочится

тут дело в том, что у нас код работает так

// заблочь кнопку
btnGenerateCats.Enabled = false;

// запусти процесс в отдельном потоке
Task.Run(() =>
{
    GenerateCats(connection, breedsProbablity, count, maleFemale, progress);
});

// и сразу разблочь кнопку
btnGenerateCats.Enabled = true;

чтобы разблокировка кнопку произошла только после выполнения процесса в C# встроен механизм async/await, который позволяет прервать выполнение функции и вернуться к ней по завершению какого-нибудь процесса.

Делается это так.

// помечаем функцию как асинхронную добавив async
private async void btnGenerateCats_Click(object sender, EventArgs e)
{
    // ...
    
    // блокируем кнопку
    btnGenerateCats.Enabled = false;
    
    // тут добавляем ключевое слово await, это значит, что запустится генерацию
    // C# выйдет из этой функции и вернется продолжать выполнять код в ней только когда генерация закончится
    await Task.Run(() =>
    {
        GenerateCats(connection, breedsProbablity, count, maleFemale, progress);
    });
    
    // то есть теперь разблокировка сработает только после завершения генерации
    btnGenerateCats.Enabled = true;
}

пробуем:

ура, кнопка заблокировалась! =)

Добавлям отмену генерации

А давайте сделаем еще лучше. Вместо блокирования кнопки, будем писать на ней отмена. И при клике на нее, прерывать генерацию.

// создаем поле под токен-отмены, по умолчанию пусть он равен null
CancellationTokenSource catGeneratorCanceletionToken = null;

private void GenerateCats(
    SqlConnection connection, 
    Dictionary<string, Range> breedsProbablity, 
    int count, 
    int maleFemale,
    IProgress<int> progress,
    // добавляем специальный паарметр-токен 
    // с помощью которого мы сможем определить что был запрос на завершение
    CancellationTokenSource cancellationToken 
)
{
    using (connection) // тут теперь просто передаю переменную в using
    {
        connection.Open();

        Random rnd = new();
        for (var i = 0; i < count; ++i)
        {
            // ...

            progress?.Report(i + 1);

            // если пришел запрос на завершение
            if (cancellationToken.IsCancellationRequested)
            {
                return; // то выходим их функции
            }

            Thread.Sleep(30);
        }
    }
}


private async void btnGenerateCats_Click(object sender, EventArgs e)
{
    // когда токен не равен нулю значит идет генерацию
    if (catGeneratorCanceletionToken != null)
    {
        // отправляем запрос на отмену генерации
        catGeneratorCanceletionToken.Cancel(); 
        // выходим из функции
        return;
    }
    // если токен равен нулю значит генерация не идет, создаем новый токен
    catGeneratorCanceletionToken = new CancellationTokenSource();

    var connection = GetConnection();
    pbCatGenerator.Value = 0;
    pbCatGenerator.Maximum = (int)udCount.Value;

    // ...

    //btnGenerateCats.Enabled = false; убрал
    btnGenerateCats.Text = "Отмена"; // теперь просто меняю текст кнопки

    await Task.Run(() =>
    {
        GenerateCats(
            connection, 
            breedsProbablity, 
            count, 
            maleFemale, 
            progress,
            catGeneratorCanceletionToken // передаем токен на отмену
        );
    });

    //btnGenerateCats.Enabled = true; убрал
    btnGenerateCats.Text = "Запустить"; возвращаю текст кнопки
    catGeneratorCanceletionToken.Dispose(); // по завершению генерации  очищаем память под токен
    catGeneratorCanceletionToken = null; // и сбрасываем его в null;
}

проверяем:

красота! =)

1

Сделать вызовы методов генерации асинхронными. Реализовать механизм отмены процесса генерации.