GraphABC обрабатывает ввод с мыши во втором потоке и потому падает?

Если взять пример “Рисование мышью в графическом окне” и добавить в него отрисовку в бесконечном цикле (например для анимации), то после некоторого активного рисования мышкой программа упадёт.

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

Извиняюсь, если говорю глупости. Совсем не знаю как внутри устроены PascalABC .NET и .NET Framework. Я что-то делаю не так? Надо использовать синхронизацию через lock или ещё что-то?

Мне кажется хорошо было бы отказаться от обработчиков во втором потоке. Лучше бы программа узнавала бы о событиях через вызов какого-нибудь GetInputEvents(): array of InputEvent.

uses GraphABC;

procedure MouseDown(x,y,mb: integer);
begin
  MoveTo(x,y);
end;

procedure MouseMove(x,y,mb: integer);
begin
    if mb=1 then LineTo(x,y);
end;

begin
  // Привязка обработчиков к событиям
  OnMouseDown := MouseDown;
  OnMouseMove := MouseMove;
  while (true) do
    begin
      Circle(100,100,50);
    end;
end.

Upd: Чтобы тема лучше гуглилась, добавлю ошибку текстом.

Ошибка времени выполнения: System.InvalidOperationException: В данный момент объект используется другим процессом.
Стек:
   в System.Drawing.Pen.get_DashStyle()
   в GraphABC.GraphABC.Ellipse(Int32 x1, Int32 y1, Int32 x2, Int32 y2)
   в GraphABC.GraphABC.Circle(Int32 x, Int32 y, Int32 r)
   в падает.Program.$Main() в C:\PABCWork.NET\падает.pas:строка 21
   в падает.Program.Main()

Да, вы почти правильно поняли. Дело как раз в том что одновременно в 2 потоках начинается рисование.

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

И - да, лучшее решение это lock. Надо сделать общий объект ( new object сойдёт, главное не строку и не typeof() ) и положить Circle и LineTo под lock для этого объекта.

1 лайк

Спасибо за пояснение!

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

По-моему самый лучший подход в данном случае это что-то типа такого:

Uses GraphABC;

var
  in_q := new Queue<integer>;

procedure KeyDown(Key: integer);
begin
  lock in_q do
    in_q.Enqueue(Key);
end;


function GetPressedKeys(): array of integer;
begin
  lock in_q do
  begin
    Result := in_q.ToArray();
    in_q.Clear();
  end;
end;

var
  PressedKeys: array of integer;

begin
  OnKeyDown := KeyDown;
  while(true) do
  begin
    PressedKeys := GetPressedKeys();
    if PressedKeys.Length > 0 then 
      Print(PressedKeys);
    sleep(1000); //какое-нибудь долгое действие
  end;
end.

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

Если при нажатии кнопки надо её как то по-сложному обрабатывать - имеет смысл даже запускать обработку в отдельный поток (потому что поток обработки эвентов - это поток формы, у вас всё на экране иначе зависнет, пока вы будете обрабатывать).

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

Мне кажется вы упускаете из виду, что из-за многопоточности тут сложности не только с рисованием на экране. Во втором потоке (то есть в обработчиках) так же нельзя обсчитывать игровой мир, если не использовать блокировку. Потому что если основной поток попробует работать с данными, которые находятся в середине процесса изменения обработчиком, то может получиться очень грустно: потеряется консистентность данных и всё сломается.

Поэтому у таких обработчиков всего 2 варианта использования:

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

  2. Обработчики сами изменяют игровой мир/выводят на экран картинку. Но в их коде придётся использовать явные блокировки игрового мира. Со всеми сложностями блокировок. И скорее всего обсчёт мира так же последовательно будет выполняться, так как параллелить нечего.

Я всеми руками за первый подход! Мне нужно просто дать возможность школьникам написать игру. Тратить кучу времени на объяснение блокировок и всяких сложностей создания многопоточных приложений не хочется. Этот же подход используется в PyGame: pygame.event.get()

Многопоточность улучшает производительность только если задачу можно хорошо распараллелить. Тут вроде бы параллелить ничего не получится, да и вряд ли мы в простой игре упрёмся в производительность 1 ядра. Боль вызванная попыткой использовать многопоточность будет не оправданной. Если даже что-то делать в другом потоке, то ради предсказуемости лучше самостоятельно его создать, чем полагаться на вызов обработчиков.

У второго подхода, по-моему, нет никаких плюсов. Сплошные минусы.

Вообще мне кажется, что разработчики GraphABC сделали 2 потока только ради того, чтобы скрыть от новичков event loop. Создал окошко и рисуешь в нём что угодно без понимания всяких event loop. Красота. Оно просто не рассчитано на то, чтобы тут что-то интерактивное делать.

Это всё исходя из мысли - что GraphABC подходит для создания игр… GraphABC подходит для рисования графиков в школьных программах, а так же создания простейших интерфейсов, в которых - много-поточность уже имеет смысл (хотя для интерфейсов больше подходит FormsABC, или ещё лучше, само System.Windows.Forms).

Дело в том - что GDI (на котором построен GraphABC) никогда не даёт >20fps даже на суперкомпе. Для создания игр надо использовать OpenGL/DirectX. А входные данные получать, переопределив обработчик событий формы (если сделать класс наследующий от формы - там виден protected метод, получающий на вход данные о эвенте).

Не совсем, 2 потока - это поток формы (в котором находится event loop) и основной поток программы (который не обязательно состоит из 1 большого цикла, поэтому засунуть его в поток формы - не выйдет).

Мне GraphABC очень понравился своей простотой для новичков. Даже функцию инициализации запускать не нужно. uses GrpaphABC и вперёд! Тут основная цель – сделать изучение Паскаля интересным. И слегка низкий FPS не проблема. Ну и OpenGL проще объяснять после опыта с GraphABC.

Не согласен, что для создания интерфейса обязательно нужны несколько потоков. Насколько я знаю Gtk, qt и winforms работают с одним потоком. Потому что рисовать GUI не затратно. Подвисания могут возникнуть только когда нужно делать медленные операции ввода-вывода: работать с файлами или с сетью. Только тогда создаётся отдельный поток. Или не создаётся, если использовать магию асинхронного IO, но я в ней пока не смыслю. https://blog.stephencleary.com/2013/11/there-is-no-thread.html Там даже как-то интересно делается, что когда поток засыпает в ожидании каких-то событий, то в его контексте может выполняться другой код. (https://docs.microsoft.com/en-us/windows/desktop/sync/asynchronous-procedure-calls) Вроде где-то видел статью мол “наконец то можно делать приложение в один поток, да здравствует асинхронный IO”

Ну и для долгих вычислений тоже делают отдельный поток. Хотя с помощью APC возможно получится без этого.

А для самого GUI многопоточность не нужна.

Вот и я о том же. Остальные фреймворки заставляют программиста обязательно использовать event loop в своей программе. Либо создавая его явно (while(GetMessege()){…; DefWindowProc();} , либо вызывая какой-нибудь gtk_main(). Иначе не работает.

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

Первыми 2 не пользовался, но Windows.Forms создаёт по потоку на каждый элемент.
И - да, многопоточность не нужна инфтерфесу, но с ней всё становится проще.

+, всё для новичков. А для не_новичков - OpenGL, DirectX и т.п. Конечно, промежуточное тоже есть, взять хотя бы SharpDX.

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

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

В GDI - всё затратно. Лаги в Windows.Forms не сильно заметны только потому что там редко рисуется много и сразу, обычно всё изменяется по 1 элементу управления. И то - особенно при изменении размеров окна, легко получить лаги в 0.25fps.

1 лайк

Кстати, спустя лет семь после вариантов ускоренного интерфейса от Stardock и прочих на базе видеокарты/чипа, начиная с Win8 пытались ускорить GDI: на скорости модулей PABC это сказывается?

Не пробовал, но поидее - раз GDI используется на прямую в GraphABC, а не через доп библиотеки - достаточно перекомпилировать вашу программу на Win8.

С другой стороны - GDI это библиотеки .Net , то есть если вы не заставите паскаль использовать >.Net4.7 (а он пытается всегда использовать .Net4.0) - он будет компилироваться по-старому.

Это оказалось багом. Я посмотрел код GraphABC: там во всех функциях рисования используется мьютекс f. То есть библиотека рассчитана на рисование в обработчиках.

Например вот

procedure FillEllipse(x1, y1, x2, y2: integer);
begin
  Monitor.Enter(f);
  if NotLockDrawing then
    FillEllipse(x1, y1, x2, y2, gr);
  if DrawInBuffer then   
    FillEllipse(x1, y1, x2, y2, gbmp);
  Monitor.Exit(f);
end;

Но в Ellipse() обращения к Brush и Pen оказались не защищены:

procedure Ellipse(x1, y1, x2, y2: integer);
begin
  if Brush.NETBrush <> nil then 
    FillEllipse(x1, y1, x2, y2);
  if Pen.NETPen.DashStyle <> DashStyle.Custom then
    DrawEllipse(x1, y1, x2, y2);
end;
3 лайка

Ухты)) Даже так.
#1566

Я тут немного поэкспериментировал и могу сказать, что вы не правы по поводу WinForms и GDI в GraphABC.

В WinForms для GUI используется всего один поток. Там просто общий event loop, в котором живут все элементы. Хотя можно руками создать второй поток и запустить вторую форму в нём. Но тогда придётся делать Invoke/BeginInvoke, если обращаться к элементу из другого потока, иначе будет падать с исключением.

Можно ещё вот такой эксперимент поставить:

   public partial class Form1 : Form
   {
   private object obj = new Object();
   private System.Threading.Thread t;
    
    
    public Form1()
    {      
      InitializeComponent();
    }

    public void block_me(){
      System.Threading.Monitor.Enter(obj);
    }

    private void Form1_Load(object sender, EventArgs e)
    {
      t = new System.Threading.Thread(block_me);
      t.Start();
    }
    
    private void button1_Click(object sender, EventArgs e)
    {
      System.Threading.Monitor.Enter(obj); 
    }
   }

Остановка в обработчике нажатия вешает всю форму. Новый поток пришлось создавать потому что примитивы синхронизации в System.Threading нельзя заблокировать из того же потока (то есть рекурсивные). ReaderWriterLockSlim не рекурсивный, но он падает при попытке второй раз захватить вместо создания дедлока. Выстрелить в ногу оказалось сложнее, чем я думал :smiley:

Теперь по поводу GDI+ (System.Drawing, используемый в GraphABC, это GDI+, не GDI. Вроде бы разница есть) GDI+ на самом деле не такой уж медленный! Ограничиние на 20 FPS лежит где-то внутри GraphABC. Я написал на Паскале программку, которая рисует а экране вращающуюся 3д модель (просто проволочный рендеринг, на Хабре есть цикл статей, модель головы оттуда же). 2492 треугольника. Каждый треугольник рисуется 3 вызовами Line() == 7476 линий.

На GraphABC оно выдавало 12-13 FPS. Потом переделал отрисовку на System.Drawing — получил 37 FPS. Полностью загружает 1 ядро моего ноута в это и упирается. Когда заменил три вызова DrawLine на один DrawPolygon стало 51 FPS. Похоже вызовы тут дорогие. Замена обводки на закраску (FillPolygon) не ухудшает производительность, возможно даже повышает на пару FPS.

Если в оригинальной программе с GraphABC заменить вызовы Line на DrawPolygon, то особого прироста нет — около 14 FPS. А вот FillPolygon выдаёт 19-20 FPS. И не понятно во что упирается. Грузит на 22% CPU. Версия c System.Drawing грузит на 24,8%. Там то похоже, что в ядро. А тут не ясно.

Есть подозрение что всё дело всё дело в том, как в GraphABC сделан back buffer/double buffering. Я использовал нативный вариант: выставить DoubleBuffered в true и рисовать только в graphics прилетающий в OnPaint. В GraphABC используется что-то своё и похоже оно медленнее нативного. Но я не уверен, что ограничение на FPS только тут.

Что удивляет, так это глубокое обсуждение GraphABC после того как разработчики официально проинформировали год с четвертью тому назад:

[30.08.17] Вышла версия 3.3.0.1531
- Новый модуль растровой графики GraphWPF
- Модуль GraphABC объявляется устаревшим. Он будет входить в последующие версии,
но обновляться не будет

А разве разница не только в большем функционале, с теми же кишками? Как и в случае с C/C++ ?

Нет, конечно, если вы отрисовываете 200х200 пикселей - вы можете и 500fps выжать. Но после того как скорость и функционал GraphABC мне надоели - я решил использовать System.Drawing напрямую. И вот только тогда я замерил fps, и оказалось что рисование прямоугольника на весь экран даёт ~17 fps.

Вряд ли, там слишком просто всё чтоб было куда такую систему впихнуть.

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

Я замечал, System.Threading.Monitor вызывает такое. Некоторая часть времени тратится на ожидание, поэтому в диспетчере задач не показывает.

Ну а скорость - в GraphABC хватает костылей. В паре с System.Threading.Monitor - это уже наверняка объясняет потерю нескольких кадров.

back buffer Если вы включаете LockDrawing. Иначе всегда прямо на элемент управления на форме рисует (не на саму форму, потому что надо что ещё полоска ввода снизу работала, да попроще).

И GDI+ :wink:.

P.S. Наверное я всё же делал что то не так используя GDI в своих программах, потому что это даёт 1.5к-1.7к fps…

{$reference System.Windows.Forms.dll}
{$reference System.Drawing.dll}

uses System.Windows.Forms;
uses System.Drawing;

begin
  var f := new Form;
  f.WindowState := FormWindowState.Maximized;
  f.FormBorderStyle := FormBorderStyle.None;
  
  f.Shown += procedure(o,e)->
  System.Threading.Thread.Create(()->
  begin
    var WW := f.ClientSize.Width;
    var WH := f.ClientSize.Height;
    var Brush := new SolidBrush(Color.Empty);
    var gr := f.CreateGraphics;
    
    var n := 0;
    var LT := System.DateTime.Now;
    var m := 2000;
    
    while true do
    begin
      
      var c := Color.FromArgb(255,128,128,Random(128-20,128+20));
      Brush.Color := c;
      gr.FillRectangle(Brush, 0,0,WW,WH);
      
      n += 1;
      if n=m then
      begin
        var NT := System.DateTime.Now;
        n := 0;
        writeln(m/(NT-LT).TotalMilliseconds*1000);//готовые fps
        LT := NT;
      end;
      
    end;
    
  end).Start;
  
  f.Closing += procedure(o,e)->Halt;//без этого падает поток рисования при Alt+F4
  Application.Run(f);
end.

Правда, у меня в программах была двойная буферизация, ещё и через битмап…

P.P.S Вот, ели откопал в архиве старого бекапа проект который ещё использовал GDI: GA.zip. Оно даже всё ещё запускается :smiley: (ну, я там добавил () в 1 месте и стало запускаться). Ну в общем если хотите закопаться - всё по делу в GData и в Data в процедуре Redrawing. Только осторожно - говнокод)))

Да я просто из спортивного интереса) GraphWPF тоже пробовал. Значение FPS не помню, но около GraphABC (или даже чуть хуже). И оно у меня постоянно падало, нарисовав вроде бы 20500 (каждый раз разное число).

Учитывая, что Паскаль по старинке позиционируют для обучения (“Ой! А в чём тогда программировать и делать софт?”), где редко требуют динамическую графику в современном понимании, поэтому это пока* явно не приоритет для разработчиков. Кроме того, попадались случаи с дельта-таймингом (компенсация) и просто неверным подсчётом кадров (особенно адаптивный Vsync и прочие хитрые настройки драйвера видеокарты), однако примеры довольно правдоподобны для FPS<30.

По теме графики в PABC,NEТ: меня интересует возможность (желательно настраиваемых) средств для отслеживания и перерисовки подвижных объектов только в изменённых (invalidate) участках экрана.

GraphABC и GraphWPF, а также Graph3D писались исключительно с ориентацией на сверхпростое использование (обучение, статическая и малоанимационная графика). Для ускорения графики следует использовать базовые библиотеки .NET - ускорение будет значительным, но код усложнится пропорционально.

Хочу обратить внимание, что WPF использует DirectX для прорисовки, но сама занимается определением того, что надо прорисовывать, поэтому получается универсальнее, но самому вызвать Invalidate нельзя, что не позволяет тонко оптимизировать скорость.

Действительно, в GraphABC и GraphWPF, а также Graph3D используется два потока - в потоке begin - end можно рисовать (в базовом .NET - не так). Но на подобное тратится очень много ресурсов - в частности, все примитивы в GraphWPF, Graph3D рисуются через Invoke, что страшно медленно.

3 лайка