unsafePerformIO в многопоточных приложениях не работает

Ниже приведен исходный код примера программы:

Когда я запускаю его из ghci, и printJob, и printJob2 работают нормально и записывают десять строк в текстовый файл.

Но при компиляции с флагом -threaded программа записывает только одну строку.

У меня есть ghc 7.0.3 на ArchLinux.

Вот команда компиляции:

ghc -threaded -Wall -O2 -rtsopts -with-rtsopts=-N -o testmvar testmvar.hs

Что я делаю неправильно? Почему он не работает в потоковом режиме?

import Control.Concurrent.MVar
import Control.Concurrent (forkIO)
import Control.Exception (bracket)
import Control.Monad (forM_)
import System.IO.Unsafe (unsafePerformIO)
import System.IO (hPutStrLn, stderr)


{-# NOINLINE p #-}
p :: MVar Int
p = unsafePerformIO $ newMVar (1::Int)


{-# NOINLINE printJob #-}
printJob x = bracket (takeMVar p) (putMVar p . (+ 1))
                   (\a -> do
                       appendFile "mvarlog.txt" $ "Input: " ++ x ++ "; Counter: " ++ show a ++ "\n"
                       )


{-# NOINLINE printJob2 #-}
printJob2 = unsafePerformIO $ do
   p2 <- newEmptyMVar
   return $ (\x -> bracket (putMVar p2 True) (\_ -> takeMVar p2)
                   (\_ -> do
                       appendFile "mvarlog.txt" $ "preformed " ++ x ++ "\n"
                   ))

main = do
  forM_ [1..10]
    (\x -> forkIO $ printJob (show x))

РЕДАКТИРОВАТЬ: Хаммар указал, что если основное приложение завершается раньше, чем все порожденные потоки, то они будут убиты, и предложил добавить задержку в конце основного. Я так и сделал, как он и предсказывал, это работает.


person Vagif Verdi    schedule 25.02.2012    source источник
comment
Я не уверен, что здесь происходит (возможно, компиляция запускает оптимизации, которых нет в GHCi, и они устраняют вызовы unsafePerformIO), но я чувствую, что стоит еще раз сказать, что unsafePerformIO есть, как имя означает, небезопасно, и что-то сломается, если вы его используете. (Хорошо, если вы не очень, очень осторожны, но, возможно, и в этом случае тоже.)   -  person Antal Spector-Zabusky    schedule 26.02.2012
comment
Вы получите тот же результат, если добавите задержку в конце main? Все остальные потоки уничтожаются после завершения основного потока, поэтому в зависимости от того, как запланированы дела, это может ничего не делать — независимо от каких-либо проблем, связанных с unsafePerformIO.   -  person hammar    schedule 26.02.2012
comment
@hammar Вы правы! Я добавил threadDelay в конец main, и теперь все работает нормально. Спасибо! Если вы поставите это как отдельный ответ, я приму его.   -  person Vagif Verdi    schedule 26.02.2012
comment
совет: чтобы узнать о MVars верхнего уровня и многом другом, ознакомьтесь с безопасными глобальными переменными пакет!   -  person danr    schedule 27.02.2012
comment
@VagifVerdi: интересно, но заметил, что ошибка, на которую вы ссылаетесь, не использует безопасные глобальные переменные, а напрямую использует такой код, как вы; создание ссылки MVar с помощью unsafePerformIO.   -  person danr    schedule 28.02.2012
comment
Я инкапсулирую глобальную переменную в замыкание, поэтому она невидима. Таким образом, безопасные глобалы не будут мне очень полезны. Как и большинство других хаскеллеров, я против общедоступных глобальных переменных.   -  person Vagif Verdi    schedule 28.02.2012


Ответы (2)


Проблема в том, что ваш основной поток завершается слишком рано, и когда основной поток программы на Haskell завершается, все остальные потоки автоматически уничтожаются. В зависимости от того, как потоки планируются, это может произойти до того, как какой-либо из потоков вообще сможет запуститься.

Быстрое и грязное решение состоит в том, чтобы просто добавить threadDelay в конце main, хотя более надежным методом было бы использование примитива синхронизации, такого как MVar, чтобы сигнализировать, когда основной поток может завершиться.

Например:

main = do
  vars <- forM [1..10] $ \x -> do
    done <- newEmptyMVar -- Each thread gets an MVar to signal when it's done
    forkIO $ printJob (show x) >> putMVar done ()
    return done

  -- Wait for all threads to finish before exiting
  mapM_ takeMVar vars
person hammar    schedule 26.02.2012
comment
Все это говорит о том, что использование unsafePerformIO почти наверняка является плохой идеей. Есть способы обойти это для этого варианта использования. - person Louis Wasserman; 26.02.2012
comment
@Louis Я хотел бы узнать, если вы покажете мне альтернативный способ создания семафора без утечки абстракций. Первоначально я объявил его в глобальной структуре и инициализировал в main перед запуском сервера приложений, а затем явно передал его в качестве параметра функции printJob. Но это просто неправильно с точки зрения инкапсуляции. - person Vagif Verdi; 27.02.2012

КОНЕЧНО не работает. Использование unsafePerformIO всегда будет преследовать вас. Структурируйте свой код, чтобы не использовать его. Использование его для создания глобальных переменных не является законным. Для этого и предназначена монада читателя. Вам не нужен unsafePerformIO ни для чего в Haskell.

Меня убивает, когда люди рекомендуют этот «трюк», когда он явно не работает. Это как слепой ведет слепого. Просто не делайте этого, и у вас не будет проблем с использованием Haskell. У Haskell есть действительно красивые и элегантные решения для каждой проблемы, которую вы ставите перед ним, но если вы настаиваете на борьбе с ней вместо того, чтобы изучать ее, вы всегда будете сталкиваться с ошибками.

person Gabriel Gonzalez    schedule 26.02.2012
comment
извини но ты дебил. Я использую его для создания семафора для общего ресурса (принтера). Нет другого способа сделать это, кроме как использовать глобальный MVar. И вот вы начинаете нести чушь про злобный UnsafePerformIO. Это не зло. Это просто продвинутый инструмент. Явно не для таких как ты. - person Vagif Verdi; 27.02.2012
comment
Вы правы в том, что для этого требуется MVar, но вы не правы в том, что для этого требуется unsafePerformIO. Ничто не мешает вам инициализировать его в теле вашей основной функции и передавать ссылку MVar. Если вам не нравится передавать ссылку на переменную, используйте монаду чтения. Если вам вообще не нравится функционально структурировать свой код, то почему вы используете Haskell? - person Gabriel Gonzalez; 28.02.2012
comment
Кто что-нибудь говорит о необходимости использования UnsafePerformIO? На самом деле, если вы так выразились, то даже сам haskell не требуется, я мог бы просто написать его на java. Инициализация глобальной переменной вне функции уродлива и неправильна. Вы разливаете кишки повсюду. В моей первой реализации этот MVar был определен в одном модуле, инициализирован в другом и использован в третьем. Это нарушает все законы инкапсуляции. И я ценю свою инкапсуляцию больше, чем идиотские суеверия по поводу UnsafePerformIO. Небезопасный в этом названии действительно означает держаться подальше от детей. Инженеры знают, как и когда это использовать. - person Vagif Verdi; 28.02.2012
comment
То, о чем вы просите, - это примесь, замаскированная под модульность. Haskell требует, чтобы вы передавали такие параметры либо явно, либо через неявную монаду чтения, чтобы гарантировать чистоту. Компилятор доверяет вам, когда вы говорите, что ваш код чист, но если ваш код на самом деле не чист, то компилятор становится вашим врагом, и вы вынуждены делать такие вещи, как NOINLINE, и тратить значительное время на устранение ошибок параллелизма в коде, который зависит от вашей функции. будучи чистым. - person Gabriel Gonzalez; 28.02.2012
comment
Рассматриваемая процедура распечатывает пакеты файлов. Очевидно, что это настолько нечисто, насколько это возможно. Я не делаю его более нечистым, чем он уже есть :) Но я существенно упрощаю его использование и скрываю детали низкоуровневой реализации. Кроме того, у меня даже больше нет глобальной переменной (она скрыта в замыкании), поэтому какой-нибудь невежественный программист-новичок не сможет сломать ее, используя глобальную общедоступную переменную для чего-то другого, или, что еще хуже, передав функции printJob другой MVar . Архитектура имеет значение. - person Vagif Verdi; 28.02.2012
comment
Вы можете скрыть код от программиста, но вы не можете скрыть его от компилятора. Придет время, когда компилятор применит преобразование кода, которое предполагает, что ваша функция чистая, а затем сломает ваш код. Вы не должны верить мне на слово, потому что, несмотря на всю вашу браваду, я знаю, что вы вернетесь позже, умоляя о помощи, когда ваш код ведет себя не так, как вы думаете. Как я уже говорил, если вы не понимаете важности чистоты, то Haskell вам не подходит. - person Gabriel Gonzalez; 28.02.2012
comment
Ваш иррациональный страх перед UnsafePerformIO затуманивает ваш разум. Единственным результатом его применения является то, что этот код будет выполнен один раз и запомнен. Это ИМЕННО то, что я хочу от него. С какой стати haskell ПРЕОБРАЗУЕТ что-то, что уже вычислено и сохранено? Это сломало бы не только мой код, но и код ВСЕХ ОСТАЛЬНЫХ :), если вы не понимаете приложений и последствий чистоты, тогда Haskell не подходит для вас. - person Vagif Verdi; 28.02.2012