Haskell tính toán chuyên sâu chặn tất cả các chủ đề khác


8

Tôi muốn viết một chương trình có luồng chính tạo ra một luồng mới để tính toán và chờ đợi nó kết thúc trong một khoảng thời gian. Nếu chủ đề con không hoàn thành trong thời gian nhất định, nó đã hết thời gian và bị giết. Tôi có mã sau đây cho việc này.

import Control.Concurrent

fibs :: Int -> Int 
fibs 0 = 0
fibs 1 = 1
fibs n = fibs (n-1) + fibs (n-2)

main = do 
    mvar  <- newEmptyMVar 
    tid   <- forkIO $ do
        threadDelay (1 * 1000 * 1000)
        putMVar mvar Nothing 
    tid'  <- forkIO $ do
        if fibs 1234 == 100
            then putStrLn "Incorrect answer" >> putMVar mvar (Just False)
            else putStrLn "Maybe correct answer" >> putMVar mvar (Just True)
    putStrLn "Waiting for result or timeout"
    result <- takeMVar mvar
    killThread tid
    killThread tid' 

Tôi đã biên dịch chương trình trên với ghc -O2 Test.hsghc -O2 -threaded Test.hschạy nó, nhưng trong cả hai trường hợp, chương trình chỉ bị treo mà không in bất cứ thứ gì hoặc thoát. Nếu tôi thêm một threadDelay (2 * 1000 * 1000)luồng vào luồng tính toán trước ifkhối thì chương trình sẽ hoạt động như mong đợi và kết thúc sau một giây vì luồng hẹn giờ có thể lấp đầy mvar.

Tại sao luồng không hoạt động như tôi mong đợi?


Các ghi chú về MVartrạng thái dễ bị điều kiện chủng tộc. Tôi sẽ thực hiện lưu ý đó một cách nghiêm túc.
Bob Dalgleish

@BobDalgleish, tôi rất nghi ngờ điều đó. Các MVarkỷ luật có vẻ tốt với tôi ở đây.
dfeuer

1
bạn đã chạy chương trình với +RTS -N? kiểm tra wiki.haskell.org/Concurrency để biết thêm thông tin
lsmor

Dưới đây là một ví dụ tương tự về hành vi này và phản hồi từ anh chàng chủ yếu chịu trách nhiệm cho phần lớn đồng thời trong ghc, huyền thoại Simon :) github.com/simonmar/async/issues/93
lehins

Câu trả lời:


10

GHC sử dụng một loại kết hợp đa nhiệm ưu tiên và hợp tác trong triển khai đồng thời.

Ở cấp độ Haskell, điều này có vẻ được ưu tiên vì các luồng không cần phải mang lại một cách rõ ràng và dường như có thể bị gián đoạn bởi thời gian chạy bất cứ lúc nào. Nhưng ở mức thời gian chạy, các luồng "mang lại" bất cứ khi nào chúng phân bổ bộ nhớ. Vì hầu hết tất cả các luồng Haskell liên tục phân bổ, nên điều này thường hoạt động khá tốt.

Tuy nhiên, nếu một phép tính cụ thể có thể được tối ưu hóa thành mã không phân bổ, nó có thể trở nên không hợp tác ở cấp độ thời gian chạy và do đó không được ưu tiên ở cấp độ Haskell. Như @Carl đã chỉ ra, đây thực sự là -fomit-yieldslá cờ, được ngụ ý bởi -O2điều đó cho phép điều này xảy ra:

-fomit-yields

Yêu cầu GHC bỏ qua kiểm tra heap khi không thực hiện phân bổ. Mặc dù điều này cải thiện kích thước nhị phân khoảng 5%, nhưng điều đó cũng có nghĩa là các luồng chạy trong các vòng không phân bổ chặt chẽ sẽ không được ưu tiên một cách kịp thời. Nếu điều quan trọng là luôn có thể làm gián đoạn các luồng như vậy, bạn nên tắt tối ưu hóa này. Cũng xem xét việc biên dịch lại tất cả các thư viện với việc tối ưu hóa này đã bị tắt, nếu bạn cần đảm bảo tính gián đoạn.

Rõ ràng, trong thời gian chạy một luồng (không có -threadedcờ), điều này có nghĩa là một luồng hoàn toàn có thể bỏ đói tất cả các luồng khác. Ít rõ ràng hơn, điều tương tự có thể xảy ra ngay cả khi bạn biên dịch -threadedvà sử dụng +RTS -Ncác tùy chọn. Vấn đề là một luồng không hợp tác có thể bỏ đói bộ lập lịch thời gian chạy . Nếu tại một thời điểm nào đó, luồng không hợp tác là luồng duy nhất hiện đang chạy, nó sẽ không bị gián đoạn và bộ lập lịch sẽ không bao giờ được chạy lại để xem xét lập lịch cho các luồng bổ sung, ngay cả khi chúng có thể chạy trên các luồng O / S khác.

Nếu bạn chỉ đang thử kiểm tra một số thứ, hãy thay đổi chữ ký fibthành fib :: Integer -> Integer. Vì Integernguyên nhân phân bổ, mọi thứ sẽ bắt đầu hoạt động trở lại (có hoặc không có -threaded).

Nếu bạn gặp phải vấn đề này trong mã thực , thì giải pháp đơn giản nhất, là giải pháp được đề xuất bởi @Carl: nếu bạn cần đảm bảo tính gián đoạn của các luồng, mã sẽ được biên dịch -fno-omit-yields, giữ cho các cuộc gọi của trình lập lịch trong mã không phân bổ . Theo tài liệu, điều này làm tăng kích thước nhị phân; Tôi cho rằng nó đi kèm với một hình phạt hiệu suất nhỏ, quá.

Ngoài ra, nếu tính toán đã có IO, thì rõ ràng yielding trong vòng lặp tối ưu hóa có thể là một cách tiếp cận tốt. Đối với một tính toán thuần túy, bạn có thể chuyển đổi nó thành IO và yieldmặc dù thông thường bạn có thể tìm thấy một cách đơn giản để giới thiệu phân bổ lại. Trong hầu hết các tình huống thực tế, sẽ có một cách để chỉ giới thiệu "một vài" yieldhoặc phân bổ - đủ để làm cho luồng phản hồi trở lại nhưng không đủ để ảnh hưởng nghiêm trọng đến hiệu suất. (Ví dụ: nếu bạn có một số vòng lặp đệ quy lồng nhau yieldhoặc buộc phân bổ trong vòng lặp ngoài cùng.)


4
Cũng xem xét biên dịch với -fno-omit-yields, kể từ khi -O2ngụ ý -fomit-yields. download.haskell.org/~ghc/latest/docs/html/usftimefide/iêu
Carl

Đúng! Tôi nghĩ rằng có một lá cờ có liên quan, nhưng tôi không thể nhớ tên hoặc tìm thấy nó trong hướng dẫn. Đây thường sẽ là cách khắc phục dễ dàng và đáng tin cậy nhất và tôi đã cập nhật câu trả lời của mình cho phù hợp.
KA Buhr
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.