Advertisement
runewalsh

Старые призраки

May 3rd, 2021
1,578
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Delphi 16.28 KB | None | 0 0
  1. // LazyGhost — синглтон, создаваемый при первом обращении и уничтожаемый с задержкой, когда в нём пропадает необходимость.
  2. //
  3. // Например:
  4. // — DLL, удерживаемая в памяти, пока существует хотя бы один работающий с ней объект,
  5. //   и затем висящая ещё 1 минуту на случай, если снова кому-то понадобится.
  6. //
  7. // type
  8. //     LuaScriptState = record
  9. //         state: lua_State;
  10. //         lib: LuaLibAnchor;
  11. //
  12. //         procedure Init;
  13. //         begin
  14. //             LuaLibGhost.Summon(lib);
  15. //             ...
  16. //         end;
  17. //     end;
  18. //
  19. // — Вычисляемые лукап-таблицы, такие как в CRC-алгоритмах.
  20. //
  21. //     function CRC32(data: pointer; size: SizeUint): uint32;
  22. //     var
  23. //         table: CRC32TableAnchor;
  24. //     begin
  25. //         CRC32TableGhost.Summon(table);
  26. //         ...
  27. //     end;
  28. //
  29. // Была неоднозначная багофича с рекурсивными призраками.
  30. // Например, плагины BASS требуют, и удерживают призрак, самой BASS.
  31. // Поэтому если отпустить плагин, то сначала запускался таймер самоуничтожения плагина, по истечении которого плагин выгружался,
  32. // в ходе выгрузки отпускал BASS, и только тогда запускался таймер самоуничтожения BASS.
  33. // Если тот и другая имели таймер выгрузки 30 секунд, то BASS в результате выгружалась через 30+30 вместо ожидаемых 30.
  34. //
  35. // Это не сильно критично, но всё равно неприятно.
  36. // Чтобы этого избежать, теперь запоминается lastTouch — время последнего Summon'а или НЕРЕКУРСИВНОГО Unsummon'а,
  37. // и при рекурсивном Unsummon — UnsummonRecursive, таймер уменьшается на время, прошедшее с lastTouch.
  38. //
  39. // UnsummonRecursive нужно вызывать явно вместо обычного Unsummon, вызываемого автоматически. Например, призраки плагинов вызывают его в деструкторах.
  40. // Если этого не сделать, потенциально будет получено старое поведение с полным сложением таймеров самоуничтожения.
  41. //
  42. // TODO:
  43. // Возникают проблемы с финализацией глобальных переменных, приходится вручную следить за pending'ами. Пока полностью забил.
  44. // Как минимум можно придумать что-то с флагом «УМРИ», с которым работа Finalize будет отложена до освобождения последней ссылки.
  45. // В идеале можно попробовать извернуться и переписать всё на Reference'ы.
  46. // Насчёт обоих вариантов не уверен, что они вообще возможны.
  47. //
  48. // TODO2:
  49. // К тому же явно инициализируемые глобальные переменные не дают линкеру выбросить неиспользуемых призраков. Всё-таки желательно всё переделать.
  50.  
  51. {$include opts.inc}
  52. unit Framework.LazyGhosts;
  53.  
  54. {$include non_copyable.inc} {$include logging.inc}
  55.  
  56. interface
  57.  
  58. uses
  59.     Framework.Chrono, Framework.Threads, Framework.Exceptions, Framework.Intrinsics, Framework.Strings;
  60.  
  61. type
  62.     pLazyGhost = ^LazyGhost;
  63.     LazyGhost = record
  64.     type
  65.         InstanceBase = class
  66.             class function NameFancySrc: Noun;
  67.         protected
  68.             constructor CreateGhost; virtual; abstract;
  69.             class function Timeout: umsec; virtual;
  70.         end;
  71.         InstanceClass = class of InstanceBase;
  72.  
  73.         Anchor = record
  74.             procedure SetMove(var anch: Anchor);
  75.             procedure UnsummonRecursive;
  76.         private
  77.             g: pLazyGhost;
  78.             class operator Initialize(var self: Anchor);
  79.             class operator Finalize(var self: Anchor);
  80.             {$define typ := Anchor} non_copyable
  81.         end;
  82.  
  83.         procedure Init(ctl: InstanceClass);
  84.         function OK: boolean;
  85.         function Summon(out anch: Anchor): InstanceBase;
  86.  
  87.     const
  88.         SmallTimeout = umsec(5 * 1000);
  89.         QuiteATimeout = umsec(36 * 1000);
  90.  
  91.     private const
  92.         CREATION_FAILED = pointer(1);
  93.         LAST_FAKE_INSTANCE = CREATION_FAILED;
  94.         NotInitialized = 'LazyGhost не инициализирован.';
  95.         MinTimeout = umsec(100);
  96.     var
  97.         instance, destroyingInstance: InstanceBase;
  98.         refcount: SizeInt;
  99.         ctl: InstanceClass;
  100.         delayedDestroy: ThreadTimer;
  101.         err: TObject;
  102.         lastTouch: Ticks;
  103.         procedure Unsummon;
  104.         procedure UnsummonRecursive;
  105.         procedure HandleZeroRefCount(recursive: boolean);
  106.         class procedure TimerCallback(param: pointer; var ci: ThreadTimer.CallbackInstance); static;
  107.         class operator Initialize(var self: LazyGhost);
  108.         class operator Finalize(var self: LazyGhost);
  109.         {$define typ := LazyGhost} non_copyable
  110.  
  111.     public
  112.         class procedure GlobalInitialize; static;
  113.  
  114.     private class var
  115.         GlobalInitialized: boolean;
  116.         GlobalLock: ThreadLock;
  117.         GlobalHey: ThreadCV;
  118.     end;
  119.  
  120. implementation
  121.  
  122. uses
  123.     Framework.System;
  124.  
  125.     class function LazyGhost.InstanceBase.NameFancySrc: Noun;
  126.     var
  127.         s: StringView;
  128.     begin
  129.         s := ViewClassName(ClassType).CutSuffix('Ghost').CutSuffix('.');
  130.         result := Noun.Parse(s.ToString);
  131.     end;
  132.  
  133.     class function LazyGhost.InstanceBase.Timeout: umsec;
  134.     begin
  135.         result := {$ifdef Debug} SmallTimeout {$else} QuiteATimeout {$endif};
  136.     end;
  137.  
  138.     procedure LazyGhost.Anchor.SetMove(var anch: Anchor);
  139.     begin
  140.         if Assigned(g) then g^.Unsummon;
  141.         g := anch.g;
  142.         anch.g := nil;
  143.     end;
  144.  
  145.     procedure LazyGhost.Anchor.UnsummonRecursive;
  146.     var
  147.         t: pLazyGhost;
  148.     begin
  149.         t := Exchange(self.g, nil);
  150.         if Assigned(t) then t^.UnsummonRecursive;
  151.     end;
  152.  
  153.     class operator LazyGhost.Anchor.Initialize(var self: Anchor);
  154.     begin
  155.         self.g := nil;
  156.     end;
  157.  
  158.     class operator LazyGhost.Anchor.Finalize(var self: Anchor);
  159.     var
  160.         t: pLazyGhost;
  161.     begin
  162.         t := Exchange(self.g, nil);
  163.         if Assigned(t) then t^.Unsummon;
  164.     end;
  165. {$define typ := LazyGhost.Anchor} non_copyable_impl
  166.  
  167.     procedure LazyGhost.Init(ctl: InstanceClass);
  168.     begin
  169.         Assert(Assigned(ctl), 'не задан LazyGhost.InstanceClass');
  170.         Assert(GlobalInitialized, 'LazyGhost.Initialize не выполнена');
  171.         if Assigned(self.ctl) then
  172.             ThrowOpenedOver('LazyGhost', FancyString.Parse(ctl.NameFancySrc.ToNom).ToString, FancyString.Parse(self.ctl.NameFancySrc.ToGen).ToString);
  173.         self.ctl := ctl;
  174.     end;
  175.  
  176.     function LazyGhost.OK: boolean;
  177.     begin
  178.         result := Assigned(ctl);
  179.     end;
  180.  
  181.     function LazyGhost.Summon(out anch: Anchor): InstanceBase;
  182.     const
  183.         InstanceExpectedToBeNil = 'кто-то помимо первого призванного изменил instance = nil';
  184.     var
  185.         terr: TObject;
  186.         r: SizeInt;
  187.     begin
  188.         Assert(Assigned(ctl), NotInitialized);
  189.         if InterlockedIncrement(refcount) = 1 then
  190.         begin
  191.             // Первый саммон.
  192.             // Либо инстанса вообще не существует, и нужно создать новый, либо взведён таймер уничтожения, и нужно попытаться вырвать старый инстанс из его лап.
  193.             // В любом случае instance = nil, и, видя это, остальные потоки (которые пойдут по ветке refcount > 1) будут ждать, пока этот что-то придумает.
  194.             //
  195.             // Если создание инстанса провалилось, инстанс выставляется в CREATION_FAILED и (возможно) запоминается исключение в err, так что все ждущие
  196.             // смогут его скопировать и перевыбросить. Последний ждущий в этом случае очищает инстанс назад в nil и уничтожает err
  197.             // (вернее, утаскивает err себе и перевыбрасывает без копирования).
  198.             //
  199.             // Можно кэшировать ошибку на несколько секунд (или на тот же timeout — более безумный вариант), но это спорная практика.
  200.             // Сделаю, если сильно понадобится.
  201.             //
  202.             // Всюду делается вид, что Enter и Wake не могут бросить исключение.
  203.  
  204.             GlobalLock.Enter;
  205.             try
  206.                 lastTouch := Ticks.Get;
  207.  
  208.                 // Нужна перепроверка на Assigned(instance) на случай, если перед этим InterlockedIncrement'ом начинал исполняться Unsummon,
  209.                 // но после него заметил, что refcount > 0, и отменился, оставив инстанс в живых.
  210.                 result := instance;
  211.                 if Assigned(result) then
  212.                 begin
  213.                     Assert(pointer(result) > LAST_FAKE_INSTANCE, '{}: странный инстанс при refcount=1'.Format(instance.ToString));
  214.                     anch.g := @self;
  215.                     exit;
  216.                 end;
  217.  
  218.                 // отобрать destroyingInstance у таймера уничтожения, если он взведён, так что если он начал срабатывать, то сработает вхолостую.
  219.                 // Если таймер не взведён, destroyingInstance = result = nil.
  220.                 pointer(result) := Exchange(pointer(destroyingInstance), nil);
  221.             finally
  222.                 GlobalLock.Leave;
  223.             end;
  224.  
  225.             try
  226.                 // Отпускание блокировки в любом случае не было избыточным:
  227.                 //
  228.                 // — (1) если таймера не существовало, в том числе если он успел сработать, уничтожил инстанс и уничтожился сам,
  229.                 //       то нужно отпустить блокировку, чтобы создать инстанс с нуля.
  230.                 //
  231.                 // — (2) если таймер начал срабатывать, то Close будет ждать завершения обработчика таймера, поэтому под блокировкой дедлокнется.
  232.                 //
  233.                 //       (Если таймер взведён, но ещё НЕ начал срабатывать, то Close отменит его без ожидания и блокировку отпускать было необязательно.
  234.                 //       Но отличить этот случай без гонки невозможно).
  235.  
  236.                 if Assigned(result) then
  237.                 begin
  238.                     delayedDestroy.Close; // реальная работа в случае (2)
  239.                 end else
  240.                 begin
  241.                     result := ctl.CreateGhost; // реальная работа в случае (1)
  242.                     Assert(pointer(result) > LAST_FAKE_INSTANCE);
  243.                 end;
  244.             except
  245.                 GlobalLock.Enter;
  246.                 try
  247.                     r := InterlockedDecrement(refcount);
  248.                     Assert(not Assigned(instance), InstanceExpectedToBeNil);
  249.                     if r > 0 then
  250.                     begin
  251.                         pointer(instance) := CREATION_FAILED;
  252.                         err := Exception.Acquire;
  253.                         GlobalHey.WakeAll;
  254.                     end else
  255.                         ; // Нет ждущих, просто оставляется instance = nil. Для кэширования ошибки вместо этого нужно было бы выставлять таймер, как в Unsummon.
  256.                 finally
  257.                     GlobalLock.Leave;
  258.                 end;
  259.                 raise;
  260.             end;
  261.  
  262.             GlobalLock.Enter;
  263.             try
  264.                 Assert(not Assigned(instance), InstanceExpectedToBeNil);
  265.                 instance := result;
  266.                 GlobalHey.WakeAll;
  267.             finally
  268.                 GlobalLock.Leave;
  269.             end;
  270.  
  271.             anch.g := @self;
  272.         end else
  273.         begin
  274.             lastTouch := Ticks.Get;
  275.  
  276.             result := instance;
  277.             if pointer(result) > LAST_FAKE_INSTANCE then
  278.             begin
  279.                 anch.g := @self;
  280.                 exit;
  281.             end;
  282.  
  283.             GlobalLock.Enter;
  284.             try
  285.                 repeat
  286.                     result := instance;
  287.                     if Assigned(result) then break;
  288.                     GlobalHey.Wait(GlobalLock);
  289.                 until no;
  290.  
  291.                 if pointer(result) > LAST_FAKE_INSTANCE then
  292.                 begin
  293.                     anch.g := @self;
  294.                     exit;
  295.                 end;
  296.  
  297.                 Assert(pointer(result) = CREATION_FAILED, result.ToString + ' — инстанс после ожидания');
  298.                 if InterlockedDecrement(refcount) = 0 then
  299.                 begin
  300.                     instance := nil;
  301.                     pointer(terr) := Exchange(pointer(err), nil);
  302.                 end
  303.                 else if Assigned(err) then
  304.                     terr := Exception.Clone(err)
  305.                 else
  306.                     terr := nil;
  307.                 if not Assigned(terr) then terr := Error('Создание инстанса провалилось.');
  308.                 raise terr;
  309.             finally
  310.                 GlobalLock.Leave;
  311.             end;
  312.         end;
  313.     end;
  314.  
  315.     procedure LazyGhost.Unsummon;
  316.     begin
  317.         if InterlockedDecrement(refcount) = 0 then HandleZeroRefCount(no) else lastTouch := Ticks.Get;
  318.     end;
  319.  
  320.     procedure LazyGhost.UnsummonRecursive;
  321.     begin
  322.         if InterlockedDecrement(refcount) = 0 then HandleZeroRefCount(yes);
  323.     end;
  324.  
  325.     procedure LazyGhost.HandleZeroRefCount(recursive: boolean);
  326.         procedure TraceInstantUnload(recursive: boolean);
  327.         begin
  328.             log.Trace('Немедленная выгрузка {M}{}.', IfThen(recursive, ' (рекурсивно)'), {M} FancyString.Parse(ctl.NameFancySrc.ToGen));
  329.         end;
  330.     var
  331.         t: InstanceBase;
  332.         timeout: umsec;
  333.     begin
  334.         // По-хорошему не должно происходить никогда, но с зависимыми призраками, такими как BASS и плагины, возможно при завершении приложения
  335.         // из-за неопределённого порядка финализации.
  336.         if not Assigned(ctl) then exit;
  337.  
  338.         t := nil;
  339.         GlobalLock.Enter;
  340.         try
  341.             // Здесь нужна перепроверка на refcount = 0, потому что после InterlockedDecrement'а и перед захватом блокировки мог исполниться Summon.
  342.             if (refcount = 0) and (pointer(instance) > LAST_FAKE_INSTANCE) then
  343.             begin
  344.                 Assert(not Assigned(destroyingInstance), 'destroyingInstance и instance существуют одновременно');
  345.                 timeout := ctl.timeout;
  346.                 if recursive then timeout := umsecBaseType(timeout).SatSub((Ticks.Get - lastTouch).ToUmsecClamp);
  347.                 if timeout < MinTimeout then
  348.                 begin
  349.                     if log.NeedTrace then TraceInstantUnload(recursive);
  350.                     pointer(t) := Exchange(pointer(instance), nil);
  351.                 end else
  352.                 begin
  353.                     pointer(destroyingInstance) := Exchange(pointer(instance), nil);
  354.                     try
  355.                         delayedDestroy.Start(@TimerCallback, @self, timeout, 0, ctl.NameFancySrc, [ThreadTimer.NonCritical]);
  356.                     except
  357.                         pointer(t) := Exchange(pointer(destroyingInstance), nil);
  358.                         raise;
  359.                     end;
  360.                 end;
  361.             end;
  362.         finally
  363.             GlobalLock.Leave;
  364.             if Assigned(t) then t.Destroy;
  365.         end;
  366.     end;
  367.  
  368.     class procedure LazyGhost.TimerCallback(param: pointer; var ci: ThreadTimer.CallbackInstance);
  369.     var
  370.         g: pLazyGhost absolute param;
  371.         t: InstanceBase;
  372.     begin
  373.         GlobalLock.Enter;
  374.         try
  375.             t := g^.destroyingInstance;
  376.             if Assigned(t) then
  377.             begin
  378.                 g^.destroyingInstance := nil;
  379.                 GlobalLock.Leave;
  380.                 try
  381.                     t.Destroy;
  382.                 finally
  383.                     GlobalLock.Enter;
  384.                 end;
  385.             end;
  386.             ci.Close;
  387.         finally
  388.             GlobalLock.Leave;
  389.         end;
  390.     end;
  391.  
  392.     class operator LazyGhost.Initialize(var self: LazyGhost);
  393.     begin
  394.         self.instance := nil;
  395.         self.destroyingInstance := nil;
  396.         self.refcount := 0;
  397.         self.ctl := nil;
  398.         self.err := nil;
  399.     end;
  400.  
  401.     class operator LazyGhost.Finalize(var self: LazyGhost);
  402.     begin
  403.         self.delayedDestroy.Close;
  404.         if Assigned(self.destroyingInstance) then self.destroyingInstance.Free(self.destroyingInstance);
  405.         if pointer(self.instance) > LAST_FAKE_INSTANCE then self.instance.Free(self.instance);
  406.         self.ctl := nil;
  407.     end;
  408. {$define typ := LazyGhost} non_copyable_impl
  409.  
  410.     class procedure LazyGhost.GlobalInitialize;
  411.     begin
  412.         Assert(not GlobalInitialized, 'LazyGhost.Initialize уже выполнена.');
  413.         GlobalLock.Init;
  414.         GlobalHey.Init;
  415.         GlobalInitialized := yes;
  416.     end;
  417.  
  418. end.
  419.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement