There seem to be some "global vars" (unsafePerformIO
+ NOINLINE
) in warp
s code base. Is it safe to run two instances of warp
from the same main
function, despite this?
It appears to be safe.
At least in warp-3.3.13
, the global variable trick is used (only) to generate keys for the vault
package, using code like:
pauseTimeoutKey :: Vault.Key (IO ())
pauseTimeoutKey = unsafePerformIO Vault.newKey
{-# NOINLINE pauseTimeoutKey #-}
Note that this is different than the "usual" global variable trick, since it does not create a global IORef
that multiple threads might try to use, while each expecting to be the sole user of the reference.
Instead, the vault
package provides a type-safe, persistent "store", a Vault
, that acts like a collection of mutable variables of various types, accessible through unique keys. Keys are generated in IO
, effectively using newUnique
from Data.Unique
. The Vault
itself is a pure, safe data structure. It is implemented using unsafe operations, but constructed in a manner that makes it safe. Ultimately, it's a HashMap
from Key a
(so, a type-annotated Integer
) to an Any
value that can be unsafeCoerce
d to the needed type a
, with type safety guaranteed by the type attached to the key. Values in the Vault
are "mutated" by inserting new values in the map, creating an updated Vault
, so there's no actual mutation going on here.
Since Vault
s are just fancy immutable HashMap
s of pure values, there's no danger of two servers overwriting values in each others' vaults, even though they're using the same keys.
As far as I can see, all that's needed to ensure safety is that, when a thread calls something like pauseTimeoutKey
, it always gets the same key, and that key is unique among keys for that thread. So, it basically boils down to the thread safety of the global variable trick in general and of newUnique
when used under unsafePerformIO
.
I've never heard of any cautions against using the global variables trick in multi-threaded code, and unsafePerformIO
is intended to be thread-safe (which is why there's a separate "more efficient but potentially thread-unsafe" version unsafeDupablePerformIO
).
newUnique
itself is implemented in a thread-safe manner:
newUnique :: IO Unique
newUnique = do
r <- atomicModifyIORef' uniqSource $ \x -> let z = x+1 in (z,z)
return (Unique r)
and I can't see how running it under unsafePerformIO
would make it thread-unsafe.