Хорошо, может быть, я должен сделать свой комментарий ответом, если кто-то может доказать, что Network.Socket
не потоко-безопасен и может прокомментировать некоторые убедительные комментарии.
Модуль Network.Socket
представляет собой тонкий слой вокруг обычных сокетов Беркли API. Для сокетов Беркли нет проблем с использованием одного UDP-сокета с несколькими потоками, выполняющими одновременные вызовы recvFrom
или sendTo
или оба. Для соединений UDP сам сокет в основном не имеет состояния, за исключением локального IP-адреса и порта, к которому он был привязан. В частности, вызовы recvFrom
и sendTo
по сути являются «атомами c» для UDP - нет никакого способа, чтобы две одновременные исходящие дейтаграммы были бы «чередованы», и нет никакой возможности, чтобы входящая дейтаграмма была бы разделена на маленькие куски между потоками.
Протокол UDP не гарантирует, что все пакеты будут доставлены, и не гарантирует, что они не будут дублироваться, поэтому ваше приложение должно быть подготовлено для (целого, полного) датаграмма должна быть доставлена более чем одному потоку или нулевым потокам, но это не имеет никакого отношения к безопасности потока. Это просто UDP.
Если Network.Socket
добавил слой буферизации или какую-либо другую сложную обработку, то, возможно, он не может быть потокобезопасным, даже для UDP, но, глядя на код, я вижу recvFrom
и sendTo
не делает ничего, кроме выделения памяти и эквивалентных сокетов C вызовов.
Учитывая это, наиболее разумной архитектурой для многопоточного эхо-сервера UDP является использование одного принимающего потока, который безоговорочно отправляет новый поток за каждый запрос. Вы, вероятно, не использовали бы эту архитектуру в программе C с pthreads, потому что эти потоки довольно дороги, если вы обрабатываете много запросов, но потоки GH C forkIO
легки, поэтому, скажем, несколько тысячи из них не должны быть проблемой.
module Main where
import Network.Socket hiding (recvFrom, sendTo)
import Network.Socket.ByteString
import Control.Concurrent
import Control.Monad
import Data.ByteString (ByteString)
main :: IO ()
main = do
sock <- socket AF_INET6 Datagram defaultProtocol
addr:_ <- getAddrInfo (Just defaultHints
{ addrFamily = AF_INET6, addrSocketType = Datagram })
(Just "::1") (Just "7331")
bind sock (addrAddress addr)
forever $ do
result <- recvFrom sock 4096
forkIO $ worker sock result
worker :: Socket -> (ByteString, SockAddr) -> IO ()
worker sock (msg, client) = do
threadDelay 1000000 -- simulate some processing
void $ sendTo sock msg client
В своем первоначальном ответе, в дополнение к описанному выше подходу, я предложил альтернативную архитектуру, использующую фиксированное число рабочих потоков в recv-send l oop, вот так:
import Network.Socket hiding (recvFrom, sendTo)
import Network.Socket.ByteString
import Control.Concurrent
import Control.Monad
main :: IO ()
main = do
sock <- socket AF_INET6 Datagram defaultProtocol
addr:_ <- getAddrInfo (Just defaultHints
{ addrFamily = AF_INET6, addrSocketType = Datagram })
(Just "::1") (Just "7331")
bind sock (addrAddress addr)
replicateM_ 16 $ forkIO $ worker sock
forever $ threadDelay longtime
where longtime = 10^12
worker :: Socket -> IO ()
worker sock = forever $ do
(msg, client) <- recvFrom sock 4096
threadDelay 1000000 -- simulate some processing
sendTo sock msg client
Преимущество этого состоит в том, что, даже если одновременно поступает много запросов, существует заранее заданная верхняя граница для числа одновременных рабочих, которые будут работать , (Фактический верхний предел для «всплеска» запросов до того, как они начнут отбрасываться, будет выше, чем число работников, так как O / S будет буферизовать пакеты, даже если все работники заняты.) Один недостаток, указанный восходящим потоком, заключается в том, что все потоки Haskell (в приведенном выше примере 16 из них) просыпаются, что приводит к связке вызовов recvFrom
, только один из которых получает ответ. Однако весь смысл использования этого подхода состоит в том, чтобы ограничить число одновременных запросов, поэтому мы не находимся в контексте, где несколько десятков дополнительных системных вызовов имеют значение.
Факт остается фактом: при любом подходе при использовании одного разъема проблем с безопасностью потоков не существует.