tagCANDY CGI VBレスキュー(花ちゃん) の Visual Basic 2010 用 掲示板(VB.NET 掲示板) [ツリー表示へ]   [Home]
一括表示(VB.NET VB2005)
タイトル共有フォルダ上のあるファイルへ複数ユーザーからランダムアクセスすると読みだせなくなる
記事No11986
投稿日: 2019/07/09(Tue) 18:28
投稿者皆月
知恵を貸してください。

プログラム@
パソコン(A)より共有フォルダ(NAS)上の「yyMMdd.txt」へ固定文字列長(Sift-JIS)の文字列を
1秒に1回、追記し続けています。(日付が変わるとファイル名も変わります)

プログラムA
パソコン(B〜/複数台)より共有フォルダ(NAS)上の「yyMMdd.txt」より
Seekで新しい固定文字列長(Shift-JIS)を読みだしています。

困っていること:
パソコン数台でデータを読みだしている間は問題なく動いているのですが、
プログラムAを実行するパソコンが増えてくると、一部パソコンで「yyMMdd.txt」より
固定文字列が読み込めなく?なります。 ※Try Catchでは特にエラーは出ていません
デバッグで確認すると固定文字列がNull?になっていました。
ランダムアクセス?での同時アクセス数に限界があるのかな?と素人推論しています。
感覚でいうと10台ぐらいで発生している気がします。

【Q.】どうにか読み出すパソコンが増えても動作するように改造したいのですが、何か良い案はありませんか?

ファイルアクセス数に問題があるのならパソコン(A)よりUDP通信で
パソコン(B〜)に文字列配信しようかとも考えてはいますが、二の足踏んでいます。
データベース化すればいいのかもしれませんが、小生の力量では現状不可です。

開発環境:Windows7 64bit VisualBasic2010 Express / NetFramework3.5用アプリケーション
実行環境:Win7(32bit/64bit)、Win10(64bit)
共有フォルダ:Linuxサーバー(バッファローの安いNAS)

******************
※ c変数名 はConstで宣言してある文字列です

プログラム@(抜粋)
Private FS As FileStream
Private DataLength As Long
Private DataEnc As System.Text.Encoding
Private tim1 As New Windows.Forms.Timer

Private Sub frmMain_Load(sender As Object, e As System.EventArgs) Handles Me.Load
    '***** データ書出し処理実行 *****
    DataLength = cDataFormat.Length
    DataEnc = System.Text.Encoding.GetEncoding("Shift_JIS")
    tim1.Interval = 950    
    tim1.Enabled = True
    AddHandler tim1.Tick, AddressOf tim1_Tick
End Sub

'データを定周期書出し
    Private Sub tim1_Tick(sender As System.Object, e As System.EventArgs)

        '***日付変更確認***
        Static today As String
        If today <> Format$(Now, "yyMMdd") Or today = "" Then
            today = Format$(Now, "yyMMdd")
            CreateNewFile()
        End If

        '***書込み***
        Dim bufNow As Date = Now
        Dim seekNo As Double = CDbl(Format$(bufNow, "HH")) * 3600 _
                                + CDbl(Format$(bufNow, "mm")) * 60 + CDbl(Format$(bufNow, "ss"))
        Dim Data As String = Format$(bufNow, "HHmmss") & "-" & 固定文字列 & vbCrLf

        FS.Seek(DataLength * seekNo, SeekOrigin.Begin)
        Dim bytData As Byte() = DataEnc.GetBytes(Data)
        FS.Write(bytData, 0, DataLength)

        'Debug.Print("Now:{0} SeekNo:{1} Data:{2}", bufNow, seekNo, Data)
        lblOUT.Text = String.Format("Now:{0} SeekNo:{1} Data:{2}", bufNow, seekNo, Data)

    End Sub
    '*****ファイル準備*****
    Private Sub CreateNewFile()

        Try
            '開いていたら閉じる
            If Not FS Is Nothing Then FS.Close()

            '保存フォルダ作成(年)
            Dim filePath As String = cSaveDir & Format$(Now, "yyyy")
            If Dir(filePath, FileAttribute.Directory) = "" Then MkDir(filePath)

            'ランダムアクセスでファイルオープン  別プロセスからは読出しのみ可。
            FS = New FileStream(filePath & "\" & Format$(Now, "yyMMdd") & "txt" ,_
                        FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)

        Catch ex As Exception
            lblERR.Text = String.Format("Now:{0} ファイル切替失敗", Now)

        End Try

    End Sub

動作Aのプログラム(抜粋)
'データ書出し用
    Private FS As FileStream
    Private DataLength As Long
    Private DataEnc As System.Text.Encoding

'*****タイマーイベント*****
    Private Sub tim1_Tick(sender As System.Object, e As System.EventArgs)

        tim1.Enabled = False

        Try
            Dim bufDate As Date = Now.AddSeconds(-5) '5秒前のデータを読む
            '***日付変更確認***
            Static today As String
            If today <> bufDate.ToString("yyMMdd") Or today = "" Then
                today = bufDate.ToString("yyMMdd")
                FileOpen(bufDate)
            End If

            Dim LineN As Integer = bufDate.Hour * 3600 + bufDate.Minute * 60 _
                                     + bufDate.Second   '何行目?
            Dim flowData As String = ReadData(LineN)  '文字列を貰う

            '***貰った文字列の処理***

        Catch ex As Exception
            Debug.Print("{0}:タイマーイベントでエラー {1}", Now.ToString("HH:mm:ss"), ex.Message)

        Finally
            tim1.Enabled = True

        End Try

    End Sub

    '*****ファイル準備*****
    Private Sub FileOpen(ByVal bufDate As Date)

        '開いていたら閉じる
        If Not FS Is Nothing Then FS.Close()

        Dim fileName As String = cSaveDir & Format$(Now, "yyyy") & "\" & _
                                  Format$(bufDate, "yyMMdd") & cFileExt

        'ランダムアクセスでファイルオープン  別パソコンからは読出しのみ可。
        Debug.Print("{0}をひらきまーす", fileName)
        FS = New FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)

    End Sub

    '*****ランダムアクセスで吸い出す*****
    Private Function ReadData(ByVal LineN As Integer) As String

        Dim bufRecord(DataLength) As Byte       '読出し格納用

        Try
            FS.Seek(LineN * DataLength, SeekOrigin.Begin)       '読出し位置へ移動
            FS.Read(bufRecord, 0, DataLength)                   '何文字読み出す?

            Dim strBuf As String = DataEnc.GetString(bufRecord) 'ByteデータをStringへ変換
      ***【↑ここのbufRecordがNullになって困っています】***
            
            Debug.Print("{0}行目のデータ {1}", LineN, strBuf)

            If Mid(strBuf, 7, 1) = "-" Then Return strBuf '7文字目は[-]のはず。

        Catch ex As System.IO.IOException
            Debug.Print("{0}:データ読出しIOException →{1}({2})", _
                          Now.ToString("HH:mm:ss"), ex.ToString, ex.Message)
            'ファイルが見当たらないなら開き直そう
            Dim bufDate As Date = Now.AddSeconds(subSecond) '時間差
            FileOpen(bufDate)

        Catch ex As System.ObjectDisposedException
            Debug.Print("{0}:データ読出しObjectDisposedException →{1}({2})", _
                          Now.ToString("HH:mm:ss"), ex.ToString, ex.Message)
            'ファイルが見当たらないなら開き直そう
            Dim bufDate As Date = Now.AddSeconds(subSecond) '時間差
            FileOpen(bufDate)

        Catch ex As Exception
            Debug.Print("{0}:データ読出しでエラー {1}({2})", _
                        Now.ToString("HH:mm:ss"), ex.ToString, ex.Message)
            
        End Try

        Return cDataFormat '0データを表示させる。

    End Function

[ツリー表示へ]
タイトルRe: 共有フォルダ上のあるファイルへ複数ユーザーからランダムアクセスすると読みだせなくなる
記事No11987
投稿日: 2019/07/09(Tue) 20:12
投稿者魔界の仮面弁士
> 「yyMMdd.txt」へ固定文字列長(Sift-JIS)の文字列を
> Seekで新しい固定文字列長(Shift-JIS)を読みだしています。

Sift :ふるいにかける
Shift:(配置を)変える

…自分も時々やらかします。


> プログラムAを実行するパソコンが増えてくると、一部パソコンで「yyMMdd.txt」より
> 固定文字列が読み込めなく?なります。 ※Try Catchでは特にエラーは出ていません

FileStream のコンストラクタに、FileOptions.WriteThrough を指定して開いた上で、
レコード出力後に、FileStream.Flush(Boolean) メソッドを呼ぶようにしたら
改善されないでしょうか。


> ファイルアクセス数に問題があるのならパソコン(A)よりUDP通信で
> パソコン(B〜)に文字列配信しようかとも考えてはいますが、二の足踏んでいます。

.NET Remoting か WCF という手もあるかも。根本解決になるかは別として。


> If today <> Format$(Now, "yyMMdd") Or today = "" Then
この実装だと、OS の地域設定が和暦モードになっていた場合に誤動作するかも。

[ツリー表示へ]
タイトルRe^2: 共有フォルダ上のあるファイルへ複数ユーザーからランダムアクセスすると読みだせなくなる
記事No11988
投稿日: 2019/07/09(Tue) 23:56
投稿者皆月
魔界の仮面弁士さん、回答有り難うございます。

>>sift
\(^0^)/修正ぬけてt

>>FileStream のコンストラクタ〜〜
書き込み側ソフトのファイル開き時に、
FS = New FileStream("yyMMddtxt" ,FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)

FS = New FileStream("yyMMddtxt" ,FileMode.OpenOrCreate, FileAccess.Write, _
                      FileShare.ReadWrite , bufferSize , FileOptions.WriteThrough)  に変更

書き込み側ソフトのファイル書き込み時に
FS.Write(bytData, 0, DataLength) の下に
FileStream.Flush(Boolean) ってことですか?

Q1)bufferSize は 4096 (デフォルト)で良いものなんでしょうか?
Q2)ファイルに文字列は問題なく?書き込まれていて読み込み側の問題だと思っていましたが
  上記コードを書いて確実にデータをHDDに書込む処理を書くとなにか変わるものなのでしょうか?

とりあえず、明日実装してみます。  

>>.NET Remoting か WCF 〜
通信系はそのうち手を出さざるを得ないので手が空いたら調べてみます。

>OS の地域設定が和暦モードになっていた〜
今の所、コレではトラブルなってないので一先ずこのままで・・・

[ツリー表示へ]
タイトルRe^3: 共有フォルダ上のあるファイルへ複数ユーザーからランダムアクセスすると読みだせなくなる
記事No11989
投稿日: 2019/07/10(Wed) 12:00
投稿者魔界の仮面弁士
> FS.Write(bytData, 0, DataLength) の下に
> FileStream.Flush(Boolean) ってことですか?

失礼しました。 VB2010 といっても、
.NET Framework 4 を使っているのではなく、
.NET Framework 3.5 での開発なのですね。

Flush(Boolean) は .NET Framework 4 で追加されたメソッドです。
3.5 で使えるのは Flush() ですね。これは Flush(False) に相当します。


FileStream は、Read/Write のための内部バッファーを持っていますが、
Write 時にすべての内容が即座にファイルに出力されるとは限らず、
書き込まれずに内部バッファに残留した状態になることがあります。
この残留バッファは、即座に出力されることは無く、
「次回の Write 時」「Close あるいは Flush された時」まで保留されます。

Flush() あるいは Flush(False) を呼び出すと、このバッファが
直ちに出力されてファイルに書き込まれます(Read バッファの Seek 補正も行われます)。

一方 Flush(True) メソッドは、Flush(False) 相当の処理が行われた後で、
さらに続けて FlushFileBuffers API を呼び出すように設計されています。
これは、.NET 側ではなく、OS のライトバッファをフラッシュするための命令です。
(これを呼ばずとも、書き込み後暫くすると自動的にフラッシュされるはずですが、
 負荷状況によってはフラッシュのタイミングが変わることがあります)

もしも .NET 3.5 で Flush(True) 相当の処理を行いたい場合は、
 fs.Flush()
 FlushFileBuffers(fs.Handle)
というコードで処理できるかもしれません。試していませんけど。

http://rarara.cafe.coocan.jp/cgi-bin/lng/vc/vclng.cgi?print+200405/04050054.txt


> Q1)bufferSize は 4096 (デフォルト)で良いものなんでしょうか?

bufferSize は、Read / Write 時の「既定のバッファーサイズ」を表します。

今回は、固定長のランダムアクセスファイルのようなので、
たとえば各レコード長を表す Private DataLength As Long と
同じ値またはその倍数にしておくのはどうでしょうか。


> Q2)ファイルに文字列は問題なく?書き込まれていて読み込み側の問題だと思っていましたが
>  上記コードを書いて確実にデータをHDDに書込む処理を書くとなにか変わるものなのでしょうか?

どうでしょうね。デバイスにもよるところなので、実検証は必要ですが、
少なくとも Flush() は行っておいた方が良いと思います。

コンストラクタで FileOptions.WriteThrough を指定した場合は、
バッファを経由せずにディスクに直接出力させるために、
パフォーマンスを犠牲にして OS のライトキャッシュが無効化されますので、
Flush(True) あるいは FlushFileBuffers の出番はありませんが、
それでも Flush(False) は必要となります。

毎回 Flush するのが面倒なら、StreamWriter の AutoFlush に頼るという手もあります。


> ランダムアクセス?での同時アクセス数に限界があるのかな?と素人推論しています。
> 感覚でいうと10台ぐらいで発生している気がします。

ネットワークや NAS 側の性能に依存する可能性はありそうですね。
10 万円ぐらいの NAS だと、上限 10 人ぐらいが相場でしょう。
https://www.itmedia.co.jp/pcuser/articles/1403/12/news024.html


==== 以下蛇足 ====

> プログラム@(抜粋)
>  Dim bufNow As Date = Now
>  Dim seekNo As Double = CDbl(Format$(bufNow, "HH")) * 3600 _
>                       + CDbl(Format$(bufNow, "mm")) * 60 + CDbl(Format$(bufNow, "ss"))
>  Dim Data As String = Format$(bufNow, "HHmmss") & "-" & 固定文字列 & vbCrLf
>  FS.Seek(DataLength * seekNo, SeekOrigin.Begin)

Seek メソッドの第一引数は Long を指定する必要がありますが、
上記では Double を渡しています。Option Strict On 時にエラーになりますね。

ということで、seekNo は
  Dim seekNo As Double = Int(bufNow.TimeOfDay.TotalSeconds())
あるいは、
  Dim seekNo As Long = CLng(Math.Floor(bufNow.TimeOfDay.TotalSeconds()))
に変更することを提案しておきます。


> 動作Aのプログラム(抜粋)
> Dim LineN As Integer = bufDate.Hour * 3600 + bufDate.Minute * 60 _
>                                     + bufDate.Second   '何行目?
こっちは Integer ですが、値の範囲的には問題無いはず。


それにしても、@とAでコーディングスタイルが随分違いますね。

@
> If today <> Format$(Now, "yyMMdd") Or today = "" Then
A
> If today <> bufDate.ToString("yyMMdd") Or today = "" Then

@は何となく VB6 時代の雰囲気を感じます。
まぁ、Aも OrElse が Or になってたりはしますが…。


> Private Sub CreateNewFile()
大晦日 24 時から正月 0 時に変わるタイミングで呼び出すと、
保存フォルダとファイル名の整合性が崩れる可能性があります。

Timer 処理で行っているように、Now を変数に渡してから使うことをお奨めします。


それと、提示コードだけ見た場合、例外処理が不十分に感じました。
(掲示板投稿のために端折っているせいもあるとは思いますが)

たとえば:
>    If Dir(filePath, FileAttribute.Directory) = "" Then MkDir(filePath)
の場合、filePath なファイルが存在し、かつそれが不可視属性だった場合、
上記の If 文は True となり、その後の MkDir によって IOException がスローされます。

それを受けるための Catch 句は用意されていますが、それによって得られる
lblERR.Text を見ただけでは、どのパスでどの例外クラスが投げられたのかが、
記録されていないので、障害発生時に苦労しそうです。
(エラー内容を画面に表示するかどうかは別として、障害記録は残せた方がベター)

[ツリー表示へ]
タイトル【解決】Re^4: 共有フォルダ上のあるファイルへ複数ユーザーからランダムアクセスすると読みだせなくなる
記事No11990
投稿日: 2019/07/11(Thu) 08:50
投稿者皆月
魔界の仮面弁士さん

色々アドバイスありがとうございます。
プログラム@のほうはなぜか.NET 4.0で開発してたので
7/10の朝一でFS.FLUSH(TRUE)を実装してみて1日経ちましたが今の所良好です。
様子見ます。

他のアドバイスは折を見て修正していこうと思います。
長々と色々知恵とアドバイスありがとうございました。一度コレで〆たいと思います。

※VBと.NETが混ざってる
.NET3.0が存在する時代にVB6とVBAで開発、VBのお勉強して最近までVB6でやっていたので
どうしてもVB6ライクに書いてしまうので頑張って.NET風に書こうとしていた時期のプログラムです。
未だにVBAは現役、VB6も保守で必要。時代的に.NETに移行しないとで頭がこんがらがっています。

[ツリー表示へ]