Apartamentos de Thread Única

Aviso

Risco de deadlock com async/await e chamadas bloqueantes em threads STA: Em código gerenciado (.NET), o loop de mensagens STA é crítico para o despacho de chamadas COM. Bloquear uma thread STA com Task.Wait(), Task.Result, Thread.Sleep() ou ManualResetEvent.WaitOne() impede que callbacks COM e chamadas entre apartamentos sejam concluídas — causando um deadlock.

// ❌ DEADLOCK — blocks the STA message loop
[STAThread]
static void Main()
{
    var result = GetDataAsync().Result; // Deadlock: awaited continuation
                                        // can't post back to this STA thread
}

// ✅ Correct — use async entry point or pump messages
[STAThread]
static async Task Main()
{
    var result = await GetDataAsync(); // continuation resumes on STA via SynchronizationContext
}

Se você não puder usar await, use CoWaitForMultipleHandles (nativo) ou um loop de mensagens/frame dispatcher (gerenciado) em vez de primitivas de espera brutas em threads STA.

O uso de apartamentos de thread única (o processo do modelo de apartamento) oferece um paradigma baseado em mensagens para lidar com vários objetos em execução simultânea. Ele permite que você escreva um código mais eficiente permitindo que um thread, enquanto aguarda a conclusão de alguma operação demorada, permita que outro thread seja executado.

Cada thread em um processo inicializado como um processo do modelo de apartamento, que recupera e despacha mensagens de janela, é uma thread de apartamento de thread única. Cada thread reside em seu próprio apartamento. Dentro de um apartamento, ponteiros de interface podem ser passados sem serialização (marshaling), e, portanto, todos os objetos em uma thread de apartamento de thread única se comunicam diretamente.

Um agrupamento lógico de objetos relacionados que executam na mesma thread e, portanto, devem ter execução síncrona, pode residir na mesma thread de apartamento de thread única. No entanto, um objeto de modelo de apartamento não pode residir em mais de um thread. Chamadas a objetos em outras threads devem ser feitas dentro do contexto da thread proprietária; portanto, o COM distribuído alterna as threads automaticamente quando você chama um proxy.

Os modelos entre processos e entre threads são semelhantes. Quando é necessário passar um ponteiro de interface para um objeto em outro apartamento (em outra thread) dentro do mesmo processo, você usa o mesmo modelo de serialização que objetos em processos diferentes usam para passar ponteiros entre limites de processo. Obtendo um ponteiro para o objeto de serialização padrão, você pode serializar ponteiros de interface entre limites de thread (entre apartamentos) da mesma forma que faz entre processos. (Os ponteiros de interface devem ser serializados quando passados entre apartamentos.)

As regras para apartamentos de thread única são simples, mas é importante segui-las cuidadosamente:

  • Cada objeto deve residir em apenas uma thread (dentro de um apartamento de thread única).
  • Inicialize a biblioteca COM para cada thread.
  • Serialize todos os ponteiros para objetos ao passá-los entre apartamentos.
  • Cada apartamento de thread única deve ter um loop de mensagens para lidar com chamadas de outros processos e apartamentos dentro do mesmo processo. Apartamentos de thread única sem objetos (somente cliente) também precisam de um loop de mensagens para despachar as mensagens de broadcast que alguns aplicativos usam.
  • Objetos baseados em DLL ou em processo não chamam as funções de inicialização COM; em vez disso, eles registram seu modelo de threading com o valor nomeado ThreadingModel na chave InprocServer32 no registro. Objetos com reconhecimento de apartamento também devem escrever pontos de entrada de DLL com cuidado. Existem considerações especiais que se aplicam ao uso de threads em servidores em processo. Para obter mais informações, consulte Problemas de threading do servidor no processo.

Embora vários objetos possam residir em um único thread, nenhum objeto de modelo de apartamento pode viver em mais de um thread.

Cada thread de um processo cliente ou de um servidor executado fora do processo deve chamar CoInitialize ou chamar CoInitializeEx e especificar COINIT_APARTMENTTHREADED para o parâmetro dwCoInit. O apartamento principal é a thread que chama CoInitializeEx primeiro. Para obter informações sobre servidores em processo, consulte Problemas de Threading em Servidores em Processo.

Todas as chamadas a um objeto devem ser feitas em sua thread (dentro de seu apartamento). É proibido chamar um objeto diretamente de outra thread; usar objetos dessa maneira, sem threads, pode causar problemas para os aplicativos. A implicação dessa regra é que todos os ponteiros para objetos devem ser serializados (marshaled) quando passados ​​entre apartamentos. O COM fornece as duas funções a seguir para esta finalidade:

Essas funções encapsulam chamadas às funções CoMarshalInterface e CoUnmarshalInterface, que exigem o uso do sinalizador MSHCTX_INPROC.

Em geral, o empacotamento é realizado automaticamente pelo COM. Por exemplo, ao passar um ponteiro de interface como parâmetro em uma chamada de método em um proxy para um objeto em outro apartamento, ou ao chamar CoCreateInstance, o COM realiza a serialização automaticamente. No entanto, em alguns casos especiais, em que o desenvolvedor do aplicativo está passando ponteiros de interface entre apartamentos sem usar os mecanismos normais do COM, o desenvolvedor deve lidar com a serialização manualmente.

Se um apartamento (Apartamento 1) em um processo tiver um ponteiro de interface e outro apartamento (Apartamento 2) exigir seu uso, o Apartamento 1 deverá chamar CoMarshalInterThreadInterfaceInStream para serializar a interface. O fluxo criado por esta função é thread-safe e deve ser armazenado em uma variável acessível pelo Apartamento 2. O Apartamento 2 deve passar este fluxo para CoGetInterfaceAndReleaseStream para desserializar a interface e receberá de volta um ponteiro para um proxy através do qual poderá acessar a interface. O apartamento principal deve permanecer ativo até que o cliente tenha concluído todo o trabalho COM (porque alguns objetos em processo são carregados no apartamento principal, conforme descrito em Problemas de Threading do Servidor em Processo). Depois que um objeto for passado entre threads dessa maneira, é muito fácil passar ponteiros de interface como parâmetros. Dessa forma, o COM distribuído realiza a serialização e a troca de threads para o aplicativo.

Para lidar com chamadas de outros processos e apartamentos dentro do mesmo processo, cada apartamento de thread única deve ter um loop de mensagens. Isso significa que a função de trabalho do thread deve ter um loop GetMessage/DispatchMessage. Se outros primitivos de sincronização estiverem sendo usados para se comunicar entre threads, a função MsgWaitForMultipleObjects poderá ser usada para aguardar mensagens e eventos de sincronização de thread. A documentação dessa função tem um exemplo desse tipo de loop de combinação.

O COM cria uma janela oculta usando a classe do Windows "OleMainThreadWndClass" em cada apartamento de thread única. Uma chamada a um objeto é recebida como uma mensagem de janela para esta janela oculta. Quando o apartamento do objeto recupera e despacha a mensagem, a janela oculta a receberá. Em seguida, o procedimento de janela chamará o método de interface correspondente do objeto.

Quando vários clientes chamam um objeto, as chamadas são enfileiradas na fila de mensagens e o objeto receberá uma chamada cada vez que seu apartamento recuperar e despachar mensagens. Como as chamadas são sincronizadas por COM e as chamadas são sempre entregues pelo thread que pertence ao apartamento do objeto, as implementações de interface do objeto não precisam fornecer sincronização. Apartamentos de thread única podem implementar IMessageFilter para permitir que cancelem chamadas ou recebam mensagens de janela quando necessário.

O objeto pode ser reentrado se uma de suas implementações de método de interface recuperar e despachar mensagens ou fizer uma chamada ORPC para outra thread, fazendo com que outra chamada seja entregue ao objeto (pelo mesmo apartamento). OLE não impede a reentrância na mesma thread, mas pode ajudar a fornecer segurança de thread. Isso é idêntico à maneira como um procedimento de janela pode ser reentrado se recuperar e despachar mensagens enquanto processa uma mensagem. No entanto, chamar um servidor de apartamento de thread única fora do processo que chama outro servidor de apartamento de thread única permitirá que o primeiro servidor seja reentrado.

Acessando interfaces em apartamentos

Escolher o modelo de encadeamento

Apartamentos com múltiplos threads

Problemas de encadeamento do servidor em processo

Processos, threads e apartamentos

Thread único e comunicação com múltiplos threads