2011-07-10 6 views
0

コードがコンソールプログラムにコンパイルされているか、 fsiとして実行されていると、スレッドの一部が終了する前に終了します。--use:Program.fs - exec --quiet。すべてのスレッドが終了するのを待つ方法はありますか?コンパイルされたコンソールコマンドラインプログラムは、すべてのスレッドが終了するのを待機しません

この問題は、複数のMailboxProcessersが存在する場合に "プログラム終了の問題"と記述することができます。

出力例

(最後の行は切り捨てられ、最後の出力機能(printfn "[Main] after crawl")が実行されることはありません注意してください。)

 
[Main] before crawl 
[Crawl] before return result 
http://news.google.com crawled by agent 1. 
[supervisor] reached limit 
Agent 5 is done. 
http://www.gstatic.com/news/img/favicon.ico crawled by agent 1. 
[supervisor] reached limit 
Agent 1 is done. 
http://www.google.com/imghp?hl=en&tab=ni crawled by agent 4. 
[supervisor] reached limit 
Agent 4 is done. 
http://www.google.com/webhp?hl=en&tab=nw crawled by agent 2. 
[supervisor] reached limit 
Agent 2 is done. 
http://news.google. 

コード

編集:いくつかを追加しましたSystem.Threading.Thread.CurrentThread.IsBackground <- false。私はゼロのF#を知っているが、一般的にあなたが興味のあるすべてのスレッドがThread.Joinを使用して待つ

open System 
open System.Collections.Concurrent 
open System.Collections.Generic 
open System.IO 
open System.Net 
open System.Text.RegularExpressions 

module Helpers = 

    type Message = 
     | Done 
     | Mailbox of MailboxProcessor<Message> 
     | Stop 
     | Url of string option 
     | Start of AsyncReplyChannel<unit> 

    // Gates the number of crawling agents. 
    [<Literal>] 
    let Gate = 5 

    // Extracts links from HTML. 
    let extractLinks html = 
     let pattern1 = "(?i)href\\s*=\\s*(\"|\')/?((?!#.*|/\B|" + 
         "mailto:|location\.|javascript:)[^\"\']+)(\"|\')" 
     let pattern2 = "(?i)^https?" 

     let links = 
      [ 
       for x in Regex(pattern1).Matches(html) do 
        yield x.Groups.[2].Value 
      ] 
      |> List.filter (fun x -> Regex(pattern2).IsMatch(x)) 
     links 

    // Fetches a Web page. 
    let fetch (url : string) = 
     try 
      let req = WebRequest.Create(url) :?> HttpWebRequest 
      req.UserAgent <- "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)" 
      req.Timeout <- 5000 
      use resp = req.GetResponse() 
      let content = resp.ContentType 
      let isHtml = Regex("html").IsMatch(content) 
      match isHtml with 
      | true -> use stream = resp.GetResponseStream() 
         use reader = new StreamReader(stream) 
         let html = reader.ReadToEnd() 
         Some html 
      | false -> None 
     with 
     | _ -> None 

    let collectLinks url = 
     let html = fetch url 
     match html with 
     | Some x -> extractLinks x 
     | None -> [] 

open Helpers 

// Creates a mailbox that synchronizes printing to the console (so 
// that two calls to 'printfn' do not interleave when printing) 
let printer = 
    MailboxProcessor.Start(fun x -> async { 
     while true do 
     let! str = x.Receive() 
     System.Threading.Thread.CurrentThread.IsBackground <- false 
     printfn "%s" str }) 
// Hides standard 'printfn' function (formats the string using 
// 'kprintf' and then posts the result to the printer agent. 
let printfn fmt = 
    Printf.kprintf printer.Post fmt 

let crawl url limit = 
    // Concurrent queue for saving collected urls. 
    let q = ConcurrentQueue<string>() 

    // Holds crawled URLs. 
    let set = HashSet<string>() 


    let supervisor = 
     MailboxProcessor.Start(fun x -> async { 
      System.Threading.Thread.CurrentThread.IsBackground <- false 
      // The agent expects to receive 'Start' message first - the message 
      // carries a reply channel that is used to notify the caller 
      // when the agent completes crawling. 
      let! start = x.Receive() 
      let repl = 
       match start with 
       | Start repl -> repl 
       | _ -> failwith "Expected Start message!" 

      let rec loop run = 
       async { 
        let! msg = x.Receive() 
        match msg with 
        | Mailbox(mailbox) -> 
         let count = set.Count 
         if count < limit - 1 && run then 
          let url = q.TryDequeue() 
          match url with 
          | true, str -> if not (set.Contains str) then 
               let set'= set.Add str 
               mailbox.Post <| Url(Some str) 
               return! loop run 
              else 
               mailbox.Post <| Url None 
               return! loop run 

          | _ -> mailbox.Post <| Url None 
            return! loop run 
         else 
          printfn "[supervisor] reached limit" 
          // Wait for finishing 
          mailbox.Post Stop 
          return! loop run 
        | Stop -> printfn "[Supervisor] stop"; return! loop false 
        | Start _ -> failwith "Unexpected start message!" 
        | Url _ -> failwith "Unexpected URL message!" 
        | Done -> printfn "[Supervisor] Supervisor is done." 
           (x :> IDisposable).Dispose() 
           // Notify the caller that the agent has completed 
           repl.Reply(()) 
       } 
      do! loop true }) 


    let urlCollector = 
     MailboxProcessor.Start(fun y -> 
      let rec loop count = 
       async { 
        System.Threading.Thread.CurrentThread.IsBackground <- false 
        let! msg = y.TryReceive(6000) 
        match msg with 
        | Some message -> 
         match message with 
         | Url u -> 
          match u with 
          | Some url -> q.Enqueue url 
              return! loop count 
          | None -> return! loop count 
         | _ -> 
          match count with 
          | Gate -> (y :> IDisposable).Dispose() 
             printfn "[urlCollector] URL collector is done." 
             supervisor.Post Done 
          | _ -> return! loop (count + 1) 
        | None -> supervisor.Post Stop 
           return! loop count 
       } 
      loop 1) 

    /// Initializes a crawling agent. 
    let crawler id = 
     MailboxProcessor.Start(fun inbox -> 
      let rec loop() = 
       async { 
        System.Threading.Thread.CurrentThread.IsBackground <- false 
        let! msg = inbox.Receive() 
        match msg with 
        | Url x -> 
         match x with 
         | Some url -> 
           let links = collectLinks url 
           printfn "%s crawled by agent %d." url id 
           for link in links do 
            urlCollector.Post <| Url (Some link) 
           supervisor.Post(Mailbox(inbox)) 
           return! loop() 
         | None -> supervisor.Post(Mailbox(inbox)) 
            return! loop() 
        | _ -> printfn "Agent %d is done." id 
          urlCollector.Post Done 
          (inbox :> IDisposable).Dispose() 
        } 
      loop()) 

    // Send 'Start' message to the main agent. The result 
    // is asynchronous workflow that will complete when the 
    // agent crawling completes 
    let result = supervisor.PostAndAsyncReply(Start) 
    // Spawn the crawlers. 
    let crawlers = 
     [ 
      for i in 1 .. Gate do 
       yield crawler i 
     ] 

    // Post the first messages. 
    crawlers.Head.Post <| Url (Some url) 
    crawlers.Tail |> List.iter (fun ag -> ag.Post <| Url None) 
    printfn "[Crawl] before return result" 
    result 

// Example: 
printfn "[Main] before crawl" 
crawl "http://news.google.com" 5 
|> Async.RunSynchronously 
printfn "[Main] after crawl" 
+1

コードを単純化しても問題は解決しますが、無関係のコードがたくさん含まれていませんか? – svick

答えて

0

プリンタエージェントのlet!の後にSystem.Threading.Thread.CurrentThread.IsBackground <- falseを追加することで、問題を解決したようです。

let!の後に元のコード(TomasのAsyncChannel修正の前の最初のバージョン)をSystem.Threading.Thread.CurrentThread.IsBackground <- falseに変更しようとしましたが、それでも機能しません。いい考えはありません。

ありがとうございました。私は最終的にバッチ処理のための最初のF#アプリケーションを開始することができます。私はMailboxProcessorが実際にIsBackgroundをデフォルトでfalseに設定するべきだと思う。とにかくそれを変更するようにMicrosoftに依頼する。

[更新]コンパイルされたアセンブリがうまく機能することがわかりました。しかし、fsi --user:Program --exec --quietはまだ同じです。 fsiのバグのようですか?

+0

フィーチャー提案は、[email protected]に送信できます。 – kvb

+0

メールボックスプロセッサスレッドはすべてのスレッドプールスレッドと同様に、バックグラウンドスレッドである必要があります。プログラム終了を停止するには制御フローを使用する必要があります。 – 7sharp9

0

警告。あなたの場合のように私に見えますが、.Startへの呼び出しによって開始された興味のあるものを待つ必要があります。

また、未加工の管理対象スレッドに対してより高いレベル(より簡単な)抽象化を提供するタスク並列ライブラリを検討することもできます。タスクが完了するのを待つ例here

+0

ありがとうございます。タスク並列ライブラリはF#ではまだ利用できません。 MailboxProcessorはこの種のタスクを処理するうえで便利なメカニズムだと思います。 MailboxProcessorにはまだ複数のMailboxProcessersのThread.Joinのようなものはありません。 – ca9163d9

+2

TPLは、F#から100%使用可能です(すべての.NETライブラリと同様)。しかし、F#ライブラリから期待されるような構文上の甘さがないかもしれません。また、あなたがコードを実行しているように見えるので、私はMailboxProcessorsが良い "フィット"だと思います。 – pblasucci

3

コードを正しく認識すると、それはyour previous question(と私の答え)に基づいています。

プログラム(Startメッセージを送信した後RunSynchronouslyを使用して応答を待つことによって)スーパバイザ・エージェントまで完了を待ちます。これにより、アプリケーションが終了する前にメインエージェントとすべてのクローラが完了することが保証されます。

問題は、printerエージェントが完了するまで待機しないことです。したがって、(再定義された)printfnファンクションへの最後の呼び出しにより、エージェントにメッセージが送信され、印刷エージェントが終了するまで待たずにアプリケーションが完了します。

私が知る限り、エージェントが現在キューにあるすべてのメッセージの処理を完了するまで待機するための「標準パターン」はありません。あなたが試みることができるいくつかのアイデアがあります:

  • あなたは(それが0になるまで待つ)CurrentQueueLengthプロパティを確認することができ、それはまだエージェントは、すべてのメッセージを処理を完了したことを意味するものではありません。

  • 新しいタイプのメッセージを追加し、エージェントがそのメッセージに返信するまで待つことでエージェントをより複雑にすることができます(現在Startメッセージへの返信を待っているのと同じです)。

+0

はい、以前の質問とあなたの答えに基づいています。解決に感謝します。監督者以外のすべてのエージェントも「開始」メッセージを必要としているようであり、現在は(3 + 5)のエージェントが存在するため、複雑になる可能性があります。より良い解決策があるかどうか疑問に思うだけです。 – ca9163d9

+0

@NickW - 'supervisor'は、すべてのクローラから' Done'メッセージを受信した後で 'Done'メッセージを返します(すべてのクローラから' Done'メッセージを受信する必要があります)。したがって、監督者は他のすべての(プリンタを除いて)完了した後に終了する必要があります。私は解決策が少し複雑すぎると思う。私はおそらく、このエージェントを使用して実装されたたった一つのURLキューを使用するだろう:http://tomasp.net/blog/parallel-extra-blockingagent.aspx –

0

.NETスレッドにはプロパティがあります。Thread.IsBackgroundこれがtrueに設定されていると、スレッドはプロセスの終了を妨げません。 falseに設定すると、プロセスが終了しなくなります。参照してください:http://msdn.microsoft.com/en-us/library/system.threading.thread.isbackground.aspx

エージェントを実行するスレッドはスレッドプールから来て、Thread.IsBackgroundをデフォルトでfalseに設定します。

メッセージを読むたびにスレッドのIsBackgroundをfalseに設定してみてください。あなたは、これを行うための機能を追加して、アプローチをよりきれいにすることができます。 letを使用するたびに、おそらくこの問題に対する最良の解決策ではありません。スレッドを変更して、正しく動作するように慎重に実装する必要があります。私はちょうど特定の質問に答えるためにそれを言いました

すべてのスレッドが終了するのを待つ方法はありますか?

そして、特定のスレッドがプログラムを終了させる理由と他のスレッドが終了しなかった理由を理解するのに役立ちます。

+0

それはそれ以外の方法ではない? http://msdn.microsoft.com/en-us/library/h339syd0.aspx – 7sharp9

関連する問題