Now you know the fundamental requirement for thread-safe code: mutation of shared state must be done atomically. Any time you change a variable that is shared by many threads, it needs to be done atomically. Unfortunately Ruby and most other mainstream languages only give you one tool to do this: the lock aka the mutex.
Mutex is short for “mutual exclusion” as in “only one thread can be executing this code at a time”. Usage is simple:
@mutex = Mutex.new
@mutex.synchronize do
i += 1
end