Q:

Entendendo threads em C#

Estou tentando entender como usar Tasks e métodos assíncronos em C#, mas todo lugar que eu vejo sobre isso na internet menciona uma forma "multithreading usual" que seria diferente do uso de tasks.
O problema é que até agora eu não entendi muito bem como funcionam as threads, eu só sei que elas seriam intuitivamente "caminhos de execução do código".
Dessa forma, como entender corretamente threads em C#? O que são e como trabalhamos com essas threads? Eu acredito que essa pergunta seja análoga à essa que fiz, mas considerando agora a relação com o processador e não com a memória.

A:

Utilizar threads em C#, é semanticamente similar a utilizar threads em Java, C ou C++, o que muda é a forma (sintaxe) com que se criam/utilizam as threads nas diferentes linguagens.
Responder à pergunta sobre como utilizar threads, de uma forma exaustiva e em âmbito geral, requer mais espaço e tempo do que o fornecido aqui, visto que existem cursos e disciplinas inteiras dedicadas a isso.
Contudo, esses são alguns livros conhecidos, utilizados em cursos de graduação, que podem dar uma boa iluminada no assunto (existem outros, mas esses são os mais comuns):

Sistemas Distribuídos - Princípios e Paradigmas do Tanenbaum
Fundamentos de Sistemas Operacionais do Silberschatz
Sistemas Operacionais Modernos do Tanenbaum

Agora, aproveitando o espaço, de maneira simplificada, você pode imaginar uma thread como um trecho de código que é executado em paralelo a seu código, sendo que podem existir diversas threads executando em um dado momento.
A partir do momento que você cria uma thread, o Sistema Operacional fica sabendo que, além de tudo que já está fazendo, há mais código que precisa ser executado. Se existem mais threads do que processadores/núcleos (que é o caso mais visto nos computadores pessoais e dispositivos móveis), então o Sistema Operacional começa a "agendar um momento" para que cada thread execute em um determinado núcleo/processador (processo conhecido como escalonamento, ou agendamento).
Por exemplo, supondo que você tenha um computador com 4 núcleos, e o Sistema Operacional tenha 8 threads atualmente sendo executadas (TA, TB, TC, TD, TE, TF, TG e TH), então um possível escalonamento das threads pode ser como o da figura abaixo:

Aqui, pode-se ver que as threads TA e TB compartilham o Núcleo 0, TC e TD compartilham o Núcleo 1 e assim por diante, de modo que cada thread utiliza 10 milissegundos de cada núcleo, para então "ceder a vez" para outra thread que está esperando. Apesar de existirem 8 threads, apenas 4 são executadas simultaneamente no processador, enquanto que as outras 4 ficam aguardando (sem consumir recursos de processamento).
É claro que isso é um exemplo simples. Na vida real, o escalonamento não é tão simétrico, e depende de uma série de fatores, como a prioridade da thread (quando mais privilégio, mais tempo ela tem direito de usar do processador), ou até mesmo se a thread está esperando por um evento externo ocorrer, o que com que ela fique no estado de espera por muito mais tempo. Esse, aliás, é o motivo que faz com que as threads sejam tão importantes nos sistemas atuais.
Por exemplo, imagine que em um trecho do seu código você pede para gravar um texto em um arquivo (código em C#):
public void GravaArquivo(...) {
    //Bloco A
    File.WriteAllText(...);
    //Bloco B
}

O código no Bloco B não será executado até que o método WriteAllText termine de executar. Isso é conhecido como sincronismo: um evento só ocorre depois do anterior ter terminado por completo. Ou seja, do ponto de vista do método GravaArquivo a execução desse código "trava" na linha File.WriteAllText(...); até que o arquivo tenha sido gravado em disco.
Por mais breve que possa parecer para nós, humanos, esse tempo é muito grande para o processador, por isso mesmo o Sistema Operacional deixa a thread executando o método GravaArquivo em estado de espera, até que o disco reporte o término da operação. Esse é o evento externo, mencionado anteriormente.
É nesse ponto que a criação de threads extras se faz necessária.
Imagine o cenário da figura abaixo, onde você clica no botão "Salvar" de um programa:

O código do botão "Salvar" irá executar na thread responsável por toda a interface com o usuário do programa, ou seja, enquanto a execução do código do botão Salvar não terminar, não é possível clicar em outro botão daquele programa, utilizar comandos do teclado naquele programa, ou até mesmo, atualizar o visual da janela do programa (tudo parte da interface com o usuário, assim como o botão Salvar)!
Quando a interface com o usuário para de responder por um determinado tempo, os sistemas operacionais começam a tomar providências, pois eles não têm como saber se o código está travado de verdade (bug), ou se está apenas demorando. Daí vêm os avisos de "Não respondendo" do Windows, ou aquela janelinha de ANR do Android.
Para evitar esses cenários, as boas práticas de programação pedem que tarefas demoradas, como ler/gravar um arquivo, enviar/receber dados via rede etc, sejam executadas em threads diferentes da thread utilizada para processar a interface com o usuário. É nesse momento que você precisará criar threads extras.
É claro que existem bibliotecas e classes já prontas para facilitar essa tarefa em todas as linguagens modernas, como a classe Task do C#, ou até mesmo suas novas construções async e await.
Por mais que simplifiquem ou "disfarcem" o uso de threads extras, as threads estão lá, e a semântica de uso é a mesma. O que muda é a quantidade de código que você terá que escrever.
Assim, para criar uma thread em C#, você pode utilizar meios clássicos, como:
...
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        ...
        private Thread thread;
        private void CorpoDaThread()
        {
            //Código que será executado em paralelo ao resto do código
        }
        private void button1_Click(object sender, EventArgs e)
        {
            //Cria uma nova thread, indicando qual método essa thread deverá executar
            thread = new Thread(CorpoDaThread);
            //Inicia a execução da thread (em paralelo a esse código)
            thread.Start();
        }
    }
}

Pode criar threads utilizando lambdas:
...
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        ...
        private Thread thread;
        private void button1_Click(object sender, EventArgs e)
        {
            //Cria uma nova thread, indicando qual método essa thread deverá executar
            thread = new Thread(() => {
                //Código que será executado em paralelo ao resto do código
            });
            //Inicia a execução da thread (em paralelo a esse código)
            thread.Start();
        }
    }
}

Ou pode utilizar Task, BackgroundWorker, async e await etc.
O importante é ter em mente que, no Windows usado nos PC's e outros sistemas operacionais que não são de tempo real, não é possível saber com 100% de certeza quando uma thread começará a ser executada de verdade. Só quem sabe disso é o Sistema Operacional.
Por exemplo, não se pode afirmar com 100% de garantia que o código logo abaixo da instrução thread.Start() irá executar ou antes ou depois do código dentro dessa nova thread sendo iniciada.
É para isso que existem diversos mecanismos para sincronizar a execução entre threads concorrentes (threads que estão executando em paralelo), o que inclui conceitos como Critical Section, Join, Event, Semaphore, Wait, Sleep, etc.
Esse assunto é muito extenso, e realizar operações fora da thread de interface com o usuário não é o único motivo pelo qual alguém cria threads extras.
Você pode criar threads para dividir o processamento de uma tarefa. Por exemplo, para buscar um elemento qualquer em um vetor desordenado, se o computador tiver condições de executar mais de uma thread simultaneamente, você pode dividir a busca entre duas threads (cada uma busca o elemento em uma metade do vetor), de modo que o tempo final será aprox. a metade do tempo que o algoritmo de busca normal levaria. Isso pode ser fieto com três, quatro, cinco ou mais threads.
Em um jogo, você pode dividir o processamento da cena entre várias threads, por exemplo, enquanto uma thread cuida do áudio, outra cuida da rede, e uma terceira da parte gráfica.
Enfim, é um assunto bem extenso, mas muitíssimo interessante!
Dominar o conceito de threads, hoje, é fundamental! Não basta apenas saber utilizar essa ou aquela classe/biblioteca. Eu recomendo se aprofundar mais nesse assunto, que o tempo gasto não será em vão :D

A:

Você falou de "assíncrono". Deixa eu te perguntar: você sabe a diferença técnica de um processo assíncrono para um processo síncrono?
Basicamente, o processo síncrono acontece com concorrência; o assíncrono não possui essa necessidade por si só – eu posso te falar algo sem que você esteja esperando ouvir.
Um exemplo bem bacana que li no meta do SOPT – não lembro o autor – é o caso do telefone: nós dizemos "alô?" esperando obter uma resposta para que só assim nós continuemos o processo de conversação; quando a conversa se encerra, para confirmação de tal fato, nós nos despedimos com um "até logo" ou, mais simples ainda, com um "tchau!" – isso tudo é para garantir que a concorrência está à par da ação subsequente.

As threads são formas de levar ou dividir informações de um ponto de origem até um destino dentro de um fluxo. Por exemplo, você quer salvar string na memória. Pra fazer isso, uma ou mais threads são responsáveis para que a tarefa de "salvar" seja concluída com êxito.
Lembra do que falávamos sobre síncrono e assíncrono?
Pois bem, você mencionou o tal do "multithreading": esse cara é assíncrono, o que significa que você pode ter várias threads paralelas que dividem a responsabilidade pelas tarefas para elas designadas. Em outras palavras, threads assíncronas e múltiplas conseguem fazer operações sem a necessidade de que uma thread anteriormente executada seja por fim finalizada.
Threads síncronas executam só e somente só uma operação por vez e nenhuma é engatilhada enquanto a anterior estiver sendo processada.
Piscar e escovar os dentes é perfeitamente possível e são processos assíncronos e independentes. Neste caso, teríamos um exemplo trivial de multithreading.
Agora, escovar os dentes e assoviar/assobiar opera sobre uma thread apenas –  é preciso que um processo seja parado/concluído antes que o outro comece.

