DOBON.NET DOBON.NETプログラミング掲示板過去ログ

大量ファイルコピーでエラー

環境/言語:[Vista、XP VisualStudio2008]
分類:[.NET]

お世話になっています。

Framework3.5 VB8の環境で、
FileStreamを使ってファイルのコピーを行っています。
合計で1.6GBぐらいまでは正常にコピーできるのですが、
ある程度の容量を超えると、
「GDI+ で汎用エラーが発生しました。」や
「メモリーが不足しています。」
が表示され止まってしまいます。

ソースは以下のような形で行っています。
Using inFile As New BinaryReader(New FileStream(strSouceFilePath, _
FileMode.Open, _
FileAccess.Read, _
FileShare.Read)), _
outFile As New BinaryWriter(New FileStream(strDestFilePath, _
FileMode.Create, _
FileAccess.Write, _
FileShare.Read))
Dim block() As Byte = inFile.ReadBytes(1024)
Do Until block.Length = 0
outFile.Write(block)
block = inFile.ReadBytes(glngCopyBlockSize)
Loop
inFile.Close()
outFile.Close()
End Using

回避する為の何かいい方法はありますでしょうか?
よろしくおねがいします。
> FileStreamを使ってファイルのコピーを行っています。
> 合計で1.6GBぐらいまでは正常にコピーできるのですが、
> ある程度の容量を超えると、
> 「GDI+ で汎用エラーが発生しました。」や
> 「メモリーが不足しています。」
> が表示され止まってしまいます。

  基本的なメモリリークですネ!
  MSDNのReadBytesの説明でサンプルコード見て下さい。

  因みに、何故わざわざバイト単位で読み込み・書き出ししている
  んですか?非効率だと思いますが・・・

  ファイルコピーのクラスはちゃんとありますが・・・
  http://dobon.net/vb/dotnet/file/filecopy.html

以上。参考まで・・・
ご回答ありがとうございます。

MemoryStreamでコピーするということなのでしょうか?
http://msdn.microsoft.com/ja-jp/library/system.io.binaryreader.readbytes.aspx
 サンプルの方を見てみましたが、ファイルのパスを指定して
読みだすものではなかったので良く把握することができませんでした。
 もしよろしければ、実際のファイルを指定するまでの
方法を教えて頂けないでしょうか?

 バイト単位での読み書きは、コピーしている際どこまでコピー
できたかプログレスバーに表示させる為に行っています。
他にもコピーで状況が分かるようないい方法が
ありますでしょうか?
> MemoryStreamでコピーするということなのでしょうか?

  提示したURLのページには、MemoryStreamでとは一切記述
  ないですが・・・

>  もしよろしければ、実際のファイルを指定するまでの
> 方法を教えて頂けないでしょうか?
>
>  バイト単位での読み書きは、コピーしている際どこまでコピー
> できたかプログレスバーに表示させる為に行っています。
> 他にもコピーで状況が分かるようないい方法が
> ありますでしょうか?

  そういう仕様に関わる話は、最初から!

  で、やっぱり記載URLのページの後半
  .NET Framework 2.0以降のVB.NETで、My.Computer.FileSystemを使用する方法
  と言うところで、お望みのようなことできますが・・・

以上。
オショウ様
アドバイスを頂きましてありがとうございます。

http://dobon.net/vb/dotnet/file/copyfolder.html
を参考にしてファイルのコピーソフトを作っている
ところなのですが、コピーの部分で
私も、My.Computer.FileSystemを検討してみましたが、
独自の画面に表示されているプログレスバーに
進行状況を表示する必要があり使うことができませんでした。

別の手段でも、独自に進行状況を取得することは可能でしょうか?
また、メモリリークとのことですが、どのように開放
するのがベストなのでしょうか?

すみませんが、よろしくおねがいします。
■No24700に返信(ららさんの記事)
> コピーの部分で
> 私も、My.Computer.FileSystemを検討してみましたが、
> 独自の画面に表示されているプログレスバーに
> 進行状況を表示する必要があり使うことができませんでした。
>

スレッドを作成し、コピー先のファイルを監視して一定間隔で
サイズを取得すればプログレスバーに進行状況を出せると思います。
やじゅ様、ご回答ありがとうございます。

スレッドを作成し、コピー先のファイルを監視して一定間隔で
FileInfoで確認してみましたが、返される値はコピー中でも
コピー元に指定したファイルサイズと同じものしか返ってきません
でした。

やはり、コピーの進行中に転送した値を取得する方法が
いいのではないでしょうか?
■No24705に返信(ららさんの記事)
> スレッドを作成し、コピー先のファイルを監視して一定間隔で
> FileInfoで確認してみましたが、返される値はコピー中でも
> コピー元に指定したファイルサイズと同じものしか返ってきません
> でした。

  FileInfo?ひとつづつファイルを見るんですか?
  DirectoryInfoかDirectoryクラスじゃ〜ないですか?

  複雑になりますが、
  http://dobon.net/vb/dotnet/file/filesystemwatcher.html

  で、監視して取得された情報から算出するとか・・・
  その方が結果的に楽かと。

※ 大量なファイルを一つ一つコピーするのを、いちいちオープン
  してくみ出して・・・非常に非効率だと思います。
  自身でバー出して・・・はよいのですが、犠牲にする部分と、
  効率化を天秤にかけて、より安全により高速に・・・と思うの
  ですが。

以上。
お世話になっています。

 オショウ様の言われるとおり、
より安全で高速な手法を使いたいのはやまやまなの
ですが、クライアントサイドからのご要望でこの仕様でないと
いけない状況なのです。
 速度については、LAN経由での使用を考慮して
コピー時にすべての帯域を使いきるのを避ける必要があるので
逆にスレッドスリープ等で処理時間を長く取るように
しています。
 話は戻りますが、メモリリークしないような
ファイル操作の方法はありますでしょうか?
> ですが、クライアントサイドからのご要望でこの仕様でないと
> いけない状況なのです。

  やりかたはいろいろあると思うのですが・・・

>  速度については、LAN経由での使用を考慮して
> コピー時にすべての帯域を使いきるのを避ける必要があるので
> 逆にスレッドスリープ等で処理時間を長く取るように
> しています。

  Windowsの場合、アプリが帯域を使いきるようなことは、よほど
  のことが無い限りできません。

  因みにLAN経由で先のPCの共有?フォルダに書き込む場合
  OSによっては書けたように見えて、かけていないトラブルが
  あります。

  まぁ〜そういうトラブルが発生してからでしょうか・・・対策
  は・・・

※ 私ならFTP使いますが・・・もしくはもらう側のPCから読
  み込む方向でコピーします。その方が安全!

>  話は戻りますが、メモリリークしないような
> ファイル操作の方法はありますでしょうか?

  PCの搭載メモリの空き容量が1本のファイルより明らかに
  大きい場合なら、MemoryStreamに1本分読み込んで、一気に
  に書きだす方向で速度は稼ぎます。

  ここを分割すると、ネットワークの不具合があった場合、よ
  り無茶苦茶になったりするんで・・・
  (ただし、無いと言うことにはなりません)

※ あと、メモリリークって・・・

> Dim block() As Byte = inFile.ReadBytes(1024)
> Do Until block.Length = 0
> outFile.Write(block)
> block = inFile.ReadBytes(glngCopyBlockSize)
> Loop  

  この部分しかありませんよネ?
  ReadBytesの仕様見れば・・・と言うより
  宣言部分の、Dim block() As Byte で配列の大きさ指定して
  いないのに、= inFile.ReadBytes(1024) で、1024バイト発生
  しますよね・・・
  で、それをDo 〜 Loop の中で回したら・・・

  コピーしたファイルの大きさ分、メモリをどんどん消耗して
  いくと言う風になっていると思いますが・・・

● コピーした元ファイルのフルパスを配列に代入しておいて
  配列数分、StreamReader => BinaryStream => MemoryStream =>
  StreamWriter => BinaryStream を繰り返して終わり・・・

  と言う構造は如何?

  私の場合、ファイル数は知れてましたが、巨大なファイルだった
  ので、MemoryStream => StreamWrite のところで圧縮工程を入れ
  て、コピーしてました。

以上。参考まで・・・
エラーと関係があるかは分かりませんが、このコードを見る限りでは、BinaryReaderとBinaryWriterを使う必要がないように思えます。FileStreamのReadとWriteメソッドで読み込みと書き込みを行う方法は試されましたか?
お世話になっています。

返事が遅くなってすみません。いろいろなパターンで
試していました。

管理人様ありがとうございます。
FileStreamのReadとWriteメソッドでも試してみましたが
同じように「メモリー不足です。」が表示されてしまいます。
 ちなみに、ハードは2Gのメモリーがありますが50%しか
使われていませんでした。

オショウ様
StreamReader => BinaryStream => MemoryStream =>
  StreamWriter => BinaryStream
の BinaryStream が勉強不足で分かりませんでした。
もう少し詳しく教えていただけると助かります。
> StreamReader => BinaryStream => MemoryStream =>
>   StreamWriter => BinaryStream
> の BinaryStream が勉強不足で分かりませんでした。
> もう少し詳しく教えていただけると助かります。

Using sr As StreamReader = New StreamReader(sz)
Dim br As BinaryReader = New BinaryReader(sr.BaseStream)
Using ms As MemoryStream = New MemoryStream(sr.BaseStream.Length)
Using sw As StreamWriter = New StreamWriter(szOutFile)
Dim bw As BinaryWriter = New BinaryWriter(sw.BaseStream)
ms.Write(br.ReadBytes(sr.BaseStream.Length), 0, sr.BaseStream.Length)
ms.Seek(0, SeekOrigin.Begin)
bw.Write(ms.GetBuffer)
sw.Close()
End Using
ms.Close()
End Using
sr.Close()
End Using

  今回行いたいやり方に適しているか解りませんので、適宜修正
  してご使用下さい。

※ 動作未確認です。そのままカキコしましたもので・・・
  う〜んメモリリークしているかな・・・

以上。
プログレスを表示する場合は、非同期で読み書きすると簡単になります。

フォームにボタンとプログレスバーを設定してから下記のソースを貼り付けてください。

4GByte程度のファイルで問題なくコピーできる事を確認しています。
エラー処理してないので、適度に行ってください。

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim fromInfo As New FileInfo("C:\hoge,dat")
    Dim toInfo As New FileInfo("C:\foo.dat")

    Dim bufferSize As Integer = 1048576

    Me.ProgressBar1.Value = 0
    Me.ProgressBar1.Maximum = fromInfo.Length / bufferSize

    Me.Button1.Enabled = False

    Dim status As New Status
    status.Buffer = New Byte(bufferSize) {}
    status.FromStream = fromInfo.OpenRead()
    status.ToStream = toInfo.Create()

    status.FromStream.BeginRead(status.Buffer, 0, status.Buffer.Length, AddressOf Me.ReadResult, status)
End Sub

Private Sub ReadResult(ByVal result As IAsyncResult)
    Dim status As Status = DirectCast(result.AsyncState, Status)
    Dim readCount As Integer = status.FromStream.EndRead(result)

    If (readCount <= 0) Then
        Me.Invoke(New Action(AddressOf Me.Completed))
        Exit Sub
    End If

    status.ToStream.BeginWrite(status.Buffer, 0, readCount, AddressOf Me.WriteResult, status)
End Sub

Private Sub WriteResult(ByVal result As IAsyncResult)
    Dim status As Status = DirectCast(result.AsyncState, Status)
    status.ToStream.EndWrite(result)

    Me.Invoke(New Action(AddressOf Me.AddProgress))

    Array.Clear(status.Buffer, 0, status.Buffer.Length)

    status.FromStream.BeginRead(status.Buffer, 0, status.Buffer.Length, AddressOf Me.ReadResult, status)
End Sub

Private Sub Completed()
    MessageBox.Show(Me, "完了")
    Me.Button1.Enabled = True
End Sub

Private Sub AddProgress()
    If (Me.ProgressBar1.Value < Me.ProgressBar1.Maximum) Then
        Me.ProgressBar1.Value = Me.ProgressBar1.Value + 1
    End If
End Sub

Public Structure Status
    Public FromStream As FileStream
    Public ToStream As FileStream
    Public Buffer() As Byte
End Structure
Algol様、大変ありがとうございます。
サンプルの方を試してみましたところ、
大量のファイルでも問題がでることなく
コピーすることができました。
同期処理でのコピーばかり見ておりましたが、
非同期のこの手法だと簡単に実現できるんですね。
とても参考になりました。

記事を投稿して頂いた、
みなさま大変ありがとうございました。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板