Threads e Thread-Local Storage
Multitasking já traça a linha básica entre processo e thread numa seção curta; este artigo permanece nessa mesma distinção e vai mais fundo no que uma thread de fato é por baixo, no que exatamente é compartilhado entre threads do mesmo processo versus o que cada uma mantém privado, e em como thread-local storage dá a cada thread sua própria cópia do que parece, a partir do código-fonte, uma variável global comum.
O que é compartilhado e o que não é
Seção intitulada “O que é compartilhado e o que não é”Toda thread pertencente ao mesmo processo compartilha o espaço de endereçamento virtual desse processo por completo: o mesmo código, o mesmo heap, os mesmos arquivos mapeados em memória, e a mesma tabela de descritores de arquivo abertos, então um arquivo que uma thread abre é imediatamente visível para toda thread irmã sem mecanismo de compartilhamento explícito algum necessário. O que cada thread mantém privado é exatamente o que uma troca de contexto de fato precisa salvar e restaurar em seu nome: sua própria stack, seus próprios registradores de propósito geral salvos, e sua própria área de thread-local storage, descrita abaixo. A stack de uma thread é em si apenas uma região comum dentro do espaço de endereçamento compartilhado, não um espaço de endereçamento separado próprio, o que significa que nada em nível de hardware impede uma thread de ler ou corromper a stack de outra através de um ponteiro perdido; o isolamento entre stacks de threads é uma convenção que o código impõe a si mesmo, não uma fronteira de proteção que a CPU ou as tabelas de página estejam impondo da forma que fariam entre dois processos separados.
Thread-local storage
Seção intitulada “Thread-local storage”Variáveis globais comuns são inadequadas para qualquer coisa da qual uma thread precise de sua própria cópia, uma variável de relato de erro como o errno do C sendo o caso clássico, já que toda thread compartilhando o mesmo espaço de endereçamento de outra forma estaria lendo e escrevendo exatamente o mesmo local de memória e pisando nos valores umas das outras. Thread-local storage (TLS) resolve isso dando a cada thread sua própria cópia privada de variáveis marcadas especialmente, alcançadas através de uma indireção em vez de um endereço fixo: em x86-64, os registradores de segmento FS ou GS (sua base, especificamente, definida através das MSRs FS_BASE/GS_BASE em vez de através de um descritor de segmento real da forma que real mode ou protected mode os usariam) apontam para uma pequena área de memória por thread, e acessos a variáveis TLS compilam para um load ou store num offset fixo a partir dessa base de segmento em vez de a partir de um endereço absoluto fixo.
__thread int error_code; // uma instância por thread, não compartilhada
void set_error(int code) { error_code = code; // compila para um store relativo a FS ou GS}mov eax, fs:[error_code@tpoff] ; carrega a própria cópia desta threadA área de TLS de toda thread é alocada separadamente quando essa thread é criada, e uma troca de contexto entre duas threads de processos diferentes (ou até, em alguns designs, threads irmãs) precisa recarregar a MSR de base de segmento relevante como parte da troca, de forma que a mesma instrução relativa a TLS resolva para um local físico diferente dependendo de qual thread está rodando no momento, sem que o código compilado em si precise saber ou se importar com qual thread é essa.
O que de fato muda numa troca de thread
Seção intitulada “O que de fato muda numa troca de thread”Como threads irmãs já compartilham um espaço de endereçamento, trocar entre elas pode pular a etapa que domina o custo de trocar entre dois processos sem relação: recarregar CR3, o que, como Context Switching já cobre, esvazia a TLB e produz uma rajada de cache misses de outra forma evitáveis nos próximos acessos de memória. Um kernel escalonando threads do mesmo processo em sequência se beneficia diretamente disso: o salvamento e restauração de registradores de propósito geral, e o reload da MSR de base de segmento para TLS, ainda ambos precisam acontecer, mas a troca de espaço de endereçamento, o componente único mais caro de uma troca entre processos, não, o que é parte significativa do motivo pelo qual designs baseados fortemente em threads costumam ser mais rápidos na prática do que um design equivalente construído a partir de muitos processos pequenos separados.
Notas de implementação
Seção intitulada “Notas de implementação”A stack de uma thread, diferente da de um processo, não é automaticamente posicionada e dimensionada por qualquer mecanismo que monta a imagem inicial do processo; ela precisa ser explicitamente alocada (comumente a partir do heap, ou de uma região mapeada dedicada) por qualquer chamada de criação de thread que a inicia, com um tamanho fixo escolhido de antemão, já que diferente da stack original de um processo não há fronteira de espaço de endereçamento separada alguma naturalmente limitando o quanto ela poderia crescer. Escolher esse tamanho pequeno demais produz um estouro de stack que, sem uma guard page mapeada logo após o fim da stack para capturá-lo, silenciosamente corrompe qualquer memória que esteja adjacente (frequentemente a stack de outra thread ou o heap) em vez de falhar de forma limpa no momento em que o estouro de fato acontece.
Referências
Seção intitulada “Referências”- ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Capítulo 3.4.4: descreve o uso da base de segmento
FS/GSpara dados thread-local em x86-64. - ^ D. Butenhof, Programming with POSIX Threads, Capítulo 5: cobre thread-local storage e alocação de stack por thread do lado da API de threading POSIX.
Ver também
Seção intitulada “Ver também”- Multitasking: a seção que primeiro introduz a distinção entre processo e thread que este artigo aprofunda.
- Context Switching: o que especificamente precisa ser trocado (ou pode ser pulado) ao mover entre threads irmãs.
- Model-Specific Registers: o mecanismo
FS_BASE/GS_BASEsobre o qual thread-local storage é construído.