unixODBC:在 Haskell 线程中分配句柄时句柄无效

unixODBC: invalid handle when handle is allocated in a Haskell thread

我正在 Haskell 中使用外部调用构建 ODBC 应用程序。当在同一线程中分配的句柄上调用 forkIO 或 forkOS 线程(即无界或有界线程)中的 odbc 函数时,函数 returns 无效句柄错误。

在主线程中进行相同的调用时效果很好。

我发现这个问题是由unixODBC中的一个错误引起的,我在这里记录了它:https://sourceforge.net/p/unixodbc/bugs/41/

简而言之,unixODBC 使用指向内存的句柄指针,这些内存分配用于保存这些句柄的数据。由于句柄是 32 位整数,在 64 位体系结构上,它们被截断到最后有效的一半(对于 x86 处理器)。

因此,如果指针值小于2G,一切正常,但如果大于2G(而不是4G,因为符号位扩展),unixODBC将无法定位句柄的数据结构,将报告无效句柄。

当在主线程中从Haskell调用SQLAllocHandle时,分配的指针的值小于2G,所以一切正常。但是当它从另一个线程(liftIO 或 liftOS)调用时,分配的指针的值大于 2G,因此使用 unixODBC 的多线程 ODBC 应用程序在 Haskell 中是不可能的,除非所有句柄分配都在主线程中完成.

我找到的解决方法是基于这样一个事实,即在主线程中我有一个函数等待线程中的工作完成。我修改了该函数以同时监听句柄分配请求的通道,然后分配句柄和 return 响应。

这是我用于变通的示例代码:

-- All actions are ReaderT monad actions that share a global environment
-- for threads execution

-- | wait for worker threads to complete the work
waitForWorkToEnd :: (MonadIO m) => ReaderT MyEnvironment m ()
waitForWorkToEnd = do
  threadsCountVar <- asks threads_WorkerThreadsVar
  allocHandleChan <- asks threads_AllocHandleChan

  let waitIO = join $ atomically $ orElse (readTVar threadsCountVar >>= check . (<= 0) >> (return $ return ())) (allocHandleT allocHandleChan >>= \ io -> return (io >> waitIO))
  liftIO $ waitIO
  liftIO $ log $ fromString $ "all worker threads have finished"

-- | creates an IO action inside a STM monad to allocate a new handler in the current thread
allocHandleT :: (MonadIO m, MonadFail m) => TQueue (SQLSMALLINT, SQLINTEGER, TMVar SQLINTEGER) -> STM (m ())
allocHandleT chan = do
  (hType, hParent, retVar) <- readTQueue chan
  return $ allocHandle hType hParent >>= liftIO . atomically . (putTMVar retVar) 

-- | make a handle alloc request to the main thread and wait for result
allocHandleReq :: (MonadIO m, MonadFail m) => SQLSMALLINT -> SQLINTEGER -> ReaderT MyEnvironment m SQLINTEGER
allocHandleReq htype hparent = do
  allocHandleChan <- asks threads_AllocHandleChan
  resultVar       <- liftIO $ atomically $ newEmptyTMVar
  liftIO $ atomically $ writeTQueue allocHandleChan (htype, hparent, resultVar)
  liftIO $ atomically $ takeTMVar resultVar

-- allocHandle simply calls SQLAllocHandle and takes care of the diagnostics 
-- information; it is part of the sqlcli package you can find it here:
-- https://hub.darcs.net/mihaigiurgeanu/sqlcli