In general there are critical regions in code modifying data shared between threads that leave that data in an inconsistent state. This is precisely the same problem as when there are simultaneously executing processes. As @nlucaroni points out, a context switch in the middle of a critical region should not allow another thread into the same critical region. For example:
(* f should count the number of times it's called *)
let f =
let x = ref 0 in
fun () ->
x := !x + 1;
!x
A context switch after the lookup of x
but before the store can clearly result in a miscount. This is fixed with a mutex.
(* f should count the number of times it's called *)
let f =
let x = ref 5 in
let m = Mutex.create () in
fun () ->
Mutex.lock m;
x := !x + 1;
let ret = !x in
Mutex.unlock m;
ret
will fix this.