none
ワーカープロセス内からのテキストファイル書込み時の"別のプロセスで使用されているため、プロセスはファイル'○○○'にアクセスできません。"の例外出力の件 RRS feed

  • 質問

  • 以前、VBのフォーラムに投稿して、それも解決に至っていないままですが、更にご教示願いたいことがあります。

     

    表記の内容のとおりなのですが、下記のようなコードを持つメソッドを、ワーカプロセス内で処理中プロセス内から

    呼び出しております。

    平たく言うと、単にデバッグ用のログファイルを出力しているのですが

    まれに、表記のように

    "別のプロセスで使用されているため、プロセスはファイル'○○○'にアクセスできません。"

    という例外が出力されます。

    (尚、"プロセスはファイル'○○○'にアクセスできません"の○○○には、勿論 filepath が表示されていますので、他の個所の書込み時エラーではないと判断しています。)

    こうならないように、"System.IO.TextWriter.Synchronized"を使用したコードにしているつもりなのですが

    なぜか発生します。

    Usingを使って、ファイルのクローズなどの後始末は出来ているはずなんですが...

    ちなみに、冗長にはなると思うのですが、①のように、明示的なクローズ処理を入れても同じでした

     

    '書き込みファイルの有無判定メソッド
    Dim mode As System.Boolean = mFileExists(filePath)

    'メッセージ書込み
    Using sw = New System.IO.StreamWriter(filePath, mode, System.Text.Encoding.Unicode)
        Dim sbmsg As New System.Text.StringBuilder
        sbmsg.Length = 0
        sbmsg.Append(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]: ")).Append(msg)
        sw.Flush()
        Using tw As System.IO.TextWriter = System.IO.TextWriter.Synchronized(sw)
            tw.WriteLine(sbmsg.ToString)
        End Using
        sbmsg = Nothing
        sw.Close() ------------- ①
    End Using
    2011年10月24日 6:42

回答

  • 例として、1 つの Web アプリケーションが 1 つのワーカー プロセスで構成されているとして、ここに 2 台の PC から同時にリクエストがやってくる場合、サーバー側は 2 つのリクエストを同時に処理します。
    この時に、1 つのワーカー プロセスの中で 2 つのリクエストがマルチ スレッドに処理されます。
    ですので、やはりコーディングもマルチ スレッドを考慮する必要があります。

    再帰についてですが、IOException が起こり続けるケースというのは当然考慮しないといけないわけで、もし再帰でリトライしてしまうと再帰し続けることになってしまいます。
    再帰呼び出しはメソッドの呼出しですので、再帰し続けるとスタックを消費し続けてしまいますよね。
    それはいかがなものかと思うのです。

    • 回答としてマーク RNC-COM 2011年10月25日 13:37
    2011年10月25日 8:54

すべての返信

  • 件のメソッド自体、あるいは StreamWriter の Using 句は排他制御されているのでしょうか?
    2011年10月24日 7:05
  • > totojo様

    毎度、レス頂きまして、ありがとうございます。

     

    上記コードを有するメソッドをスレッドセーフにする必要があるということでしょうか?

    Using句をを排他制御ですか?

     

    排他ロジックを書かないまでも、TextWriter.Synchronized()自体がStreamWriterをスレッドセーフに変えるラッパーだと思っていたのですが

    私の解釈が誤っているのでしょうか?

    それとも、Synchronized()の使い方が誤ってるのでしょうか?

     

    質問に質問で返すようなまねをして申し訳ないのですが、たぶん私が totojo様 の質問されている意図を理解していないように思います...

     

    2011年10月24日 8:20
  • もしや、コード的には似て否なるや...


    '書き込みファイルの有無判定メソッド
    Dim mode As System.Boolean = mFileExists(filePath)

    'メッセージ書込み
    Using sw = New System.IO.StreamWriter(filePath, mode, System.Text.Encoding.Unicode), _
         tw As System.IO.TextWriter = System.IO.TextWriter.Synchronized(sw)
        Dim sbmsg As New System.Text.StringBuilder
        sbmsg.Length = 0
        sbmsg.Append(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]: ")).Append(msg)
        sw.Flush()
        tw.WriteLine(sbmsg.ToString)
        sbmsg = Nothing
    End Using

    こうでないとだめだとか?ですか?

     
    2011年10月24日 8:33
  • メソッドの丸ごととか Using ブロックの塊でとか、何らかの排他制御をしないといけないと思うのです。
    複数のスレッドが同時に StreamWriter で同じファイルを Open しようとした場合、後から Open しようとした方が Open エラーになるのではないかと思います。
    TextWriter はスレッドセーフになっていても、StreamWriter を Open することに対してはスレッドセーフになっていないのではないかと。
    2011年10月24日 8:51
  • なるほどです。

    .NET framework 2.0 でいうところの「System.Threading.ReadWriteLockクラス」などを使って

    StreamWriterのオープンを排他制御するということですね。

    やはり、必要なんですね、排他処理が...

    これから作ってみます。

     

    取り敢えずありがとうございました。

    結果の確認が取れるまで、一応マークは付けずにおきます。

     

    2011年10月24日 9:23
  • 下記のようなコードを書き、

    1.複数起動されたワーカープロセス内から、同一ファイルに対して書込みを競合させる。
    2.別マシンの別プロセス内から、同一ファイルに対して書込みを競合させる。

    そもそも、マルチスレッドという概念がことこのシステムには無いために、「System.Threading.ReadWriteLockクラス」
    を使用して、コーディングすることは無意味?かと判断し、ミューテックスに変えました。
    今の、アプリケーションの機能上、例えばファイルエディタなどで、アプリケーションからの更新中に該当ファイルを開いて編集ということは、
    実質無いという前提で記述しています。
    あるとすれば、同一マシンの複数プロセス(IISのワーカープロセス)から同一ファイルにアクセスする。(項番1)
    もしくは、ネットワーク共有パス上のexeを共有フォルダ経由で複数のコンピュータから実行させるなどの場合(項番2)
    になるとの考えです。

    項番1に関しては名前付きミューテックスによりwait処理できると思いますが、項番2に関しては、System.IO.IOException
    発生時に、リトライを行う?ということくらいしか思いつかなかったために、System.IO.IOException発生時に、自身を再帰呼出し
    する方法を取りました...
    この、自身を再帰呼び出しがスマートではないし、もっと他にやり方があるのでは?とおも思ったのですが...
    そもそもそれなら、最初からいろいろ書かずに「System.IO.IOException発生時に、自身を再帰呼出し」でいいのではないのかとも
    考え始めました  orz





    Public Class hoge

        '同一マシン上複数プロセスからのログファイル同時書込み対策用ミューテックス
        Private Shared logMut As System.Threading.Mutex

        Shared Sub New()
            '同一マシン上複数プロセスからのログファイル同時書込み対策用名前付きミューテックス作成
            logMut = New System.Threading.Mutex(False, "logWrite")
        End Sub
      
      
        Public Shared Sub mPutLog(ByVal msg As System.String)
            Dim filePath As System.String = ""
            Dim fileName As System.String = "LogFile" & DateTime.Now.ToString("_yyyy_MM_dd") & ".txt"
           
            'ファイルパスの取得
            filePath = mGenarateFilePath(fileName)
            'ファイルの有無判定
            mode = mFileExists(filePath)
            '存在しない場合は新規作成
            Dim fileMode As System.IO.FileMode = IO.FileMode.CreateNew
            If mode Then fileMode = IO.FileMode.Append

            'メッセージ書込み
            logMut.WaitOne()
            Try
                Using fs As New System.IO.FileStream(filePath, fileMode, IO.FileAccess.Write, IO.FileShare.ReadWrite)
                    Using sw = New System.IO.StreamWriter(fs, System.Text.Encoding.Unicode), _
                         tw As System.IO.TextWriter = System.IO.TextWriter.Synchronized(sw)
                        Dim sbmsg As New System.Text.StringBuilder
                        sbmsg.Length = 0
                        sbmsg.Append(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]: ")).Append(msg)
                        sw.Flush()
                        tw.WriteLine(sbmsg.ToString)
                        sbmsg = Nothing
                    End Using
                End Using
            Catch ex As System.IO.IOException
                '他のマシンのプロセスからの書込み要求時の例外発生時は、自分を再帰呼出し
                mPutLog(msg)
            Catch ex As Exception
                Throw
            Finally
                logMut.ReleaseMutex()
            End Try
        End Sub

    End Class

    2011年10月25日 7:34
  • そもそも、マルチスレッドという概念がことこのシステムには無いために、「System.Threading.ReadWriteLockクラス」
    を使用して、コーディングすることは無意味?かと判断し、ミューテックスに変えました。

    ミューテックスを使うことに異論はありませんが、マルチスレッドの概念がないというのは問題ではないかと思います。
    ワーカー プロセスとは ASP.NET の話だと思いますが、複数のリクエストが同時にやってくると、サーバー側では複数のセッションが同時に実行されます。
    この時にマルチスレッドの意識がいると思うのです。

    そもそもそれなら、最初からいろいろ書かずに「System.IO.IOException発生時に、自身を再帰呼出し」でいいのではないのかとも
    考え始めました  orz

    ループしながらリトライだと話は分かりますが、再帰はちょっといただけないと思います。
    際限なく再帰し続けたら問題ですよね?
    2011年10月25日 7:56
  • 誤っているようでしたら、ご指摘いただきたいのですが...
    ワーカープロセスは、マルチプロセスであって、マルチスレッドとは違うと考えていました...
    仰られている様に、IISがクライアントからの処理要求によって、動的に作成されるプロセスだということは理解しているつもりです。
    その上で、動的に呼び出されるプロセス内に、意図的にマルチスレッドのコーディングをしていないということで、「マルチスレッドは考慮していない..」とのフレーズを用いました。


    あと、参考までにご教示願いたいのですが...
    System.IO.IOException 発生時に例外をトラップして再起呼び出しは危険で、ループでリトライと書いていただきましたがこれは実際にはどういった手法になるのでしょう..

    私が提示したコードでは、いずれかのプロセスがファイルを書込みのためにシェアーしているところへ、ミューテックスでは管理できない別プロセスが書込みを行おうとした?場合に
    発生する System.IO.IOException をトラップして、そのタイミングで再帰呼出し処理を記述していますが

    ループでリトライというのは、やはりトリガーは System.IO.IOException になるわけでしょうか?
    その後どの値を監視しながらリトライ処理待ちを行うべきですか?

    私的には、何度も何度も Exceptionが発生しなくなるまで、書込みをし続けてみることが、リトライになるのだという、半ばこじつけのような理由で収めようとも考えたのですが...

    2011年10月25日 8:24
  • 例として、1 つの Web アプリケーションが 1 つのワーカー プロセスで構成されているとして、ここに 2 台の PC から同時にリクエストがやってくる場合、サーバー側は 2 つのリクエストを同時に処理します。
    この時に、1 つのワーカー プロセスの中で 2 つのリクエストがマルチ スレッドに処理されます。
    ですので、やはりコーディングもマルチ スレッドを考慮する必要があります。

    再帰についてですが、IOException が起こり続けるケースというのは当然考慮しないといけないわけで、もし再帰でリトライしてしまうと再帰し続けることになってしまいます。
    再帰呼び出しはメソッドの呼出しですので、再帰し続けるとスタックを消費し続けてしまいますよね。
    それはいかがなものかと思うのです。

    • 回答としてマーク RNC-COM 2011年10月25日 13:37
    2011年10月25日 8:54
  • ご説明いただいた内容は、自分なりに理解したつもりです。
    例として、1 つの Web アプリケーションが 1 つのワーカー プロセスで構成されているとして、ここに 2 台の PC から同時にリクエストがやってくる場合、サーバー側は 2 つのリクエストを同時に処理します。
    この時に、1 つのワーカー プロセスの中で 2 つのリクエストがマルチ スレッドに処理されます。
    ですので、やはりコーディングもマルチ スレッドを考慮する必要があります。
    この内容はワーカープロセスをWebガーデン設定している場合は、別スレッド → 別プロセス と読み替えるということでいいのですね?
    #まぁWebガーデンの設定プロセス数と実際のリクエスト数にもよると思いますが...
    再帰呼び出し処理に関しては、一応対応策として、実際のファイルアクセスの部分をサブメソッド化して、上位で500ミリ秒間 リトライするというような方式です。
    Public Shared Sub mPutLog(ByVal msg As System.String)
        Dim filePath As System.String = ""
        Dim fileName As System.String = "LogFile" & DateTime.Now.ToString("_yyyy_MM_dd") & ".txt"
    
        'ログファイルパス取得
        filePath = mGenaratePath(fileName)
    
        Dim t1 As System.Int32 = System.Environment.TickCount
    
        '書き込み競合時に最大500ミリ秒間リトライ処理
        While ((System.Environment.TickCount - t1) < 500)
            If mPutLogWork(filePath, msg) Is Nothing Then Exit While
        End While
    
    End Sub
    
    Private Shared Function mPutLogWork(ByVal filePath As System.String, ByVal msg As System.String) As Exception
        Dim RetEx As System.Exception = Nothing
    
        '存在しない場合は新規作成
        Dim fileMode As System.IO.FileMode = IO.FileMode.CreateNew
        If mFileExists(filePath) Then fileMode = IO.FileMode.Append
    
        'メッセージ書込み
        logMut.WaitOne()
        Try
            Using fs As New System.IO.FileStream(filePath, fileMode, IO.FileAccess.Write, IO.FileShare.ReadWrite)
                Using sw = New System.IO.StreamWriter(fs, System.Text.Encoding.Unicode), _
                     tw As System.IO.TextWriter = System.IO.TextWriter.Synchronized(sw)
                    Dim sbmsg As New System.Text.StringBuilder
                    sbmsg.Length = 0
                    sbmsg.Append(DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss.fff]: ")).Append(msg)
                    sw.Flush()
                    tw.WriteLine(sbmsg.ToString)
                    sbmsg = Nothing
                End Using
            End Using
        Catch ex As Exception
            RetEx = ex
        Finally
            logMut.ReleaseMutex()
        End Try
    
        Return RetEx
    End Function
    
    
    のようなソースを書きました。 このソースで、自分なりに検証した限り、問題なく動作しているようです。 この件に関しては、これでフィックスとしようかと考えています。
    2011年10月25日 13:13
  • ループでリトライもよいのですが、待ち時間なしでループするとサーバー側で非常に重く、時間のかかるプロセスになる恐れがあります。
    (想定外にファイルを排他的に開かれていた場合とか)
    その呼び出し頻度次第ですが、ログ出力の方法を見直すことも考えるべきかも。
    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    2011年10月25日 13:33
    モデレータ
  • ループでリトライもよいのですが、待ち時間なしでループするとサーバー側で非常に重く、時間のかかるプロセスになる恐れがあります。
    (想定外にファイルを排他的に開かれていた場合とか)
    その呼び出し頻度次第ですが、ログ出力の方法を見直すことも考えるべきかも。
    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    確かにそうですね、ループ中の考慮をします。

    またご心配いただいております、このログ出力自体の重要性なのですが、
    正式にはDBにログを出力しており、一時的にというかログテキストファイルへの出力は
    モードフラグを設けておりますので、年中というかシステム稼働中必ず出力されるものではなく
    問題発生時に、DBの内容を精査せずとも、ログファイルに一時的にトレースして
    解析するために使用するものと考えていますので、重要性的にはさほど大きいものではありません

    だからといって安易に考えるのもいけないのでしょうが...


    というわけですから、これ以上時間をかけて検証及び機能組み込みをするのは、今後の課題程度にしようと考えています。

    2011年10月25日 13:51
  • この内容はワーカープロセスをWebガーデン設定している場合は、別スレッド → 別プロセス と読み替えるということでいいのですね?


    よくありません。
    Webガーデンを使用している場合でも基本は同じです。

    もちろんWebガーデンの場合は複数プロセスが同時に動作していますが、これは複数プロセスがそれぞれ複数スレッドで動作しているということであって、複数スレッドを使用しなくなるわけではありません。

     

    2011年10月25日 14:18
  • その上で、動的に呼び出されるプロセス内に、意図的にマルチスレッドのコーディングをしていないということで、「マルチスレッドは考慮していない..」とのフレーズを用いました。


    ここ、ちょっと気になりました。

    リソースが、プロセスの内側にあるのか、外側にあるのか、考えてください。今回の問題は、プロセスの外側にあるファイルを扱う場合に発生しました。すると、マルチ スレッドでなくても、複数のプロセスから1つのリソースにアクセスするために、衝突が発生し得ます。従ってここは、マルチ プロセスとして、考慮しなければならなかった箇所、ということになります。ファイル サーバーにあるエクセル ファイルを複数の人が同時に編集状態で開くことはできません。それと同じ状態です。


    Jitta@わんくま同盟
    2011年10月26日 13:32
  • 横からコード指摘失礼致します。

    1、ループの条件に System.Environment.TickCountを使用してますが、
      TickCountは24.9 日でオーバーフローしますので注意です

    ■Environment.TickCount プロパティ
    http://msdn.microsoft.com/ja-jp/library/system.environment.tickcount(v=VS.100).aspx

    2、ログファイル名とログ書き出し時にDateTime.Nowをそれぞれ取得してますが、
      書き込みのリトライのタイミングで日付が変わるったときに前の日付のログファイルにログが書き込まれます。

    3、ログファイルはアプリケーションごとに分けないのでしょうか?
      日付単位のログファイルだと大量にログ出力した場合はパフォーマンスがわるくなりますよ。
      一度計測されることをお勧めいたします。
      例えばログ出力レベルをログ出力関数にもたせるとか。。

    2011年10月26日 14:01