июля 28, 2010

Загрузка, ждите

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

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

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

На Free Pascal я могу назвать три решения.
1) В лоб — после каждой строчки кода загрузки вызываем функцию перерисовки. Это можно сделать так:


for I := 0 to LinesCount - 1 do begin
  case I of 
    0: <Line0>;
    1: <Line1>;
    2: <Line2>;
    ...
  end;
  RedrawLoading(I, LinesCount);
end;


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

2) Использовать нити (threads, так же известные как «потоки») — в одной нити грузим, в другой обновляем экран. Решение мне не нравится тем, что ИМХО с нитями лучше лишний раз не связываться.

3) Написать класс, предназначенный для выполнения одного шага загрузки, имеющий одну абстрактную виртуальную функцию Execute(). Унаследовать от него разные типы шагов: копирование файла, загрузка библиотеки, загрузки картинки, проверка чего-нибудь и т.д. После этого перед загрузкой занести в нужной последовательности такие классы в массив, и в цикле после каждого Execute вызывать RedrawLoading(I, Length(Steps)). Очень громоздкое решение — городить его ради ерунды как-то не очень хочется.


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

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


(defmacro watch ((watch-func) &body body)
  (let ((op body))
    `(progn 
       ,@(loop for i upto (- (* (list-length body) 2) 1) collect
               (if (evenp i)
                   `(,watch-func ,(/ i 2) ,(list-length body))
                   (let ((result `(,@(car op)))) (setf op (cdr op)) result)))
       (,watch-func ,(list-length body) ,(list-length body)))))


Функция watch-func как бы наблюдает за ходом выполнения кода body, поэтому макрос я назвал watch. Несмотря на то, что я попытался написать красивый код, получилось спагетти. Но поставленную задачу оно выполняет, вот лог нескольких тестов:


(watch (L) A B C D E F)

(PROGN (0 6) A (1 6) B (2 6) C (3 6) D (4 6) E (5 6) F (6 6))


(watch (L) A)

(PROGN (0 1) A (1 1))


(watch (L))

(PROGN (0 0))


По хорошему, нужно еще обезопасить L от многократных вычислений, это можно сделать конструкцией once-only.

В Free Pascal нормальной системы макросов нет. Поэтому аналогичную конструкцию я написал в моей утилитке dpp. Т.к. для того, чтобы достаточно точно интегрировать конструкцию в сам язык, нужно парсить программу на паскале, мне пришлось вводить несколько инородный синтаксис:


  #(watch (RenderLoading)
    (FGui := TGui.Create(FGraph, FWindow);)
    (FFontMan := TFontMan.Create(Graph);)
    (FScene := TCucuScene.Create(Graph);)

    (FStyle := TStyle.Create;)
    (FStyle.SetParam('font.primary', TStyleParamFont.Create(TFont.Create(FFontMan, 'data\font\c256.bit', -6)));)
    (FStyle.SetParam('color.primary', TStyleParamColor.Create(Vec4f(0.5, 0.5, 0, 1)));)
    (FStyle.SetParam('color.background', TStyleParamColor.Create(Vec4f(0.3, 0.0, 0.3, 1)));)  
    (FGui.Root.Style := FStyle;)  
    (FConsole := TGuiConsole.Create(FGui);)

    (FStart := GetTimer;)
  )


3 комментария:

dmitry-vk комментирует...

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

>с нитями лучше лишний раз не связываться.

Чем это объясняется?

Дож комментирует...

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

Моё решение, конечно, не претендует на всеобщность, но зато достаточно простое, для небольших программ вполне сойдёт.

> Чем это объясняется?

Наверно, ничем обоснованным :) Моё субъективное предпочтение решениям без нитей там, где это возможно. Вставил в пост «ИМХО».

Дож комментирует...

К примеру, в Free Pascal функция Random не thread-safe, её следует использовать в критических секциях, но об этом нигде не написано. Я этого до недавнего времени не знал, и не понятно сколько всего я ещё не знаю, — поэтому лишний раз лучше не рисковать :)

Отправить комментарий

Постоянные читатели

Обо мне

Моя фотография
Мой e-mail: vitek_03(at)mail(dot)ru