diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,20 @@
-# Revision history of strict-checked-vars
+# Revision history of `resource-registry`
+
+## 0.2.0.0 — 2025-10-23
+
+* Define `transferRegistry` for moving all resources from one registry to a
+  different one.
+
+* Cancel registered threads in the registry before closing the registry. This
+  prevents race conditions where the registry gets closed but the running async
+  thread still tries to allocate something in the registry before being
+  cancelled, which would result in an exception.
+
+* Expose `allocateThread` to register another thread in a registry, such that it
+  is cancelled before closing the registry, even if it belongs to a separate
+  registry.
+
+* Add the label of the thread to `Context`.
 
 ## 0.1.1.0 — 2025-05-15
 
diff --git a/resource-registry.cabal b/resource-registry.cabal
--- a/resource-registry.cabal
+++ b/resource-registry.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: resource-registry
-version: 0.1.1.0
+version: 0.2.0.0
 synopsis: Track allocated resources
 description:
   When the scope of a @bracket@ doesn't enclose all uses of the resource, a
@@ -35,7 +35,7 @@
   type: git
   location: https://github.com/IntersectMBO/io-classes-extra
   subdir: resource-registry
-  tag: resource-registry-0.1.1.0
+  tag: resource-registry-0.2.0.0
 
 common warnings
   ghc-options:
diff --git a/src/Control/ResourceRegistry.hs b/src/Control/ResourceRegistry.hs
--- a/src/Control/ResourceRegistry.hs
+++ b/src/Control/ResourceRegistry.hs
@@ -239,6 +239,7 @@
     -- * Allocating and releasing regular resources
   , ResourceKey
   , allocate
+  , allocateThread
   , allocateEither
   , release
   , releaseAll
@@ -263,11 +264,13 @@
   , modifyWithTempRegistry
   , runInnerWithTempRegistry
   , runWithTempRegistry
+  , transferRegistry
 
     -- * Unsafe combinators primarily for testing
   , closeRegistry
   , countResources
   , unsafeNewRegistry
+  , resourceKeyId
   ) where
 
 import Control.Applicative ((<|>))
@@ -342,6 +345,23 @@
 data RegistryState m = RegistryState
   { registryThreads :: !(KnownThreads m)
   -- ^ Forked threads
+  , registryReleaseThreads :: ![ReleaseThread m]
+  -- ^ The list of releasing actions for threads that were forked from this
+  -- registry.
+  --
+  -- We will cancel these when closing a registry but before actually closing
+  -- the registry. This will guard the case in which a forked thread allocates
+  -- resources in the registry. We would face a race condition among:
+  --
+  -- - The registry killing the thread
+  --
+  -- - The thread allocating something in the now closed registry.
+  --
+  -- The latter case will throw an exception.
+  --
+  -- Note this is separate from 'registryThreads' because one can add threads to
+  -- cancel via 'allocateThread' and still those threads should not be directly
+  -- added to the set of known threads.
   , registryResources :: !(Map ResourceId (Resource m))
   -- ^ Currently allocated resources
   --
@@ -433,6 +453,10 @@
 instance Show (Release m) where
   show _ = "<<release>>"
 
+-- | Release a thread when closing a registry.
+newtype ReleaseThread m = ReleaseThread {releaseThread :: m ()}
+  deriving NoThunks via OnlyCheckWhnfNamed "ReleaseThread" (ReleaseThread m)
+
 {-------------------------------------------------------------------------------
   Internal: pure functions on the registry state
 -------------------------------------------------------------------------------}
@@ -455,11 +479,18 @@
 
 -- | Allocate key for new resource
 allocKey :: State (RegistryState m) (Either PrettyCallStack ResourceId)
-allocKey = unlessClosed $ do
+allocKey = unlessClosed $ unsafeAllocKey
+
+unsafeAllocKey :: State (RegistryState m) ResourceId
+unsafeAllocKey = do
   nextKey <- gets registryNextKey
   modify $ \st -> st{registryNextKey = succ nextKey}
   return nextKey
 
+-- | Allocate multiple keys for resources
+allocNKeys :: Int -> State (RegistryState m) (Either PrettyCallStack [ResourceId])
+allocNKeys n = unlessClosed $ replicateM n $ unsafeAllocKey
+
 -- | Insert new resource
 insertResource ::
   ResourceId ->
@@ -584,18 +615,19 @@
       { registryContext = context
       , registryState = stateVar
       }
- where
-  initState :: RegistryState m
-  initState =
-    RegistryState
-      { registryThreads = KnownThreads Set.empty
-      , registryResources = Map.empty
-      , registryNextKey = ResourceId 1
-      , registryAges = Bimap.empty
-      , registryNextAge = ageOfFirstResource
-      , registryStatus = RegistryOpen
-      }
 
+initState :: RegistryState m
+initState =
+  RegistryState
+    { registryThreads = KnownThreads Set.empty
+    , registryReleaseThreads = []
+    , registryResources = Map.empty
+    , registryNextKey = ResourceId 1
+    , registryAges = Bimap.empty
+    , registryNextAge = ageOfFirstResource
+    , registryStatus = RegistryOpen
+    }
+
 -- | Close the registry
 --
 -- This can only be called from the same thread that created the registry.
@@ -619,7 +651,17 @@
   (MonadMask m, MonadThread m, MonadSTM m, HasCallStack) =>
   ResourceRegistry m ->
   m ()
-closeRegistry rr = mask_ $ do
+closeRegistry rr = mask_ $ releaseAllBy close rr
+
+-- | Release all the resources and perform another action while doing so. This
+-- is to be used both by 'closeRegistry' which will 'close' the registry, as
+-- well as 'releaseAll' which will not actually close the registry.
+releaseAllBy ::
+  (MonadMask m, MonadThread m, MonadSTM m, HasCallStack) =>
+  (PrettyCallStack -> State (RegistryState m) (Either PrettyCallStack [ResourceId])) ->
+  ResourceRegistry m ->
+  m ()
+releaseAllBy action rr = do
   context <- captureContext
   unless (contextThreadId context == contextThreadId (registryContext rr)) $
     throwIO $
@@ -627,9 +669,21 @@
         { resourceRegistryCreatedIn = registryContext rr
         , resourceRegistryUsedIn = context
         }
+  unsafeReleaseAllBy action context rr
 
+-- | Unsafe version of 'releaseAllBy'.
+unsafeReleaseAllBy ::
+  (MonadMask m, MonadThread m, MonadSTM m, HasCallStack) =>
+  (PrettyCallStack -> State (RegistryState m) (Either PrettyCallStack [ResourceId])) ->
+  Context m ->
+  ResourceRegistry m ->
+  m ()
+unsafeReleaseAllBy action context rr = do
+  ts <- updateState rr $ gets registryReleaseThreads
+  mapM_ releaseThread ts
+
   -- Close the registry so that we cannot allocate any further resources
-  alreadyClosed <- updateState rr $ close (contextCallStack context)
+  alreadyClosed <- updateState rr $ action (contextCallStack context)
   case alreadyClosed of
     Left _ ->
       return ()
@@ -1152,15 +1206,7 @@
   (MonadMask m, MonadSTM m, MonadThread m, HasCallStack) =>
   ResourceRegistry m ->
   m ()
-releaseAll rr = do
-  context <- captureContext
-  unless (contextThreadId context == contextThreadId (registryContext rr)) $
-    throwIO $
-      ResourceRegistryClosedFromWrongThread
-        { resourceRegistryCreatedIn = registryContext rr
-        , resourceRegistryUsedIn = context
-        }
-  void $ releaseAllHelper rr context release
+releaseAll rr = releaseAllBy (\_ -> unlessClosed $ gets getYoungestToOldest) rr
 
 -- | This is to 'releaseAll' what 'unsafeRelease' is to 'release': we do not
 -- insist that this funciton is called from a thread that is known to the
@@ -1171,9 +1217,9 @@
   m ()
 unsafeReleaseAll rr = do
   context <- captureContext
-  void $ releaseAllHelper rr context unsafeRelease
+  unsafeReleaseAllBy (\_ -> unlessClosed $ gets getYoungestToOldest) context rr
 
--- | Internal helper used by 'releaseAll' and 'unsafeReleaseAll'.
+-- | Internal helper used by 'runWithTempRegistry'.
 releaseAllHelper ::
   (MonadMask m, MonadSTM m, MonadThread m) =>
   ResourceRegistry m ->
@@ -1230,6 +1276,19 @@
 waitAnyThread :: forall m a. MonadAsync m => [Thread m a] -> m a
 waitAnyThread ts = snd <$> waitAny (map threadAsync ts)
 
+-- | Allocate a thread in a registry. This will ensure that such a thread is
+-- cancelled before the registry is closed. Useful for threads that belong to a
+-- different registry but will try to allocate resources in this registry.
+allocateThread ::
+  (MonadMask m, MonadAsync m, HasCallStack) =>
+  ResourceRegistry m -> (ResourceId -> m (Thread m a)) -> m (ResourceKey m, Thread m a)
+allocateThread rr alloc = do
+  (k, t) <- allocate rr alloc cancelThread
+  updateState rr $
+    modify
+      (\s -> s{registryReleaseThreads = ReleaseThread (void (release k)) : registryReleaseThreads s})
+  pure (k, t)
+
 -- | Fork a new thread
 forkThread ::
   forall m a.
@@ -1241,7 +1300,7 @@
   m (Thread m a)
 forkThread rr label body =
   snd
-    <$> allocate rr (\key -> mkThread key <$> async (body' key)) cancelThread
+    <$> allocateThread rr (\key -> mkThread key <$> async (body' key))
  where
   mkThread :: ResourceId -> Async m a -> Thread m a
   mkThread rid child =
@@ -1428,21 +1487,27 @@
   -- ^ CallStack in which it was created
   , contextThreadId :: !(ThreadId m)
   -- ^ Thread that created the registry or resource
+  , contextThreadLabel :: !(Maybe String)
+  -- ^ The label of the thread that created the registry, if it is set
   }
 
 -- Existential type; we can't use generics
 instance NoThunks (Context m) where
   showTypeOf _ = "Context"
-  wNoThunks ctxt (Context cs tid) =
+  wNoThunks ctxt (Context cs tid lbl) =
     allNoThunks
       [ noThunks ctxt cs
       , noThunks ctxt (InspectHeapNamed @"ThreadId" tid)
+      , noThunks ctxt lbl
       ]
 
 deriving instance Show (Context m)
 
 captureContext :: MonadThread m => HasCallStack => m (Context m)
-captureContext = Context prettyCallStack <$> myThreadId
+captureContext = do
+  tid <- myThreadId
+  lbl <- threadLabel tid
+  pure $ Context prettyCallStack tid lbl
 
 {-------------------------------------------------------------------------------
   Misc utilities
@@ -1528,3 +1593,50 @@
   NoThunks (Bimap k v)
   where
   wNoThunks ctxt = noThunksInKeysAndValues ctxt . Bimap.toList
+
+-- | Move all the resources from the origin registry to the destination
+-- registry and return the list of allocated 'ResourceKey's.
+--
+-- Transferring individual resources between registries is risky because a
+-- later resource in the origin registry might depend on a resource that we are
+-- trying to transfer. However, transferring whole registries is fine.
+transferRegistry ::
+  (MonadSTM m, MonadMask m, MonadThread m, HasCallStack) =>
+  ResourceRegistry m ->
+  ResourceRegistry m ->
+  m [ResourceKey m]
+transferRegistry fromReg toReg = do
+  context <- captureContext
+
+  -- The calling thread is known to the origin registry
+  ensureKnownThread fromReg context
+
+  -- Alloc all the needed keys
+  mKeys <- updateState toReg . allocNKeys =<< countResources fromReg
+
+  case mKeys of
+    -- If the destination registry is closed, throw
+    Left closed -> throwRegistryClosed toReg context closed
+    Right keys -> mask_ $ do
+      -- Get the resources out of the origin registry and empty it
+      regState <- atomically $ swapTVar (registryState fromReg) initState
+
+      forM_
+        ( zip keys $
+            Map.elems (registryResources regState)
+        )
+        ( \(k, res) -> do
+            -- Insert the resources into the new registry
+            inserted <- updateState toReg (insertResource k res)
+
+            case inserted of
+              -- If the destination registry is closed, throw
+              Left closed -> do
+                let Release rel = resourceRelease res
+                void rel
+                throwRegistryClosed toReg context closed
+              Right () ->
+                pure ()
+        )
+
+      pure $ map (ResourceKey toReg) keys
