O que acontece com um encadeamento desanexado quando main () sai?

152

Suponha que eu esteja iniciando um std::threade depois detach(), para que o encadeamento continue sendo executado, mesmo que o std::threadque antes o representasse saia do escopo.

Suponha ainda que o programa não tenha um protocolo confiável para ingressar no encadeamento desanexado 1 ; portanto, o encadeamento desanexado ainda será executado quando main()sair.

Não consigo encontrar nada no padrão (mais precisamente, no rascunho do N3797 C ++ 14), que descreve o que deveria acontecer, nem 1.10 nem 30.3 contêm palavras pertinentes.

1 Outra pergunta, provavelmente equivalente, é: "um encadeamento desanexado pode ser unido novamente", porque, independentemente do protocolo que você está inventando para ingressar, a parte de sinalização precisa ser feita enquanto o encadeamento ainda está em execução e o agendador do SO pode decida colocar o encadeamento em suspensão por uma hora logo após a sinalização, sem que o destinatário possa detectar com segurança que o encadeamento realmente terminou.

Se a execução de main()threads desanexados estiver executando um comportamento indefinido, qualquer uso de std::thread::detach()é um comportamento indefinido, a menos que o thread principal nunca saia 2 .

Portanto, a execução de main()threads desanexados em execução deve ter efeitos definidos . A questão é: onde (no padrão C ++ , não POSIX, nem documentos do SO, ...) são esses efeitos definidos.

2 Um encadeamento desanexado não pode ser unido (no sentido de std::thread::join()). Você pode aguardar resultados de threads desanexados (por exemplo, através de um futuro de std::packaged_task, ou por um semáforo de contagem ou um sinalizador e uma variável de condição), mas isso não garante que o thread tenha terminado de executar . Na verdade, a menos que você colocar a parte de sinalização para o destruidor do primeiro objeto automático do fio, não será , em geral, seja de código (destruidores) que são executados após o código de sinalização. Se o SO agendar o encadeamento principal para consumir o resultado e sair antes que o encadeamento desanexado conclua a execução dos destruidores, o que ^ Wis definirá para acontecer?

Marc Mutz - mmutz
fonte
5
Só consigo encontrar uma observação não obrigatória muito vaga em [basic.start.term] / 4: "Terminar cada encadeamento antes de uma chamada std::exitou saída mainé suficiente, mas não necessário, para atender a esses requisitos". (o parágrafo inteiro pode ser relevante) Veja também [support.start.term] / 8 ( std::exité chamado quando mainretorna)
dyp

Respostas:

45

A resposta para a pergunta original "o que acontece com um thread desanexado quando main()sai" é:

Ele continua em execução (porque o padrão não diz que está parado) e está bem definido, desde que não toque em nenhuma variável (automática | thread_local) de outros threads nem em objetos estáticos.

Parece ser permitido permitir que os gerenciadores de threads sejam objetos estáticos (observe em [basic.start.term] / 4 diz o mesmo, graças a @dyp pelo ponteiro).

Os problemas surgem quando a destruição de objetos estáticos termina, porque a execução entra em um regime no qual somente o código permitido nos manipuladores de sinais pode ser executado ( [basic.start.term] / 1, 1ª frase ). Da biblioteca padrão C ++, essa é apenas a <atomic>biblioteca ( [support.runtime] / 9, segunda frase ). Em particular, isso - em geral - exclui condition_variable (é definido pela implementação se isso é salvo para uso em um manipulador de sinal, porque não faz parte dele <atomic>).

A menos que você tenha desenrolado sua pilha neste momento, é difícil ver como evitar comportamentos indefinidos.

A resposta para a segunda pergunta "os segmentos desanexados podem ser unidos novamente" é:

Sim, com a *_at_thread_exitfamília de funções ( notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit()...).

Conforme observado na nota de rodapé [2] da pergunta, sinalizar uma variável de condição ou um semáforo ou um contador atômico não é suficiente para unir um encadeamento desanexado (no sentido de garantir que o final de sua execução tenha ocorrido antes do recebimento de referida sinalização por um thread em espera), porque, em geral, haverá mais código executado após, por exemplo, uma notify_all()variável de condição, em particular os destruidores de objetos automáticos e locais de thread.

Executar a sinalização como a última coisa que o encadeamento faz ( depois que os destruidores de objetos automáticos e de encadeamento local ocorreram ) é para o que a _at_thread_exitfamília de funções foi projetada.

Portanto, para evitar um comportamento indefinido na ausência de garantias de implementação acima do exigido pelo padrão, você precisa (manualmente) unir um encadeamento desanexado a uma _at_thread_exitfunção que faz a sinalização ou fazer com que o encadeamento desanexado execute apenas o código que seria seguro para um manipulador de sinal também.

Marc Mutz - mmutz
fonte
17
Você tem certeza disso? Em todos os lugares que eu testei (GCC 5, clang 3.5, MSVC 14), todos os threads desanexados são eliminados quando o thread principal sai.
Rustyx
3
Acredito que a questão não é o que uma implementação específica faz, mas é como evitar o que o padrão define como comportamento indefinido.
Jon Spencer
7
Essa resposta parece implicar que, após a destruição de variáveis ​​estáticas, o processo entrará em algum tipo de estado de espera, aguardando a conclusão de qualquer thread restante. Isso não é verdade, após exitterminar de destruir objetos estáticos, executar atexitmanipuladores, liberar fluxos etc., ele retorna o controle ao ambiente host, ou seja, o processo é encerrado. Se um encadeamento desanexado ainda estiver em execução (e de alguma forma evitou um comportamento indefinido por não tocar em nada fora de seu próprio encadeamento), ele simplesmente desaparece em uma nuvem de fumaça quando o processo é encerrado.
27618 Jonathan Wakely
3
Se você estiver bem usando APIs C ++ não ISO, se as mainchamadas em pthread_exitvez de retornar ou chamar exit, isso fará com que o processo aguarde o término dos threads desanexados e chame exitapós o término do último.
27618 Jonathan Wakely
3
"Continua em execução (porque o padrão não diz que está parado)" -> Alguém pode me dizer COMO um encadeamento pode continuar a execução sem seu processo de contêiner?
Gupta
42

Desanexando Threads

De acordo com std::thread::detach:

Separa o encadeamento de execução do objeto de encadeamento, permitindo que a execução continue independentemente. Quaisquer recursos alocados serão liberados assim que o encadeamento terminar.

De pthread_detach:

A função pthread_detach () deve indicar para a implementação que o armazenamento do encadeamento pode ser recuperado quando o encadeamento termina. Se o thread não tiver terminado, pthread_detach () não fará com que ele termine. O efeito de várias chamadas pthread_detach () no mesmo thread de destino não é especificado.

A desanexação de encadeamentos é principalmente para economizar recursos, caso o aplicativo não precise esperar que um encadeamento seja concluído (por exemplo, daemons, que devem ser executados até o encerramento do processo):

  1. Para liberar o identificador do lado do aplicativo: É possível deixar um std::threadobjeto fora do escopo sem ingressar, o que normalmente leva a uma chamada à std::terminate()destruição.
  2. Para permitir que o sistema operacional limpe os recursos específicos do encadeamento ( TCB ) automaticamente assim que o encadeamento sair, porque especificamos explicitamente que não estamos interessados ​​em ingressar no encadeamento posteriormente, portanto, não é possível ingressar em um encadeamento já desanexado.

Matando Tópicos

O comportamento na finalização do processo é igual ao do encadeamento principal, que pode pelo menos capturar alguns sinais. Se outros threads podem ou não manipular sinais não é tão importante, pois é possível ingressar ou encerrar outros threads na chamada do manipulador de sinais do thread principal. (Questão relacionada )

Como já foi dito, qualquer thread, desanexado ou não, morrerá com seu processo na maioria dos sistemas operacionais . O processo em si pode ser encerrado levantando um sinal, chamando exit()ou retornando da função principal. No entanto, o C ++ 11 não pode nem tenta definir o comportamento exato do sistema operacional subjacente, enquanto os desenvolvedores de uma Java VM certamente podem abstrair essas diferenças até certo ponto. AFAIK, processos exóticos e modelos de encadeamento geralmente são encontrados em plataformas antigas (para as quais o C ++ 11 provavelmente não será portado) e em vários sistemas embarcados, que podem ter uma implementação especial e / ou limitada da biblioteca de idiomas e também suporte limitado à linguagem.

Suporte de Thread

Se os threads não forem suportados std::thread::get_id(), retornará um ID inválido (construído por padrão std::thread::id), pois existe um processo simples, que não precisa de um objeto de thread para ser executado e o construtor de a std::threaddeve lançar a std::system_error. É assim que eu entendo o C ++ 11 em conjunto com os sistemas operacionais atuais. Se houver um sistema operacional com suporte para encadeamento, que não gera um encadeamento principal em seus processos, avise-me.

Controlando Threads

Se for necessário manter o controle sobre um encadeamento para o desligamento adequado, você poderá fazer isso usando primitivas de sincronização e / ou algum tipo de sinalizador. No entanto, nesse caso, definir um sinalizador de desligamento seguido por uma junção é a maneira que eu prefiro, já que não há sentido em aumentar a complexidade desanexando threads, pois os recursos seriam liberados ao mesmo tempo de qualquer maneira, onde os poucos bytes do std::threadobjeto vs. maior complexidade e possivelmente mais primitivas de sincronização devem ser aceitáveis.

Sam
fonte
3
Como cada encadeamento possui sua própria pilha (que está no intervalo de megabytes no Linux), eu escolheria desanexar o encadeamento (para que sua pilha seja liberada assim que sair) e usar algumas primitivas de sincronização se o encadeamento principal precisar sair (e para o desligamento adequado, ele precisa ingressar nos segmentos ainda em execução, em vez de finalizá-los no retorno / saída).
Norbert Bérci
8
Eu realmente não vejo como isso responde à pergunta
MikeMB
18

Considere o seguinte código:

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void thread_fn() {
  std::this_thread::sleep_for (std::chrono::seconds(1)); 
  std::cout << "Inside thread function\n";   
}

int main()
{
    std::thread t1(thread_fn);
    t1.detach();

    return 0; 
}

Executando-o em um sistema Linux, a mensagem do thread_fn nunca é impressa. O sistema operacional realmente limpa thread_fn()assim que main()sai. Substituir t1.detach()por t1.join()sempre imprime a mensagem conforme o esperado.

kgvinod
fonte
Esse comportamento acontece exatamente no Windows. Portanto, parece que o Windows mata os threads desanexados quando o programa é concluído.
Gupta
17

O destino do encadeamento após a saída do programa é um comportamento indefinido. Mas um sistema operacional moderno limpará todos os threads criados pelo processo ao fechá-lo.

Ao desanexar um std::thread, essas três condições continuarão válidas:

  1. *this não possui mais nenhum segmento
  2. joinable() será sempre igual a false
  3. get_id() será igual std::thread::id()
César
fonte
1
Por que indefinido? Porque o padrão não define nada? Pela minha nota de rodapé, isso não faria nenhuma chamada para detach()ter um comportamento indefinido? Difícil de acreditar ...
Marc Mutz - mmutz
2
@ MarcMutz-mmutz É indefinido no sentido de que, se o processo terminar, o destino do encadeamento é indefinido.
César
2
@ Caesar e como garantir que não saia antes que o thread termine?
MichalH
0

Para permitir que outros encadeamentos continuem a execução, o encadeamento principal deve terminar chamando pthread_exit () em vez de exit (3). É bom usar pthread_exit em main. Quando pthread_exit é usado, o encadeamento principal para de executar e permanece no status zumbi (extinto) até que todos os outros encadeamentos sejam encerrados. Se você estiver usando pthread_exit no thread principal, não poderá obter o status de retorno de outros threads e não poderá fazer a limpeza de outros threads (isso pode ser feito usando pthread_join (3)). Além disso, é melhor desanexar threads (pthread_detach (3)) para que os recursos do thread sejam liberados automaticamente no término do thread. Os recursos compartilhados não serão liberados até que todos os threads saiam.

yshi
fonte
@kgvinod, por que não adicionar "pthread_exit (0);" após o "ti.detach ()";
Yshi 13/05/19
Referência: stackoverflow.com/questions/3559463/…
yshi 13/05/19