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

37

Во многих местах я видел, что каноническая мудрость 1 заключается в том, что вызывающая сторона несет ответственность за обеспечение того, чтобы вы были в потоке пользовательского интерфейса при обновлении компонентов пользовательского интерфейса (в частности, в Java Swing, что вы находитесь в потоке диспетчеризации событий ) ,

Почему это так? Поток диспетчеризации событий является проблемой представления в MVC / MVP / MVVM; обрабатывать его где угодно, но представление создает тесную связь между реализацией представления и моделью потоков этого представления.

В частности, допустим, у меня есть MVC-приложение, использующее Swing. Если вызывающая сторона отвечает за обновление компонентов в потоке диспетчеризации событий, то, если я пытаюсь поменять свою реализацию Swing View на реализацию JavaFX, я должен изменить весь код Presenter / Controller, чтобы вместо этого использовать поток приложения JavaFX .

Итак, я полагаю, у меня есть два вопроса:

  1. Почему ответственность за обеспечение безопасности потоков компонентов пользовательского интерфейса лежит на вызывающей стороне? Где недостаток в моих рассуждениях выше?
  2. Как я могу спроектировать свое приложение так, чтобы оно не связывало эти проблемы с безопасностью потоков, но при этом оставалось бы соответствующим образом ориентированным на многопотоковое исполнение?

Позвольте мне добавить некоторый Java-код MCVE, чтобы проиллюстрировать, что я подразумеваю как «ответственный за вызывающего абонента» (здесь есть и другие полезные практики, которые я не выполняю, но я стараюсь быть как можно меньше):

Звонящий ответственен:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        view.setData(data);
      }
    });
  }
}
public class View {
  void setData(Data data) {
    component.setText(data.getMessage());
  }
}

Посмотреть ответственность:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    view.setData(data);
  }
}
public class View {
  void setData(Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        component.setText(data.getMessage());
      }
    });
  }
}

1: Автор этого поста имеет наивысшую оценку в Swing на переполнении стека. Он говорит это повсюду, и я также видел, что это ответственность за звонящего в других местах.

durron597
источник
1
Эх, производительность ИМХО. Эти сообщения о событиях не приходят бесплатно, и в нетривиальных приложениях вы бы хотели свести к минимуму их количество (и убедиться, что ни одно из них не слишком велико), но минимизация / конденсация должны быть логически выполнены в докладчике.
Ордос
1
@Ordous Вы все еще можете гарантировать, что количество сообщений будет минимальным, пока вы переносите поток сообщений в представление.
durron597
2
Некоторое время назад я читал действительно хороший блог, в котором обсуждается эта проблема. По сути, он говорит о том, что очень опасно пытаться сделать поток комплекта пользовательского интерфейса безопасным, так как он вводит возможные взаимоблокировки и, в зависимости от того, как он реализован, условия гонки в фреймворк. Существует также соображение производительности. Не так много сейчас, но когда Swing был впервые выпущен, его сильно критиковали за его производительность (была плохая), но это не было на самом деле виной Swing, это было отсутствие у людей знаний о том, как его использовать.
MadProgrammer
1
SWT реализует концепцию безопасности потоков, создавая исключения, если вы нарушаете это, не очень, но, по крайней мере, вы узнали об этом. Вы упомянули о переходе с Swing на JavaFX, но у вас возникла бы эта проблема практически с любой инфраструктурой пользовательского интерфейса, кажется, что Swing является той, которая подчеркивает проблему. Вы могли бы разработать промежуточный уровень (контроллер контроллера?), Задачей которого является обеспечение правильной синхронизации вызовов в пользовательском интерфейсе. Невозможно точно знать, как вы могли бы проектировать свои части API, не относящиеся к пользовательскому интерфейсу, с точки зрения интерфейса UI
MadProgrammer
1
и большинство разработчиков будут жаловаться, что любая защита потоков, реализованная в UI API, была ограничительной или не отвечала их потребностям. Лучше позволить вам решить, как вы хотите решить эту проблему, исходя из своих потребностей
MadProgrammer

Ответы:

22

В конце своего неудавшегося эссе о мечте Грэм Хэмилтон (крупный Java-архитектор) упоминает, что если разработчики «хотят сохранить эквивалентность с моделью очереди событий, им нужно будет следовать различным неочевидным правилам» и иметь видимые и явные Модель очереди событий «кажется, помогает людям более надежно следовать модели и таким образом создавать программы с графическим интерфейсом, которые работают надежно».

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

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

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

Карл Билефельдт
источник
25

Потому что обеспечение безопасности потока GUI-библиотеки - это огромная головная боль и узкое место.

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

Разработать стратегию блокировки, которая не блокирует корневое окно (вызовет много споров), сложно . Блокировка снизу вверх, в то время как другая нить блокируется сверху вниз, является отличным способом достижения мгновенного тупика.

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

чокнутый урод
источник
1
Интересно, что я решаю эту проблему с помощью инфраструктуры очередей для этих обновлений, которые я написал и которая управляет передачей потока.
durron597
2
@ durron597: А у вас никогда не будет обновлений в зависимости от текущего состояния пользовательского интерфейса, на который может повлиять другой поток? Тогда это может сработать.
дедупликатор
Зачем вам нужны вложенные замки? Зачем вам нужно блокировать все корневое окно при работе с деталями в дочернем окне? Порядок блокировок является решаемой проблемой, предоставляя единственный открытый метод для блокировки блокировок мультипликаторов в правильном порядке (либо сверху вниз, либо снизу вверх, но не оставляйте выбор вызывающей стороне)
MSalters
1
@MSalters с root я имел ввиду текущее окно. Чтобы получить все блокировки, которые вам нужно приобрести, вам нужно пройти вверх по иерархии, которая требует блокировки каждого контейнера, когда вы сталкиваетесь с ним, чтобы получить родительский блок и разблокировать (чтобы гарантировать, что вы блокируете только сверху вниз), и затем надеяться, что он не изменился на После того, как вы получите окно рута и сделаете блокировку сверху вниз.
фрик с трещоткой
@ratchetfreak: Если дочерний элемент, который вы пытаетесь заблокировать, удаляется другим потоком, пока вы его блокируете, это просто немного неудачно, но не имеет отношения к блокировке. Вы просто не можете работать с объектом, который только что удалил другой поток. Но почему этот другой поток удаляет объекты / окна, которые ваш поток все еще использует? Это не хорошо в любом сценарии, а не только в интерфейсе пользователя.
MSalters
17

Многопоточность (в модели с общей памятью) - это свойство, которое не поддается абстракции. Простым примером является Set-тип: хотя Contains(..)и Add(...)и Update(...)является совершенно допустимым API в однопотоковом сценарии, многопоточный сценарий нуждается в AddOrUpdate.

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

  1. Инструментарий не может решить эту проблему, поскольку блокировка не гарантирует правильность порядка операций.
  2. Представление может решить проблему, но только если вы разрешите бизнес-правило, согласно которому число в верхней части списка должно соответствовать количеству элементов в списке и обновлять список только через представление. Не совсем то, что MVC должен быть.
  3. Докладчик может решить эту проблему, но ему необходимо знать, что представление имеет особые потребности в отношении потоков.
  4. Привязка данных к модели с поддержкой многопоточности является еще одним вариантом. Но это усложняет модель вещами, которые должны быть связаны с пользовательским интерфейсом.

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

Патрик
источник
Может быть, слой может быть введен между View и Presenter, который также может быть заменен.
durron597
2
.NET имеет System.Windows.Threading.Dispatcherвозможность обрабатывать потоки как в WPF, так и в WinForms. Такой слой между докладчиком и представлением, безусловно, полезен. Но это только обеспечивает независимость инструментария, а не независимость от потоков.
Патрик
9

Некоторое время назад я прочитал действительно хороший блог, в котором обсуждается эта проблема (упомянутая Карлом Билефельдом), и в основном он говорит о том, что очень опасно пытаться сделать поток комплекта пользовательского интерфейса безопасным, так как он вводит возможные взаимоблокировки и зависит от того, как Реализованы условия гонки в рамках.

Существует также соображение производительности. Не так много сейчас, но когда Swing был впервые выпущен, его сильно критиковали за его производительность (была плохая), но это не было на самом деле виной Swing, это было отсутствие у людей знаний о том, как его использовать.

SWT реализует концепцию безопасности потоков, создавая исключения, если вы нарушаете это, не очень, но, по крайней мере, вы узнали об этом.

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

Вы упомянули о переходе с Swing на JavaFX, но у вас возникла бы эта проблема практически с любой инфраструктурой пользовательского интерфейса (не только с толстыми клиентами, но и с веб-интерфейсом), но Swing, кажется, является той, которая подчеркивает эту проблему.

Вы могли бы разработать промежуточный уровень (контроллер контроллера?), Задачей которого является обеспечение правильной синхронизации вызовов в пользовательском интерфейсе. Невозможно точно знать, как вы могли бы проектировать свои части API, не относящиеся к пользовательскому интерфейсу, с точки зрения API пользовательского интерфейса, и большинство разработчиков будут жаловаться, что любая защита потоков, реализованная в API пользовательского интерфейса, была ограничительной или не отвечала их потребностям. Лучше позволить вам решить, как вы хотите решить эту проблему, исходя из своих потребностей

Одна из самых больших проблем, которую вам необходимо рассмотреть, - это возможность обосновать данный порядок событий на основе известных входных данных. Например, если пользователь изменяет размер окна, модель очереди событий гарантирует, что будет происходить заданный порядок событий, это может показаться простым, но если очередь допускает события, запускаемые другими потоками, вы больше не можете гарантировать порядок в какие события могут произойти (состояние гонки), и внезапно вы должны начать беспокоиться о разных состояниях и ничего не делать, пока не произойдет что-то другое, и вам не придется делиться флагами состояний вокруг, и в итоге вы получите спагетти.

Хорошо, вы могли бы решить эту проблему, имея какую-то очередь, которая упорядочивала события в зависимости от времени их выпуска, но разве это не то, что у нас уже есть? Кроме того, вы все еще не можете гарантировать, что поток B будет генерировать свои события ПОСЛЕ потока A

Основная причина, по которой люди расстраиваются из-за того, что им приходится думать о своем коде, заключается в том, что их заставляют думать о своем коде / дизайне. "Почему это не может быть проще?" Это не может быть проще, потому что это не простая проблема.

Я помню, когда была выпущена PS3, и Sony активно рассказывала о процессоре Cell и о его способности выполнять отдельные строки логики, декодировать аудио, видео, загружать и разрешать данные модели. Один из разработчиков игр спросил: «Это все круто, но как вы синхронизируете потоки?»

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

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

Теперь, имея возможность переключать фреймворки, это непросто спроектировать, НО, на мгновение возьмем MVC, MVC может быть многоуровневым, то есть у вас может быть MVC, который непосредственно связан с управлением фреймворком UI, вы затем можно обернуть это, опять же, в MVC более высокого уровня, который имеет дело с взаимодействиями с другими (потенциально многопоточными) структурами, этот уровень будет обязан определять, как нижний уровень MVC уведомляется / обновляется.

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

MadProgrammer
источник
2
У вас не было бы этой проблемы с сетью. JavaScript намеренно не поддерживает многопоточность - среда выполнения JavaScript фактически представляет собой одну большую очередь событий. (Да, я знаю, что, строго говоря, у JS есть WebWorkers - это неродные темы, которые ведут себя как актеры на других языках).
James_pic
1
@James_pic То, что у вас есть на самом деле, это часть браузера, выступающая в качестве синхронизатора для очереди событий, о чем мы в основном говорили, вызывающая
сторона
Да, точно. Основное различие в контексте Интернета заключается в том, что эта синхронизация происходит независимо от кода вызывающего, поскольку среда выполнения не предоставляет механизма, который позволял бы выполнять код вне очереди событий. Таким образом, абонент не должен брать на себя ответственность за это. Я считаю, что это также большая часть мотивации разработки NodeJS - если среда выполнения представляет собой цикл обработки событий, то весь код по умолчанию поддерживает цикл обработки событий.
James_pic
1
Тем не менее, существуют рамки пользовательского интерфейса браузера, которые имеют свой собственный цикл обработки событий внутри цикла (я смотрю на вас, Angular). Поскольку эти платформы больше не защищены средой выполнения, вызывающие стороны также должны убедиться, что код выполняется внутри цикла обработки событий, аналогично многопоточному коду в других платформах.
James_pic
Будет ли какая-то конкретная проблема с наличием элементов управления «только для отображения», автоматически обеспечивающих безопасность потока? Если у вас есть редактируемый текстовый элемент управления, который записывается одним потоком и читается другим, нет хорошего способа сделать запись видимой для потока, не синхронизируя хотя бы одно из этих действий с фактическим состоянием пользовательского интерфейса элемента управления, но будут ли такие проблемы иметь значение только для элементов управления отображением? Я думаю, что они могут легко обеспечить любую необходимую блокировку или блокировки для операций, выполняемых над ними, и позволить вызывающей стороне игнорировать время, когда ...
суперкат