Thread safety while sharing a reference between POSIX threads in OCaml
A primitive way to have two threads share a state with type int ref
in OCaml is:
let add i x =
x := !x + i;
Printf.printf "add %d: x is now %d\n" i !x;
flush stdout
let rec run f x d =
f x;
Thread.delay d;
run f x d
let _ =
let x = ref 0 in
ignore(Thread.create (run (add 1) x) 0.2);
run (add 10) x 0.1
Function add
just adds i to the integer in the reference x. Function run
simply keeps applying f to the same argument x with delays of d between each application. The main function starts with 0 as the integer in the reference and invokes the run function in parallel with different arguments: (1) adding 1 and delaying 0.2s between calls; (2) adding 10 and delaying 0.1s between calls.
The output of running this program was:
add 10: x is now 10
add 1: x is now 11
add 10: x is now 21
add 10: x is n开发者_开发技巧ow 31
add 1: x is now 32
add 10: x is now 42
add 10: x is now 52
add 1: x is now 53
add 10: x is now 63
add 10: x is now 73
add 1: x is now 74
add 10: x is now 84
add 10: x is now 94
add 1: x is now 95
[...]
It's easy to see that the contents of the reference are shared between threads.
I have used this construction in a case where the first run
function was performing numerical calculations and the other run
function was plotting the numbers calculated in the first thread. It worked well. In that case one of the threads was writing to the state but the other one was only reading. In the example coded above both threads write to the state which I think can be problematic, but I don't exactly know why.
My Question
My questions about this are:
(1) Is this construction unsafe? If so, do you have an example where it can go wrong? (2) Do you know of a better construction to achieve the same goal? Should I use some kind of locks or signals?
Thanks in advance for any help!
All the best, Surikator.
It's not safe. You have a race condition, the code works because of your particular timings and because the add
function relies on the way the scheduler is implemented (which is subject to change).
What you need to remember is that conceptually your thread can be suspended at any time. Suppose the thread is suspended after the access to !x
, or after the computation (!x+1)
or between the assignment and the printf
. If the other thread chimes in and executes the whole function before the suspended one is allowed to proceed, the result will be wrong.
One way to show the problem is to replace the add function by :
let add i x =
x := !x + i;
for i = 1 to Random.int 1000 do ignore (Unix.getaddrinfo "localhost" "" []) done;
Printf.printf "add %d: x is now %d\n" i !x;
flush stdout
(A simpler way is to insert a random Thread.delay
between the assignement and the printf, but with the above you can see that a "regular" computation can bring in the problem)
Here's the result on my machine :
> ./test.native
add 10: x is now 11
add 1: x is now 11
add 10: x is now 21
add 1: x is now 32
add 10: x is now 32
add 1: x is now 43
You need to ensure that the addition, the assignement and the printf
are executed "atomically" by a thread ("atomically" in the sense that if a thread in suspended in this body of code no one should be allowed to enter it). One way of doing
that is to use a mutex :
let add =
let m = Mutex.create () in
fun i x ->
Mutex.lock m;
try
x := !x + i;
for i = 1 to Random.int 1000 do ignore (Unix.getaddrinfo "localhost" "" []) done;
Printf.printf "add %d: x is now %d\n" i !x;
flush stdout;
Mutex.unlock m;
with e -> Mutex.unlock m; raise e
Note that this works for the example you give but it assumes that only add
modifies x
. For more on the subject I recommend you to read :
http://ocamlunix.forge.ocamlcore.org/threads.html
and in particular :
http://ocamlunix.forge.ocamlcore.org/threads.html#htoc64
精彩评论