C'est une question complique parceque fondamentalement c'est une question de concurrence. Et les questions de concurrences sont toujours compliques. Je vais donne des descriptions et des implications a plusieurs niveau de detail. Note que je ne fais pas de C# donc certains details pourraient etre legerement differents mais fondamentalement les principes sont les memes.
Basique:
Quand tu utilises un socket synchrone, les appels de fonction a l'objet socket (ou la plupart) sont bloquant et sont traite par le thread qui fait l'appel de fonction. Ca veut dire que ce thread la ne peut rien faire d'autre tant que l'appel n'est pas resolu.
Quand tu utilises un socket asynchrone, les appels de fonction a l'objet socket sont resolution dans un thread a part qui execute une fonction callback quand l'appel est resolu. Ca veut dire que le thread qui a initie l'appel peut faire autre choses.
Moyen:
Avec un socket synchrone, le thread qui fait l'appel ne peut rien faire d'autre. Mais ca ne veut pas dire que d'autres threads ne peuvent pas faire quelquechose.
Avec un socket asynchrone, il faut se rappeller que les fonctions callback sont execute dans un thread different du thread qui a fait l'appel a l'objet socket. Et donc il faut etre conscient de l'existence de data race et de ce que ca veut dire pour ton application. En particulier, ca veut dire proteger les structures de donnees avec des mutex/semaphore/moniteur/whatever. Mais ca veut aussi dire dans le cadre d'un jeu que tu peux avoir une activite reseau qui arrive pendant l'execution d'une frame du jeu. Probablement, tu ne veux pas ca, et il faut probablement buffer les informations qui viennent du reseau pourqu'elles soient toute traite a la frame d'apres.
Avance:
D'apres ce tuto de MS ( https://docs.microsoft.com/en-us/dotnet/framework/network-programming/using-an-asynchronous-server-socket ), les differentes phases des sockets sont execute par des threads different dans le thread pool quand on utilise des threads asynchrones. Ca a l'air de vouloir dire que chaque socket peut potentiellement consomme plusieurs thread du thread pool systeme. Et ca peut etre une tres mauvaise nouvelle quand on passe a l'echelle. Si tu as 3000 joueurs sur ton serveur et que l'implementation cree 3000 threads, ca peut mal finir pour ton applications parcequ'il y a trop de thread actif en meme temps qui viennent d'un pool trop petit. Tu peux avoir des phenomenes de famine.
On pourrait etre tente de mettre tous les sockets dans un thread que l'on controlle et de faire du select ( https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.select?view=netcore-3.1 ) pour conserver le nombre de thread. Ca va afire tourner tous les sockets dans le meme thread, ce qui limite le nombre de thread utilise par le reseau et donne plus de parallelisme au reste de l'application. Mais ca a le desavantage qu'on a perdu le parallelisme du traitement des sockets et que du fait le traitement des sockets peut devenir un point de contention dans l'application, en particulier si le traitement qui suit la reception d'un message est couteux. Aussi on pourrait s'exposer a un mauvais agent qui spam le reseau et degrade la performance des autres sockets.
On veut du fait probablement mettre grouper les socket par (disons) 10 et faire un thread a part pour chaque groupe. On a toujours la possibilite d'avoir un socket du groupe qui impacte les autre groupe, mais on a gagner du parallelisme globalement et ca permet de controller exactement combien de resource processeurs est attribuer a gerer le reseau. Il faut probablement faire un peu attention a comment on construit les groupes. On peut vouloir par exemple mettre tous les joueurs de la meme equipe dans le meme groupe de facon a ce que si un joueur essaye de spammer le serveur il va penaliser les joueurs de son equipe. Et du coup probablement il ne fera pas ca.