Como o HOpenGL se comporta em relação a outros threads e TChans em Haskell?

Estou fazendo um trabalho de prova de conceito para um videogame bastante complexo que gostaria de escrever em Haskell usando a biblioteca HOpenGL. Comecei escrevendo um módulo que implementa comunicação baseada em eventos cliente-servidor. Meu problema aparece quando tento conectá-lo a um programa simples para desenhar cliques na tela.

A biblioteca de eventos usa uma lista de TChans feita em uma fila de prioridade para comunicação. Ele retorna uma fila "out" e uma fila "in" correspondente às mensagens ligadas ao servidor e ao cliente. O envio e recebimento de eventos são feitos em encadeamentos separados usando o forkIO. Testar a biblioteca de eventos sem a parte do OpenGL mostra a comunicação com sucesso. Aqui está o código que eu usei para testá-lo:

-- Client connects to server at localhost with 3 priorities in the priority queue
do { (outQueue, inQueue) <- client Nothing 3
   -- send 'Click' events until terminated, the server responds with the coords negated
   ; mapM_ (\x -> atomically $ writeThing outQueue (lookupPriority x) x)
           (repeat (Click (fromIntegral 2) (fromIntegral 4)))
   }

Isso produz a saída esperada, ou seja, muitos eventos de envio e recebimento. Eu não acho que o problema esteja na biblioteca de manipulação de eventos.

A parte OpenGL do código verifica a fila de entrada em busca de novos eventos no displayCallback e, em seguida, chama o manipulador associado do evento. Eu posso obter um evento (o evento Init, que simplesmente limpa a tela) para ser capturado pelo displayCallback, mas depois disso nada é detectado. Aqui está o código relevante:

atomically $ PQ.writeThing inqueue (Events.lookupPriority Events.Init) Events.Init
    GLUT.mainLoop

render pqueue =
    do  event <- atomically $
            do  e <- PQ.getThing pqueue
                case e of
                    Nothing -> retry
                    Just event -> return event
        putStrLn $ "Got event"
        (Events.lookupHandler event Events.Client) event
        GL.flush
        GLUT.swapBuffers

Então minhas teorias sobre por que isso está acontecendo são:

O retorno de chamada de exibição está bloqueando todos os threads de envio e recebimento na repetição.As filas não estão sendo retornadas corretamente, de forma que as filas que o cliente lê sejam diferentes daquelas que a parte do OpenGL lê.

Existem outras razões pelas quais isso pode estar acontecendo?

O código completo para isso é muito longo para postar aqui, embora não seja muito longo (5 arquivos com menos de 100 linhas cada), no entanto, é tudo no GitHubAqui.

Editar 1:
O cliente é executado a partir da função principal no código HOpenGL da seguinte forma:

main =
    do  args <- getArgs
        let ip = args !! 0
        let priorities = args !! 1
        (progname, _) <- GLUT.getArgsAndInitialize
        -- Run the client here and bind the queues to use for communication
        (outqueue, inqueue) <- Client.client (Just ip) priorities
        GLUT.createWindow "Hello World"
        GLUT.initialDisplayMode $= [GLUT.DoubleBuffered, GLUT.RGBAMode]
        GLUT.keyboardMouseCallback $= Just (keyboardMouse outqueue)
        GLUT.displayCallback $= render inqueue
        PQ.writeThing inqueue (Events.lookupPriority Events.Init) Events.Init
        GLUT.mainLoop

A única bandeira que eu passar para o GHC quando eu compilar o código é-package GLUT.

Editar 2:
Eu limpei o código no Github um pouco. Eu removi acceptInput já que ele não estava fazendo nada realmente e o código do cliente não deveria estar escutando seus próprios eventos, é por isso que está retornando as filas.

Editar 3:
Estou esclarecendo minha pergunta um pouco. Eu peguei o que aprendi com @Shang e @Laar e meio que corri com isso. Eu mudei os threads em Client.hs para usar forkOS ao invés de forkIO (e usei -threaded em ghc), e parece que os eventos estão sendo comunicados com sucesso, porém eles não estão sendo recebidos no callback do display. Eu também tentei ligarpostRedisplay&nbsp;no final do retorno de chamada de exibição, mas eu não acho que ele já é chamado (porque eu acho que a nova tentativa está bloqueando todo o segmento do OpenGL).

A nova tentativa no retorno de chamada da tela bloquearia todo o segmento do OpenGL? Em caso afirmativo, seria seguro separar o retorno de chamada de exibição em um novo segmento? Eu não imagino que seria, já que existe a possibilidade de várias coisas estarem tentando desenhar na tela ao mesmo tempo, mas eu posso lidar com isso com um bloqueio. Outra solução seria converter olookupHandler&nbsp;função para retornar uma função envolto em umMaybee não faça nada se não houver eventos. Eu sinto que isso seria menos do que ideal, pois eu teria, essencialmente, um loop ocupado que era algo que eu estava tentando evitar.

Editar 4:
Esqueci de mencionar que eu usei -readed em ghc quando fiz o forkOS.

Editar 5:
Eu fui e fiz um teste da minha teoria de que a repetição na função render (display callback) estava bloqueando todo o OpenGL. Eu reescrevi a função render para que ela não bloqueasse mais, e funcionava como se eu quisesse que funcionasse. Um clique na tela dá dois pontos, um do servidor e do clique original. Aqui está o código para a nova função render (nota: énão&nbsp;no Github):

render pqueue =
    do  event <- atomically $ PQ.getThing pqueue
        case (Events.lookupHandler event Events.Client) of
            Nothing -> return ()
            Just handler -> 
                do  let e = case event of {Just e' -> e'}
                    handler e
                    return ()
        GL.flush
        GLUT.swapBuffers
        GLUT.postRedisplay Nothing

Eu tentei com e sem o postRedisplay, e só funciona com isso. O problema agora é que isso coloca a CPU em 100% porque é um loop ocupado. Na edição 4, propus o encadeamento do retorno de chamada da tela. Ainda estou pensando em uma maneira de fazer isso.

Uma nota desde que eu não mencionei isto ainda. Qualquer um que queira construir / executar o código deve fazer assim:

$ ghc -threaded -package GLUT helloworldOGL.hs -o helloworldOGL
$ ghc server.hs -o server
-- one or the other, I usually do 0.0.0.0
$ ./server "localhost" 3
$ ./server "0.0.0.0" 3
$ ./helloworldOGL "localhost" 3

Editar 6:Solução
Uma solução! Indo junto com os threads, eu decidi fazer um thread no código OpenGL que checava por eventos, bloqueando se não havia algum, e então chamando o handler seguido por postRedisplay. Aqui está:

checkEvents pqueue = forever $
    do  event <- atomically $
            do  e <- PQ.getThing pqueue
                case e of
                    Nothing -> retry
                    Just event -> return event
        putStrLn $ "Got event"
        (Events.lookupHandler event Events.Client) event
        GLUT.postRedisplay Nothing

O retorno de chamada da tela é simplesmente:

render = GLUT.swapBuffers

E funciona, não atrapalha a CPU em 100% e os eventos são tratados prontamente. Estou postando isso aqui porque eu não poderia ter feito isso sem as outras respostas e me sinto mal em aceitar o representante quando as respostas foram muito úteis, então estou aceitando a resposta de @Laar, já que ele tem o menor representante.