Хаскелл. Как заставить мою программу завершиться?

Я хочу сделать «асинхронное» чтение stdin в течение нескольких секунд. Я делаю это, разветвляя getContents и пишу Чану. Через 5 секунд я убиваю ветку и читаю канал.

Насколько я понимаю, приведенный ниже код должен просто печатать все, что находится в chan, и завершаться, но он продолжает ждать ввода, и для завершения необходимо нажать ^C. Это поведение, которое у вас есть, когда вы делаете getContents >>= print самостоятельно, поэтому у меня есть два предположения и понятия не имею о каждом:

  • Тема не закрыта и getContent продолжает работать, запрашивая дополнительные данные
  • Что-то странное происходит с многопоточностью (см. соображения ниже)

-- OS: Ubuntu 22.04.1 LTS
-- Gnome Terminal
-- ghc 9.2.5

import Control.Concurrent.Chan ( newChan, readChan, writeChan, Chan )
import Control.Concurrent (threadDelay, forkIO, killThread)
import System.IO (getContents, BufferMode (..), stdin, hSetBuffering)

main :: IO ()
main = do
  hSetBuffering stdin NoBuffering
  chan <- newChan

  putStrLn "start"
  threadId <- forkIO $ getContents >>= writeChan chan
  threadDelay 5000000
  putStrLn "\nend"

  killThread threadId
  a <- readChan chan
  print a
  

Некоторые соображения:

  • Использование getLine заставит его работать, только если нажата клавиша Enter. Но хотелось бы просто "стримить" stdin в канал
  • hSetBuffering stdin NoBuffering обязательна, иначе программа зависнет (наверное ждет окончания ввода?)
  • Использование getContents' вызывает thread blocked indefinitely in an MVar operation, что, согласно документации , связано с тем, что канал пуст. Я думаю, getContents' на самом деле никогда не заканчивается.
  • Последнее, но самое главное, поведение отличается в зависимости от параметров компиляции:
    • ghc -threaded main.hs && ./main +RTS -N2 ничего не печатает и зависает до тех пор, пока не будет нажата ^C (то же самое с -N1, -N3 и т.д...)
    • runghc main.hs фактически напечатает все, что является stdin в течение 5 секунд (то есть все, что находится в chan), а затем зависнет.

Просто для уточнения. Вот выходы:

> runghc main.hs
start
abc     # user input
end
"abc^C" # ^C is done manually, to termiante the program and the last \" is for formatting purpose

> ghc -threaded main.hs && ./main +RTS -N2
start
abc    # user input
end
^C     # ^C is done manually, to termiante the program

Так что вопрос простой. Как мне завершить программу?

🤔 А знаете ли вы, что...
Haskell имеет множество расширений языка, которые позволяют разработчикам настраивать его поведение.


1
71
1

Ответ:

Решено

Тема getContents >>= writeChan chan — это не бесконечный цикл, который постоянно добавляет контент в chan. getContents создает преобразователь, который вставляется в chan, и поток почти мгновенно завершается. Затем в основном потоке readChan получает этот преобразователь и print a форсирует его. Это форсирование thunk, которое предлагает чтение stdin, поэтому ваша программа просто блокируется для большего ввода до тех пор, пока EOF или она не будет убита.

Что вы хотите сделать, так это явно взять небольшие биты ввода и записать их в канал. Однако в основном потоке канал не дает вам возможности узнать, когда он закончился. Обходной путь — использовать вместо этого IORef String в качестве канала. Напишите в него, явно добавив к сохраненной строке, и readIORef даст вам весь контент, который был написан до сих пор.

import Control.Concurrent.Chan ( newChan, readChan, writeChan, Chan )
import Control.Concurrent (threadDelay, forkIO, killThread)
import Control.Monad (forever)
import Data.IORef
import System.IO (getContents, BufferMode (..), stdin, hSetBuffering)

main :: IO ()
main = do
  hSetBuffering stdin NoBuffering
  buf <- newIORef []
  putStrLn "start"
  threadId <- forkIO $ forever $ do
    c <- getChar
    atomicModifyIORef' buf (\cs -> (c : cs, ()))
  threadDelay 5000000
  putStrLn "\nend"
  killThread threadId
  a <- reverse <$> readIORef buf
  print a