Async Programming for Game Servers
Implement efficient asynchronous patterns for scalable game server code.
Async Models Comparison
| Model | Language | Overhead | Complexity | Best For |
|---|
| async/await | C#, JS, Rust | Low | Low | I/O bound |
| Coroutines | C++20, Kotlin | Very Low | Medium | High performance |
| Goroutines | Go | Very Low | Low | Concurrent services |
| Futures | C++, Java | Medium | Medium | Composable async |
| Reactive | All | Low | High | Stream processing |
C# async/await
public class GameServer
{
private readonly SemaphoreSlim _connectionLimit = new(1000);
private readonly CancellationTokenSource _cts = new();
public async Task HandlePlayerAsync(
Player player,
CancellationToken cancellationToken = default)
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, _cts.Token);
var token = linkedCts.Token;
try
{
await _connectionLimit.WaitAsync(token);
while (player.Connected && !token.IsCancellationRequested)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(
token, timeoutCts.Token);
var message = await player.ReadMessageAsync(combinedCts.Token);
var result = await ProcessCommandAsync(message, token);
await player.SendAsync(result, token);
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
}
finally
{
_connectionLimit.Release();
}
}
private async Task<GameState> ProcessCommandAsync(
Message msg,
CancellationToken token)
{
var validationTask = ValidateAsync(msg, token);
var playerDataTask = LoadPlayerDataAsync(msg.PlayerId, token);
var permissionsTask = CheckPermissionsAsync(msg.PlayerId, token);
await Task.WhenAll(validationTask, playerDataTask, permissionsTask);
return ApplyCommand(msg,
await validationTask,
await playerDataTask,
await permissionsTask);
}
public async Task GracefulShutdownAsync()
{
_cts.Cancel();
await Task.Delay(TimeSpan.FromSeconds(30));
}
}
Go Goroutines
type Server struct {
listener net.Listener
players sync.Map
wg sync.WaitGroup
quit chan struct{}
semaphore chan struct{}
}
func NewServer(maxConnections int) *Server {
return &Server{
quit: make(chan struct{}),
semaphore: make(chan struct{}, maxConnections),
}
}
func (s *Server) AcceptLoop() {
for {
conn, err := s.listener.Accept()
if err != nil {
select {
case <-s.quit:
return
default:
log.Printf("Accept error: %v", err)
continue
}
}
select {
case s.semaphore <- struct{}{}:
s.wg.Add(1)
go s.handlePlayer(conn)
default:
conn.Close()
}
}
}
func (s *Server) handlePlayer(conn net.Conn) {
defer func() {
<-s.semaphore
s.wg.Done()
conn.Close()
}()
player := NewPlayer(conn)
s.players.Store(player.ID, player)
s.players.Delete(player.ID)
readChan := ( Message, )
writeChan := ( Message, )
errChan := ( , )
{
errChan <- player.readLoop(readChan)
}()
{
errChan <- player.writeLoop(writeChan)
}()
{
{
msg := <-readChan:
result := s.processMessage(msg)
writeChan <- result
err := <-errChan:
err != && !errors.Is(err, io.EOF) {
log.Printf(, player.ID, err)
}
<-s.quit:
}
}
}
Shutdown(ctx context.Context) {
(s.quit)
s.listener.Close()
done := ( {})
{
s.wg.Wait()
(done)
}()
{
<-done:
<-ctx.Done():
ctx.Err()
}
}
C++20 Coroutines
#include <coroutine>
#include <optional>
template<typename T>
class Task {
public:
struct promise_type {
T value;
std::exception_ptr exception;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T v) { value = std::move(v); }
void unhandled_exception() { exception = std::current_exception(); }
};
bool await_ready() { return handle.done(); }
void await_suspend(std::coroutine_handle<> h) { }
T await_resume() {
(handle.().exception)
std::(handle.().exception);
std::(handle.().value);
}
:
std::coroutine_handle<promise_type> handle;
};
{
:
{
(conn.()) {
msg = conn.();
result = (msg);
conn.(result);
}
}
{
[validation, playerData] = (
(msg),
(msg.playerId)
);
;
}
};
< T>
{
:
{
T current_value;
{ }
{
current_value = std::(value);
{};
}
};
{ };
{ }
{ }
};
{
(running) {
();
();
currentState;
}
}
Rust async/await
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, Semaphore};
use std::sync::Arc;
struct GameServer {
max_connections: Arc<Semaphore>,
shutdown: tokio::sync::broadcast::Sender<()>,
}
impl GameServer {
async fn run(&self, listener: TcpListener) -> Result<()> {
let mut shutdown_rx = self.shutdown.subscribe();
loop {
tokio::select! {
result = listener.accept() => {
let (socket, addr) = result?;
let permit = self.max_connections.clone().acquire_owned().await?;
let shutdown_rx = self.shutdown.subscribe();
tokio::spawn(async move {
let _permit = permit;
if let Err(e) = handle_player(socket, shutdown_rx).await {
(, e);
}
});
}
_ = shutdown_rx.() => {
();
;
}
}
}
(())
}
}
(
stream: TcpStream,
shutdown: tokio::sync::broadcast::Receiver<()>
) <()> {
(reader, writer) = stream.();
= BufReader::(reader);
= BufWriter::(writer);
{
tokio:: {
result = (& reader) => {
= result?;
= (&msg).;
(& writer, &response).?;
}
_ = shutdown.() => {
;
}
}
}
(())
}
(items: <Item>) <> {
= Arc::(Semaphore::());
: <_> = items.().(|item| {
= semaphore.();
tokio::( {
= permit.()..();
(item).
})
}).();
futures::future::(tasks)
.
.()
.(|r| r.())
.()
}
Best Practices
| Practice | Benefit |
|---|
| Avoid blocking in async | Prevents thread starvation |
| Use cancellation tokens | Clean shutdown |
| Limit concurrency | Prevent resource exhaustion |
| Structured concurrency | Proper error handling |
| Backpressure handling | Prevent memory overflow |
Troubleshooting
Common Failure Modes
| Problem | Root Cause | Solution |
|---|
| Thread starvation | Blocking in async | Use spawn_blocking |
| Memory leak | Unbounded channels | Bounded channels |
| Deadlock | Sync in async context | Async-aware locks |
| Event loop lag | Long-running tasks | Break into smaller tasks |
Debug Checklist
node --trace-warnings --inspect server.js
curl http://localhost:6060/debug/pprof/goroutine?debug=1
tokio-console http://localhost:6669
dotnet counters monitor --process-id <pid> System.Runtime
const { monitorEventLoopDelay } = require('perf_hooks');
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
setInterval(() => {
console.log(`Event loop lag: p99=${histogram.percentile(99)}ms`);
}, 5000);
Unit Test Template
[Fact]
public async Task HandlePlayer_ProcessesMessages()
{
var server = new GameServer();
var mockPlayer = new MockPlayer();
mockPlayer.EnqueueMessage(new MoveCommand { Direction = Vector3.Forward });
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await server.HandlePlayerAsync(mockPlayer, cts.Token);
Assert.Single(mockPlayer.SentMessages);
Assert.IsType<GameState>(mockPlayer.SentMessages[0]);
}
[Fact]
public async Task ProcessCommand_RunsInParallel()
{
var server = new GameServer();
var stopwatch = Stopwatch.StartNew();
var result = await server.ProcessCommandAsync(new Message());
stopwatch.Stop();
Assert.True(stopwatch.ElapsedMilliseconds < 200);
}
[Fact]
public async Task GracefulShutdown_WaitsForConnections()
{
var server = new GameServer();
var longRunningTask = server.HandlePlayerAsync(new SlowPlayer());
var shutdownTask = server.GracefulShutdownAsync();
await Task.Delay(100);
Assert.False(shutdownTask.IsCompleted);
}
Resources
assets/ - Async patterns
references/ - Concurrency guides