Сделать вызовы методов генерации асинхронными. Реализовать механизм отмены процесса генерации.
Транзакции / подсказка к 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;
}
проверяем:
красота! =)