Как отменить глубоко вложенный процесс
У меня есть класс, который является классом "менеджер". Одна из его функций - сигнализировать о том, что длительный процесс класса должен завершиться. Это делается путем установки логического значения "IsStopping" в классе.
public class Foo
{
bool isStoping
void DoWork() {
while (!isStopping)
{
// do work...
}
}
}
Теперь DoWork() была гигантской функцией, и я решил ее реорганизовать, и как часть процесса он разбил некоторые из них на другие классы. Проблема в том, что некоторые из этих классов также имеют долго выполняющиеся функции, которые должны проверять, является ли isStopping истинным.
public class Foo
{
bool isStoping
void DoWork() {
while (!isStopping)
{
MoreWork mw = new MoreWork()
mw.DoMoreWork() // possibly long running
// do work...
}
}
}
Какие у меня варианты здесь?
Я рассмотрел передачу isStopping по ссылке, которая мне не очень нравится, потому что она требует наличия внешнего объекта. Я бы предпочел сделать дополнительные занятия как можно более самостоятельными и свободными от зависимости.
Я также подумал о создании свойства isStopping, а затем о том, чтобы оно вызывало событие, на которое могут быть подписаны внутренние классы, но это кажется слишком сложным.
Другой вариант состоял в том, чтобы создать класс "Маркер отмены процесса", аналогичный тому, который используют задачи.net 4, затем этот токен был передан этим классам.
Как вы справились с этой ситуацией?
РЕДАКТИРОВАТЬ:
Также учтите, что MoreWork может иметь объект EvenMoreWork, который он создает и вызывает потенциально долго работающий метод... и так далее. Я думаю, что я ищу способ уметь сигнализировать произвольное количество объектов по дереву вызовов, чтобы сказать им прекратить то, что они делают, очистить и вернуть.
EDIT2:
Спасибо за ответы до сих пор. Похоже, что нет единого мнения о методах использования, и у каждого свое мнение. Похоже, это должен быть шаблон дизайна...
6 ответов
Вы можете пойти двумя путями здесь:
1) Решение, которое вы уже обрисовали в общих чертах: передать сигнальный механизм подчиненным объектам: bool (по ссылке), сам родительский объект скрыт в интерфейсе (Foo: IController
в приведенном ниже примере), или что-то еще. Дочерние объекты проверяют сигнал по мере необходимости.
// Either in the MoreWork constructor
public MoreWork(IController controller) {
this.controller = controller;
}
// Or in DoMoreWork, depending on your preferences
public void DoMoreWork(IController controller) {
do {
// More work here
} while (!controller.IsStopping);
}
2) Переверните его и используйте шаблон наблюдателя - который позволит вам отделить подчиненные объекты от родителя. Если бы я делал это вручную (вместо использования событий), я бы изменил свои подчиненные классы, чтобы реализовать IStoppable
интерфейс, и заставьте мой класс менеджера сказать им, когда остановиться:
public interface IStoppable {
void Stop();
}
public class MoreWork: IStoppable {
bool isStopping = false;
public void Stop() { isStopping = true; }
public void DoMoreWork() {
do {
// More work here
} while (!isStopping);
}
}
Foo
поддерживает список своих остановок и в своем собственном методе stop останавливает их все:
public void Stop() {
this.isStopping = true;
foreach(IStoppable stoppable in stoppables) {
stoppable.Stop();
}
}
Я думаю, что запуск события, на которое подписываются ваши подклассы, имеет смысл.
Засоряйте ваш код такими утверждениями везде, где наиболее разумно проверить флаг остановки:
if(isStopping) { throw new OperationCanceledException(); }
Ловить OperationCanceledException
прямо на верхнем уровне.
Для этого не существует реального снижения производительности, потому что (а) это случается не очень часто, и (б) когда это происходит, это происходит только один раз.
Этот метод также хорошо работает в сочетании с WinForms BackgroundWorker
составная часть. Рабочий автоматически поймает выброшенное исключение в рабочем потоке и перенаправит его обратно в поток пользовательского интерфейса. Вам просто нужно проверить тип e.Error
свойство, например:
private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
if(e.Error == null) {
// Finished
} else if(e.Error is OperationCanceledException) {
// Cancelled
} else {
// Genuine error - maybe display some UI?
}
}
Вложенные типы могут принять делегата (или выставить событие) для проверки условия отмены. Затем ваш менеджер предоставляет делегат вложенным типам, который проверяет свой собственный логический "shouldStop". Таким образом, единственная зависимость - это ManagerType от NestedType, который у вас уже был.
class NestedType
{
// note: the argument of Predicate<T> is not used,
// you could create a new delegate type that accepts no arguments
// and returns T
public Predicate<bool> ShouldStop = delegate() { return false; };
public void DoWork()
{
while (!this.ShouldStop(false))
{
// do work here
}
}
}
class ManagerType
{
private bool shouldStop = false;
private bool checkShouldStop(bool ignored)
{
return shouldStop;
}
public void ManageStuff()
{
NestedType nestedType = new NestedType();
nestedType.ShouldStop = checkShouldStop;
nestedType.DoWork();
}
}
Вы можете абстрагировать это поведение в интерфейс, если вы действительно этого хотите.
interface IStoppable
{
Predicate<bool> ShouldStop;
}
Кроме того, вместо того, чтобы просто проверять логическое значение, у вас может быть механизм "стоп", выдающий исключение. В методе checkShouldStop менеджера он может просто OperationCanceledException
:
class NestedType
{
public MethodInvoker Stop = delegate() { };
public void DoWork()
{
while (true)
{
Stop();
// do work here
}
}
}
class ManagerType
{
private bool shouldStop = false;
private void checkShouldStop()
{
if (this.shouldStop) { throw new OperationCanceledException(); }
}
public void ManageStuff()
{
NestedType nestedType = new NestedType();
nestedType.Stop = checkShouldStop;
nestedType.DoWork();
}
}
Я использовал эту технику раньше и считаю ее очень эффективной.
Вы можете создать метод Cancel() в вашем классе менеджера и в каждом из ваших других рабочих классов. Основывайте это на интерфейсе.
Класс менеджера или классы, которые создают экземпляры других рабочих классов, должны будут передавать вызов Cancel() объектам, из которых они состоят.
Затем самые глубокие вложенные классы просто установят для внутреннего _isStopping bool значение false, и ваши долгосрочные задачи будут проверять это.
В качестве альтернативы вы можете создать некоторый контекст, о котором знают все классы, и где они могут проверять наличие отмененного флага.
Другой вариант состоял в том, чтобы создать класс "Маркер отмены процесса", аналогичный тому, который используют задачи.net 4, затем этот токен был передан этим классам.
Я не знаком с этим, но если это в основном объект с флагом свойства bool и который вы передаете в каждый класс, то мне это кажется самым чистым способом. Затем вы можете создать абстрактный базовый класс, который имеет конструктор, который принимает это и устанавливает его в качестве закрытой переменной-члена. Тогда ваши циклы процесса могут просто проверить это для отмены. Очевидно, вам нужно будет сохранить ссылку на этот объект, который вы передали своим работникам, чтобы его флаг bool мог быть установлен на нем из вашего пользовательского интерфейса.
Вы можете сгладить свой стек вызовов, поворачивая каждый DoWork()
вызов команды с использованием шаблона Command. На верхнем уровне вы поддерживаете очередь команд для выполнения (или стек, в зависимости от того, как ваши команды взаимодействуют друг с другом). "Вызов" функции преобразуется в постановку новой команды в очередь. Затем, между обработкой каждой команды, вы можете проверить, отменять или нет. Подобно:
void DoWork() {
var commands = new Queue<ICommand>();
commands.Enqueue(new MoreWorkCommand());
while (!isStopping && !commands.IsEmpty)
{
commands.Deque().Perform(commands);
}
}
public class MoreWorkCommand : ICommand {
public void Perform(Queue<ICommand> commands) {
commands.Enqueue(new DoMoreWorkCommand());
}
}
По сути, превращая низкоуровневый стек вызовов в структуру данных, которой вы управляете, вы можете проверять вещи между каждым "вызовом", паузой, возобновлением, отменой и т. Д.