Veja na animação acima que as duas Thread ficam ocupadas aguardando a resposta do Banco de Dados. Enquanto aguardam a resposta não fazem o uso de processamento da CPU. Apenas aguardam a resposta.
Quando uma Thread está executando uma operação de I/O o SO percebe que a Thread está aguardando essa resposta. Por esse motivo, o Windows pausa a Thread para que não use recursos da CPU. Porém, ao fazer isso, ele ainda está na memória e a Thread não pode ser usado para atender outras Requests. Como consequëncia, o IIS precisa associar novas Threads para as Requests subsequentes.
Código assíncrono resolve o problema de Thread Blocking. Faz isso liberando a Thread até que a operação de I/O seja concluida. Assim você recebe um objeto Task que representa o resultado da operação assíncrona. Quando a operação de I/O termina, o Windows notifica o Runtime e a continuação da Task é agendada no Thread Pool para voltar a ser processada.
Ao marcar um método como async, o compilador gera uma máquina de estado (TAP) em segundo plano. É um código extra. Mas se escrever um código assíncrono com sabedoria e estável, o overhead necessário para criar essa estrutura será compensado com os benefícios da execução assíncrona.
Em contraste ao exemplo anterior, com execução assíncrona o Servidor poderia lidar com mais Requests, sem que estas fiquem esperando na Queue do IIS. Isso só é possivel pois as Threads são liberadas para a Thread Pool enquanto a Request esta aguardando a resposta do I/O. Assim é possivel atender a um numero maior de requisições com o mesmo número de Threads.
O async não é uma bala de prata que pode resolver todos os problemas de performance. Veja abaixo casos que o async não resolve o problema, e pior, pode piorar em alguns cenários.
Uma request que apenas faz uso de CPU, necessariamente faz uso de uma Thread para executar. Em uma request que faz uso de I/O é o Windows que sinaliza e tira a prioridade da Thread até que tenha uma resposta.