LINUX.ORG.RU

tcl/tk - философский вопрос про grab/busy/заморозку

 , ,


1

2

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

1. Ничего не мелькало.

2. Можно было отменить заморозку по волшебной кнопке.

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

★★★★★

Последнее исправление: den73 (всего исправлений: 1)

Often, the tk busy command can be used instead of Tk's grab command. Unlike grab which restricts all user interactions to one widget, with the tk busy command you can have more than one widget active (for example, a “Cancel” dialog and a “Help” button).

anonymous
()
Ответ на: комментарий от anonymous

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

С busy проблема в том, что она не работает под какой-то там аквой в макинтоше. Я эту AQUA в глаза не видел и макинтош мне вовсе не упёрся, но всё же ограниченная переносимость подрывает саму идею tcl/tk.

den73 ★★★★★
() автор топика

А надо ли вообще замораживать GUI? Если сделать отдельный поток: надо показать список локальных переменных - вызываешь процедуру во втором потоке, сообщая ей 2 параметра - скрипт запроса к серверу и скрипт отрисовки полученного результата в основном потоке (GUI). GUI не заморожен, а когда придут результаты - параллельная процедура отрисует их как задано и завершится. Или я не так понял?

anonymous
()
Ответ на: комментарий от anonymous

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

Я уже налетел на то, что событие раскрытия поддерева в определённых условиях проходит дважды, от этого лезут косяки: либо дублирование данных в поддереве, либо исчезновение курсора выделения. Пришлось разруливать это, создавать таблицу функций-продолжений, ожидающих раскрытия данного поддерева и усложнять логику почти до порога непонимания. Это только то, что мне попалось за время лёгкого тестирования. Сколько там грабель ещё впереди? Не вижу возможности обезпечить нормальную работу приложения в такой обстановке. Т.е., пока вроде работает, но кто знает, что будет завтра?

Заморозка гуя выглядит в таком случае дешёвым и вполне приемлемым решением, поскольку она будет длиться сотые доли секунды. Я бы совсем заморозил, но кнопка «отменить процесс» тоже нужна.

Отсюда и вопрос.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Заморозишь гуй, но событие-то от мышки никуда не денется, просто встанет в очередь обработки. И когда гуй разморозится, все что ты там накликал, вывалится на дерево, то есть от дублирования не спасет.

Как вариант, можно попробовать НА ВРЕМЯ «заморозки» не замораживать гуй, а перехватывать опасные события к окну с помощью bind. Чтобы все, что ты накликал во время обработки предыдущего события, исчезло (ну или в какой-то список записать, чтобы потом выполнить, как хочешь). Не забыть скрипт обработки завернуть в catch, а то грохнется посередине, а перехват в никуда так и останется...

anonymous
()
Ответ на: комментарий от anonymous

Заморозишь гуй, но событие-то от мышки никуда не денется,

Вообще это нормально. Нехрен кликать почём зря. Хотя по-хорошему в этом случае среда должна показать, что она занята.

Вообще я думал, что где-то в красном углу на время ожидания должна появляться красная лампочка, типа «занят я». С другой стороны, она должна появляться не сразу, чтобы зря не мерцать. А хотя бы через несколько десятых секунды, пока пользователь не успеет сообразить, что что-то пошло не так. Можно и на нажатие мыши включение лампочки навесить, хотя полезность этого спорна. Собственно, тогда вопрос - как это воплотить с помощью grab или busy, чтобы больше ничего не мерцало.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

У тебя сервер отвечает асинхронно?

1. Посылаем серверу запрос на список локальных переменных, сообщая ID узла, где хотим отрисовать, например: SendCommand [list GetLocalVars $node $some_data]. И забываем об этом, ничего не замораживаем.

2. Когда приходит ответ вида {<node> <some data>}, то в catch-обертке идем на узел $node (если он есть), обновляем подузлы (либо удаляем все подузлы и создаем заново, либо сравниваем имеющиеся с новым списком и корректируем - по вкусу).

anonymous
()
Ответ на: комментарий от anonymous

И забываем об этом, ничего не замораживаем.

Это, если я правильно знаю термин, требует переписывания всего кода в continuation passing style. Конкретно у меня отладчик, в нём стек. В принципе система может рухнуть при открытии любого кадра стека, поэтому открывать их все сразу не совсем хорошо.

Когда пользователь начинает поиск, я делаю следующую последовательность действий:

a1 получить данные о локалах первого кадра стека с сервера
a2 заселить ноду дерева
a3 искать в этой ноде
a4 если нашли, встать туда.
a5 если не нашли - повторить для следующих
То же самое, если я просто хочу открыть ноду, то:
b1 получить данные о локалах первого кадра стека с сервера
b2 заселить ноду дерева
b3 развернуть ноду
Начнём с того, что если b1 асинхронен, то сценарий b вообще может сломаться. По хорошему я должен перебить открытие ноды и сделать его асинхронным. Тогда сломается любой код, предполагающий открытие ноды синхронным:
c1 открыть ноду
# c2 фактически произойдёт до раскрытия ноды
c2 сделать ещё-что-то
Этот код должен быть изменён. Но я не имею доступа к перечню этого кода, поскольку он может быть внутри самой реализации tablelist. В документации не задекларирован механизм асинхронного раскрытия ноды (хотя весь мануал я не осилил, но думаю, что всё же там этого нет).

Далее, при асинхронности я должен пп 2-5 завернуть в continuation и повесить его на возврат события из п.1. Я не говорю о том, что такой код сложнее. Начинаются ещё и чудеса, когда накладываются друг на друга сценарии a и b.

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

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от den73

Философия тут в том, что тебе нужна определенная последовательность операций, но часть операций асинхронные и ты пытаешься опять их превратить в синхронные. Какие приходят в голову варианты: 1. Перевести все обращения к серверу на синхронные. 2. Сделать 2 интерфейса к серверу - синхронный и асинхронный. Например, при раскрытии узла при поиске пользоваться первым, от мышки - вторым. Наверное, проще всего. 3. Дурная задумка, что при поиске у тебя логика на клиенте перемешана с логикой на сервере, при том, что эта операция как бы должна быть «атомарной». Ты же вроде намеревался на клиенте только GUI держать, можно перенести весь алгоритм поиска на сервер? Отправляй ему запрос Find $узел $что_нужно_в_нем_найти и пусть он возвращает полный путь до искомых данных в виде вложенного списка с узлами и листьями, начиная от заданного узла, который останется только разом отрисовать. Это можно сделать асинхронно. 4. Можно избавиться от продолжений, если при раскрытии узла отправлять серверу асинхронный запрос, указывая дополнительно скрипт, который должен быть выполнен на клиенте. Пусть сервер при ответе возвращает этот скрипт. В нем прописать шаги a3-a5 (если это поиск). Получили ответ, заполнили ноду, запустили присланный скрипт - рекурсия. Если просто мышкой раскрываем узел, скрипт отправляем пустой. В случае с в дополнительный скрипт вписываем шаг с2. Очень по tcl-овски :)

anonymous
()
Ответ на: комментарий от anonymous

Вот для этого и надо регистрироваться :) п.1. невозможен.

1. Перевести все обращения к серверу на синхронные.

Невозможно.

(defun f (x) (dotimes (i 5) (sleep 0.5) (print i)))
печать происходит асинхронно. А можно было ещё это в отдельном треде запустить.

2. Сделать 2 интерфейса к серверу - синхронный и асинхронный. Например, при раскрытии узла при поиске пользоваться первым, от мышки - вторым. Наверное, проще всего.

п.2 я по сути и реализовал, но быстро понял простую вещь: синхронный режим будет один на всю среду и она должна зависать целиком, со всеми окнами. Т.е. отладка происходящего внутри синхронного режима будет крайне затруднена.

п.3. Ты же вроде намеревался на клиенте только GUI держать, можно перенести весь алгоритм поиска на сервер?

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

4. Можно избавиться от продолжений, если при раскрытии узла отправлять серверу асинхронный запрос, указывая дополнительно скрипт, который должен быть выполнен на клиенте.

Это не избавление, а лишь переоформление.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Значит опять возвращаемся к вопросу о корректной блокировке клиента.

Ладно. План Б.

перед отправкой:

{
$my_tablelist configure -state disable

# на случай зависания сервера
after 1000 "$my_tablelist configure -state normal"
}
по приходу ответа, естественно, разблокируем.

anonymous
()
Ответ на: комментарий от anonymous

Я вот что понял: мы упираемся в две вещи. Если делаем жёстко синхронный режим, то должны висеть на сокете. Т.е. среда зависнет при долгом молчании сервера. Это недопустимо, т.к. чревато падением среды и потерей несохранённых файлов. Среда падать не должна. А раз мы не висим на сокете, то цикл обработки событий неизбежно работает и мы не сможем гарантировать синхронность между запросом на список листьев и возвратом из запроса. Т.е. сценарий b в том, что я написал (кстати, код здесь) не должен работать (хотя на вид работает).

Поэтому можно такой вариант сделать: У поддерева есть 4 состояния: неизвестно, ждём, заполняем, заполнили.

Первое состояние - «неизвестно». При запросе списка листьев вставляем лист со словом «ждите» и ставим состояние «ждём». Возвращаем управление - пусть tablelist раскроет список и покажет этот элемент. Не вставить элемент «ждите» нельзя, поскольку исчезнет треугольничек. В то же время асинхронно отправляем запрос на список листьев на сервер. По приходу списка листьев с сервера ставим состояние поддерева «заполняем», удаляем лист со словом «ждите» и заполняем настоящими данными. Ставим состояние «заполнено». Здесь мы всё делаем синхронно и проблем возникнуть не должно. Если список листьев пришёл, а состояние уже «заполняем» или «заполнено», то выбрасываем этот список. Слава Богу, у нас локалы должны каждый раз быть одинаковыми.

Это был сценарий b.

Сценарий a всё же представляет трудность. Но можно бросить мультибумеранг, а именно ставить на after idle (или во вторую очередь, или через таймер) скрипт следующего содержания:

if {поддерево $x заполнено} {
  искать в нём
} else {
  after idle тот-же-скрипт
}

Тогда поиск зависнет до момента заполнения дерева, и при этом вся система не будет висеть.

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

Верно я мыслю?

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от den73

Неверно. Т.е. для данного случая это прокатит, а вообще - нет. Например, если есть команда «развернуть все ветви дерева», то она развернёт их все до показа сообщения «ждите», а реальные листья, которые могут быть не листьями, а поддеревьями, придут уже потом и в итоге останутся неразвёрнутыми.

Наверное, единственное, это делать vwait внутри процедуры, хотя это считается злом:

http://wiki.tcl.tk/1255

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Ладно. Волевое решение: забить на эту проблему, потому что пока поиск вроде бы работает.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

У тебя есть команда развернуть все ветви дерева? «Ждите» это особый случай, это считается «заполняется». Процедура ждет, пока все ветви не окажутся в состоянии «заполнено», хоть пустым списком. Далее рекурсивно вызывает саму себя применительно ко всем подузлам. Разумеется, сервер должен возвращать, кроме собственно данных, сам заполняемый узел.

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

anonymous
()
Ответ на: комментарий от anonymous

У тебя есть команда развернуть все ветви дерева?

Нету пока. Есть чрезмерные обобщения и общее желание разобраться, найти «серебряную пулю». Это болезнь, я думаю, в моём возрасте я уже могу с этим сладить.

Отправлять самому себе через сервер нет нужды - таблицы продолжений вполне нормально с этим справляются. Такой функционал есть в swank - в каждом сообщении от swank есть номер продолжения, которому оно отправлено. И у меня уже есть к нему ответная часть ,см. ::tkcon::EvalInSwankAsync

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

den73 ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.