When code can run on both main and IO threads, require an explicit ownership + synchronization rule for every shared piece of state (flags, counters, refcounts, pending lists, and memory frees). Concretely:

1) No racy reads/writes of shared fields

2) Main-thread only for shared sliding-window / non-thread-safe maintenance

3) Refcount changes must be atomic end-to-end

4) If deallocation/free callbacks are not thread-safe: defer to the safe thread

Example patterns

A) IO thread signals main using a pending flag instead of touching racy state

if (!(c->io_flags & CLIENT_IO_READ_ENABLED)) {
    /* No racy reads of c->flags; just signal via atomic pending flag */
    atomicSetWithSync(c->pending_read, 1);
    return;
}

B) Main-thread-only maintenance

atomicIncr(server.stat_total_client_process_input_buff_events, 1);
if (c->running_tid == IOTHREAD_MAIN_THREAD_ID)
    statsUpdateActiveClients(c);

C) Safe atomic refcount decrement

unsigned short new_refcnt = atomicDecr(o->refcount, 1);
if (new_refcnt == 0) {
    /* safe to free */
}

D) Deferred free from IO thread to main

/* IO thread */
ioDeferFreeRobj(c, obj);

/* main thread when client is back */
freeClientIODeferredObjects(c, /*free_array=*/0);

Documentation requirement