Tratando Concurrency

Gerenciando Concorrência

Mesmo que a nova aplicação resulte de uma migração de uma aplicação VCL atual que já é multiusuário e cliente/servidor, uma aplicação uniGUI atenderá múltiplos usuários dentro do mesmo processo.

Uma aplicação VCL cliente/servidor multiusuário geralmente tem várias instâncias do aplicativo cliente rodando em vários computadores, cada uma conectando-se a um servidor de banco de dados compartilhado. Quase todos os servidores de banco de dados suportam múltiplas conexões implementando bloqueio de linha/tabela quando necessário.

Esse tipo de aplicação VCL é uma aplicação típica de 2 camadas. Uma aplicação uniGUI similar é uma aplicação de 3 camadas, porque a interface do usuário (camada de apresentação) roda no navegador do cliente, a maior parte do código da aplicação anterior roda no servidor (agora atendendo múltiplos usuários/sessões) e compartilha o mesmo servidor de banco de dados.

O fato de o novo código não-visual da aplicação uniGUI atender múltiplos usuários exige o tratamento de acesso concorrente a quaisquer recursos compartilhados. Esse acesso concorrente ocorrerá de forma multithread, onde cada requisição vem de uma thread diferente.

ServerModule

Use o ServerModule singleton para informações globais e provavelmente apenas leitura.

A maioria das aplicações web exige acesso a um servidor de banco de dados. Às vezes usará controles de acesso a dados como TFDConnection, mas também é possível usar algum middleware. Em qualquer caso, a informação sobre como conectar ao servidor de banco de dados deve estar disponível para todas as sessões de usuário. Se estiver usando um TFDConnection ou similar, o programa precisará montar a string de conexão correta ou instanciar os componentes correspondentes.

Como mencionado anteriormente, se usar FireDAC devemos adicionar o TFDManager, o driver para o banco de dados escolhido, etc. Se usar um TADOConnection, basta ter a connection string.

Após criar o ServerModule, ele deve expor apenas propriedades de leitura para as sessões de usuário.

circle-exclamation

Se, apesar do aviso anterior, existir alguma razão muito importante para expor aqui uma propriedade de leitura/gravação, ela deve ser protegida assegurando que apenas uma sessão por vez possa modificá-la. A solução óbvia para esse tipo de proteção é usar uma seção crítica.

unit ServerModule;

interface

uses
  Classes, SysUtils, uniGUIServer, uniGUIMainModule, uniGUIApplication,
  uIdCustomHTTPServer, uniGUITypes, SyncObjs;

type
  TUniServerModule = class(TUniGUIServerModule)
    procedure UniGUIServerModuleCreate(Sender: TObject);
    procedure UniGUIServerModuleDestroy(Sender: TObject);
  private
    FConnectionString   : string;
    FSharedVariable     : string;
    FLockSharedVariable : TCriticalSection;
    function  GetSharedVariable : string;
    procedure SetSharedVariable(const Value : string);
  protected
    procedure FirstInit; override;
  public
    property ConnectionString : string read FConnectionString;
    property SharedVariable   : string read GetSharedVariable
                                       write SetSharedVariable;
  end;

function UniServerModule: TUniServerModule;

implementation

{$R *.dfm}

uses
  UniGUIVars;

function UniServerModule: TUniServerModule;
begin
  Result := TUniServerModule(UniGUIServerInstance);
end;

procedure TUniServerModule.FirstInit;
begin
  InitServerModule(Self);
end;

procedure TUniServerModule.UniGUIServerModuleCreate(Sender: TObject);
begin
  FConnectionString   := 'Some Connection String';
  FSharedVariable     := 'Shared';
  FLockSharedVariable := TCriticalSection.Create;
end;

procedure TUniServerModule.UniGUIServerModuleDestroy(Sender: TObject);
begin
  FLockSharedVariable.Free;
end;

function TUniServerModule.GetSharedVariable : string;
begin
  FLockSharedVariable.Acquire;
  try
    Result := FSharedVariable;
  finally
    FLockSharedVariable.Release;
  end;
end;

procedure TUniServerModule.SetSharedVariable(const Value : string);
begin
  FLockSharedVariable.Acquire;
  try
    FSharedVariable := Value;
  finally
    FLockSharedVariable.Release;
  end;
end;

initialization
  RegisterServerModuleClass(TUniServerModule);

end.

Neste caso, a connection string foi atribuída ao criar o ServerModule, e é uma propriedade somente leitura. No entanto, a SharedVariable é uma propriedade de leitura/gravação, e devemos garantir que apenas um usuário possa escrevê-la. Estamos reforçando o acesso único à variável usando uma seção crítica. Ainda mais, estamos ocultando esse fato no método SetSharedVariable para que não seja necessário conhecer, adquirir nem liberar o objeto TCriticalSection ao acessar a propriedade. Claro que, ao permitir modificar essa propriedade, também precisamos controlar as leituras. Qualquer propriedade de leitura/gravação desacelerará a aplicação (dependendo de quão frequentemente ela acessa a variável).

É importante notar que, embora você possa usar variáveis compartilhadas graváveis no ServerModule conforme definido acima, não é recomendado confiar nelas como uma variável global de toda a aplicação. No futuro, cada aplicação uniGUI pode ser dividida em vários processos menores para implementar balanceamento de carga e outros recursos avançados. Nesse caso, qualquer variável global compartilhada no ServerModule será visível apenas para seu processo proprietário, não para os outros processos que servirão a mesma aplicação em um pool de processos. Por isso, é recomendável manter tais variáveis globais em uma tabela de banco de dados em vez de mantê-las em memória.

circle-exclamation

MainModule

Use o MainModule para variáveis de sessão de usuário.

Usar o RAD Studio para desenvolvimento em Delphi gera código correto para uma aplicação VCL de usuário único, mas parte desse código criará problemas em um ambiente multiusuário ou multithread. Abaixo estão exemplos mostrando por que falhará e como corrigir.

1

Globais problemáticos auto-gerados de formulário/data module

Após pedir para criar um novo formulário VCL, obtemos este código:

Então outra unit pode usar essa variável global assim:

Form1 é acessível a qualquer sessão de usuário, e vários usuários poderiam estar usando-o. Imagine o que acontecerá se dois usuários criarem a mesma variável (causando um vazamento de memória de um formulário e uma referência inválida ao liberar o formulário sobrevivente).

2

Uso corrigido (sem variáveis globais)

Uma correção simples evita essa situação:

No uniGUI não precisamos manter uma instância do formulário em uma variável global. Delete todas as variáveis globais geradas automaticamente e crie sua própria instância privada. Este é um bom hábito para aplicações VCL e requer corrigir formulários e data modules.

Exemplo de um data module auto-gerado com uma variável global (não use):

uniGUI é mais do que apenas um conjunto de componentes para renderizar a camada de apresentação; é também um framework que suporta as melhores práticas para desenvolvimento de aplicações web. Entre as melhores práticas suportadas está a ausência de variáveis globais geradas automaticamente e o gerenciamento automático de memória de formulários e data modules anexados a uma sessão de usuário.

Em vez da variável global que o Delphi cria para formulários e data modules, o uniGUI cria funções que retornam a instância da sessão com base no tipo registrado anteriormente. Vamos examinar o MainModule como um exemplo típico.

Note a chamada a RegisterMainModuleClass durante a inicialização. Ao precisar acessar o MainModule da sessão, basta usar a função UniMainModule. Outra função de sessão, UniApplication, retornará a instância do MainModule registrado correspondente à sessão atual.

O mesmo tipo de solução se aplica a formulários e módulos de sessão. Se usarmos o assistente uniGUI para criar um novo formulário e selecionar Application Form, obteremos isto:

o uniGUI também gerencia o tempo de vida do formulário. Exemplo de uso a partir de um formulário principal:

No código VCL anterior precisávamos criar a instância temporária do formulário, abri-lo usando ShowModal e liberá-lo (usávamos try ... finally para evitar vazamento de memória). Esse código precisa apenas de uma chamada: UniForm1.ShowModal. Por quê?

UniForm1 cria um formulário temporário que é liberado automaticamente ao alcançar o fim de seu tempo de vida, mas estará disponível até esse momento.

E quanto a mostrar um formulário não modal? os formulários uniGUI são liberados automaticamente quando fechados.

Se você precisa de apenas alguns data modules, pode usar o MainModule para compartilhar sua conexão e outras variáveis de sessão, e criar esses data modules como "Application Data Modules". Eles serão anexados à sessão e gerenciados pelo uniGUI. O código é muito similar ao formulário anterior.

Se você tiver muitos formulários ou os data modules consumirem muitos recursos, qualquer formulário poderia gerenciar seu próprio data module manualmente criando-o em FormCreate e liberando-o em FormDestroy. Ainda assim, ao criar o data module como um data module uniGUI, nenhuma variável global será criada, não será necessário criá-lo em FormCreate e, se o desenvolvedor esquecer de liberar o data module em FormDestroy, ele será liberado automaticamente ao fechar a sessão do usuário.

Como o MainModule é criado automaticamente para cada novo usuário, todas as variáveis anexadas a uma sessão devem ser acessíveis através dele. Nunca use variáveis globais.

Forms e DataModules livres

No uniGUI você também pode criar forms e datamodules livres. Eles são chamados livres porque seu tempo de vida é gerenciado pelo desenvolvedor, não pelo framework.

Links:

  • https://unigui.com/doc/online_help/free-form.htm

  • https://unigui.com/doc/online_help/free-datamodule.htm

Formulário de Login

Uma vez que o MainModule exista, o runtime do uniGUI verificará se um formulário de login foi registrado. Você pode criar o formulário de login usando o assistente uniGUI para criar formulários e selecionando "Login Form". A única diferença em relação a qualquer outro formulário de aplicação é que ele herda de TUniLoginForm e será exibido automaticamente quando uma nova sessão for iniciada.

Ao implementar a funcionalidade deste formulário, é importante lembrar que ele só pode usar o ServerModule, DataModule e qualquer outro formulário temporário que os utilize, mas não o MainForm. De fato, nenhum formulário deve acessar o MainForm, e sim o MainModule.

Usando referências globais de Form e DataModule

Em Application Forms do uniGUI, Application DataModules e MainModule são referenciados com funções globais que retornam uma instância desse objeto para a sessão atual.

Para formulários, essa função substitui a variável padrão criada em aplicações VCL. Ela permite manter a mesma sintaxe para acessar os formulários:

UniForm1.Show;

Chamar a função UniForm1 criará uma instância de TUniForm1 ou retornará a instância existente (tornando-a um singleton por sessão). Pode ser usada para criar e exibir formulários facilmente. Um detalhe importante é que essas funções não devem ser usadas como referências globais para esses formulários ou data modules. Não é recomendável chamar a função global de um formulário a partir de outros formulários ou data modules para qualquer propósito além de exibir esse formulário.

O framework uniGUI gerencia o tempo de vida de todos os anteriores objetos "de aplicação" (formulários e datamodules). Cada sessão/MainModule uniGUI mantém uma lista de formulários e datamodules gerenciados. Uma chamada como UniMainModule.GetFormInstance(TUniForm1) solicita um singleton daquele formulário específico (TUniForm1). Se essa instância não existir, ela será criada. Se a instância já foi criada, ela será retornada.

Além das fábricas singleton, o uniGUI também tenta otimizar o uso de memória. Os application datamodules serão liberados automaticamente ao fechar a sessão (porque não há como saber se estão em uso), mas os formulários são liberados assim que não estiverem em uso. Por exemplo, após sair de uma chamada a ShowModal e receber o resultado, esse formulário pode ser liberado com segurança.

Usar funções para acessar um formulário em vez de variáveis globais fornece duas vantagens:

  • Cada sessão tem acesso ao seu próprio formulário, e apenas um (como um singleton por sessão).

  • Os formulários de aplicação são gerenciados pelo framework uniGUI, tornando-se recursos sob demanda.