質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

2回答

1581閲覧

FileSystemWatcherでのイベント二重起動を防止しつつ、同時更新にも対応したい

yajiyaji

総合スコア3

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

1グッド

1クリップ

投稿2024/06/19 08:38

実現したいこと

使用言語:C#
開発環境:Visual Studio 2022

指定フォルダを監視し、対象フォルダ内にファイルが投入された場合に、それを入力としてバッチファイルを作成・起動するWindowsFormsアプリケーションを開発しています。バッチ起動後は、エラーコードを取得しています。
バッチファイルでは更に別のアプリケーションを起動しており、そちらがスレッドにより同時起動できるため、非同期処理をしたいと考えています。

発生している問題・分からないこと

FileSystemWatcherを使用すると、ファイルの新規作成や更新が一度だけでもイベントが複数回発生することがあります。それに対処するため、Reactive ExtensionsのThrottleにて指定秒数待機し、イベントをまとめてしまう方法をとっています。

この方法で1ファイルずつ投入されるパターンには対処できました。
しかし、複数のファイルが一度に投入された場合に、指定秒数以内にイベントがまとめて発生するためか、イベントが一つしか発生しません。

まとめると、以下のようなことを実現したいと思っています。
・FileSystemWatcherでのChangedイベント重複発生を回避したい
・ただし、複数ファイルがまとめて投入された(イベント発生は同時だがe.FullPathの値が異なっている)場合は、各ファイルで発生したイベントを全て実行させたい

該当のソースコード

C#

1using Microsoft.VisualBasic.ApplicationServices; 2using Microsoft.VisualBasic.Logging; 3using System.Diagnostics; 4using System.Formats.Tar; 5using System.Text; 6using System.IO; 7using static System.Net.Mime.MediaTypeNames; 8using static System.Runtime.CompilerServices.RuntimeHelpers; 9using System; 10using System.Reactive.Linq; 11 12namespace マルチスレッドSample { 13 14 public partial class Form1 : Form { 15 public Form1() { 16 InitializeComponent(); 17 } 18 19 private FileSystemWatcher watcher = null; 20 21 public string createBat(string inputFile) { 22 DateTime productionDT = DateTime.Now; 23 string thNo = "-" + Thread.CurrentThread.ManagedThreadId.ToString(); 24 25 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 26 27 //バッチ作成 28 string batFileName = "マルチスレッドsample_" + productionDT.ToString("yyyyMMddHHmmss") + thNo + ".bat"; 29 using(var sw = new StreamWriter(batFileName,false,Encoding.GetEncoding("shift_jis"))) { 30 sw.WriteLine("説明用サンプル"); 31 sw.WriteLine("@echo errorlevel:%errorlevel%"); 32 } 33 34 //ProcessStartInfoインスタンスを生成 35 ProcessStartInfo processInfo = new ProcessStartInfo(batFileName){ 36 CreateNoWindow = true, 37 RedirectStandardOutput = true, 38 UseShellExecute = false 39 }; 40 //バッチ実行 41 Process process = Process.Start(processInfo); 42 //バッチファイルの実行結果を変数outputに格納する 43 string output = process.StandardOutput.ReadToEnd(); 44 process.WaitForExit(); 45 46 //エラーレベルを取得するための配列 47 string[] arr = output.Split("errorlevel:"); 48 //呼び出し元にエラーコードを返す 49 return arr[1].Substring(0,1); 50 } 51 52 private void button1_Click(object sender,EventArgs e) { 53 watcher = new FileSystemWatcher(); 54 55 watcher.Path = @"C:\Users\UserName\test\Input"; 56 watcher.NotifyFilter = 57 NotifyFilters.FileName 58 | NotifyFilters.DirectoryName 59 | NotifyFilters.LastWrite 60 | NotifyFilters.LastAccess; 61 62 watcher.Filter = "*.csv"; 63 watcher.SynchronizingObject = this; 64 65 //Rx使用 66 watcher.ChangedAsObservable() 67 //1秒待機 68 .Throttle(TimeSpan.FromSeconds(1)) 69 .Subscribe(e => { 70 Task.Run(() => { 71 string rtnCode = createBat(e.FullPath); 72 }); 73 }); 74 75 //監視開始 76 watcher.EnableRaisingEvents = true; 77 } 78 } 79 static class FileSystemWatcherExtensions{ 80 //ChangedイベントをIObservable<TEventArgs>にする 81 public static IObservable<FileSystemEventArgs> ChangedAsObservable(this FileSystemWatcher self){ 82 return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( 83 h => (_, e) => h(e), 84 h => self.Changed += h, 85 h => self.Changed -= h); 86 } 87 } 88 }

試したこと・調べたこと

  • teratailやGoogle等で検索した
  • ソースコードを自分なりに変更した
  • 知人に聞いた
  • その他
上記の詳細・結果

自分では、一度createBatを実行した後にe.FullPathを変数に保存し、次回実行時にその変数とe.FullPathを比較する方法を考えました。両者が一致していなければ待機せずそのままcreateBatを実行し、一致している場合のみ待機するといった具合です。
しかし、(考えてみれば当然なのですが)、この方法ですとThrottleによる待機をしない場合はやはり重複してイベントが発生します。

あくまでもThrottleでの待機は必ず行い、その中で更新されたファイル名が変わった場合と重複している場合で、処理を分岐しないといけないのだと推測しています。

不足している情報がございましたら、お手数ですがご指摘ください。
非同期処理やRxの使い方に不慣れなため、基本的な内容であればご容赦いただけますと幸いです。

補足

特になし

TN8001👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

GroupByすればいいんじゃないですかね?
Reactive Extensions再入門 その20「GroupByメソッドでグルーピングしてみよう」 - かずきのBlog@hatena

FileSystemWatcherだと確認しにくいので雑なSubjectで^^;

cs

1using System.Diagnostics; 2using System.Reactive.Linq; 3using System.Reactive.Subjects; 4 5 6var sw = new Stopwatch(); 7 8var s = new Subject<int>(); 9s.GroupBy(i => i) // 実際はi.FullPathとかi.Nameとかに 10 .Subscribe(g => 11 { 12 g.Throttle(TimeSpan.FromSeconds(1)) 13 .Subscribe(i => Console.WriteLine($@"{sw.Elapsed:ss\.fff} {i} fire!")); 14 }); 15 16 17sw.Start(); 18 19foreach (var _ in Enumerable.Range(1, 3)) 20{ 21 foreach (var i in Enumerable.Range(1, 3)) 22 { 23 Console.WriteLine($@"{sw.Elapsed:ss\.fff} {i}"); 24 s.OnNext(i); 25 Thread.Sleep(200); 26 } 27} 28foreach (var _ in Enumerable.Range(1, 5)) 29{ 30 foreach (var i in Enumerable.Range(1, 2).Reverse()) 31 { 32 Console.WriteLine($@"{sw.Elapsed:ss\.fff} {i}"); 33 s.OnNext(i); 34 Thread.Sleep(200); 35 } 36} 37 38Console.ReadKey();
00.000 1 00.226 2 00.440 3 00.655 1 00.869 2 01.082 3 01.296 1 01.509 2 01.723 3 01.945 2 02.150 1 02.352 2 02.566 1 02.736 3 fire! 02.781 2 02.994 1 03.208 2 03.422 1 03.637 2 03.851 1 04.641 2 fire! 04.854 1 fire!

GroupByUntilの例もありましたが、厳密には意味が違うような気がします(実用上は大差ないかもしれないが)
c# - Rx grouped throttling - Stack Overflow

追記

yajiyajiさんのコードに合わせるとすると、こうですかね?(動作確認はしてません^^;

cs

1watcher.ChangedAsObservable() 2 .GroupBy(i => i.FullPath) // FullPathでグループ化 3 .Subscribe(g => 4 { 5 // ここは新たなグループ(FullPath)があったときに来る(Changedイベント毎に来るのではない) 6 Debug.WriteLine(g.Key); 7 8 // グループ毎にThrottle(1秒以上同じFullPathのイベントがこなければ最後のイベントを後続に流す) 9 g.Throttle(TimeSpan.FromSeconds(1)) 10 // 購読(つまりグループ毎に別々に購読してる) 11 .Subscribe(e => 12 { 13 // ここが目的のFullPath毎のThrottle結果 14 Task.Run(() => 15 { 16 string rtnCode = createBat(e.FullPath); 17 }); 18 }); 19 });

投稿2024/06/19 11:42

編集2024/06/20 07:36
TN8001

総合スコア10056

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yajiyaji

2024/06/20 05:56

ご回答ありがとうございます。 また、ご丁寧にコードと出力結果まで記載していただきましたこと、重ねてお礼申し上げます。 i.FullPathの値でグループ化できれば、確かに実現できそうに思います。 私がRxに不慣れなため、自身のケースへの応用にお時間をいただいてしまうと思います。 何かしら進展がございましたら、改めてコメントさせていただきます。
TN8001

2024/06/20 06:47

> 私がRxに不慣れなため、自身のケースへの応用にお時間をいただいてしまうと思います。 ああ、すいません。Subscribeがネストしてるんで分かりにくいですかね^^; 回答に追記しておきました。
yajiyaji

2024/06/20 07:20

ご回答いただいた内容で解決しました! ご丁寧に追記までしていただき、とても感謝しています。 分かりにくいなんてとんでもないです。催促してしまったようで恐縮です。 ご親切にありがとうございました。 ベストアンサーにさせていただきました。
guest

0

私が話をよくわかってない気がするのですが,

指定フォルダを監視し、対象フォルダ内にファイルが投入された場合に…

という話は,
「何らかの ( FileSystemWatcher ではない別の) 手段で指定フォルダ内のファイルを列挙する」ようなことができるとすれば,その手段でフォルダ内の状態をポーリングすることで実施することができるのではないでしょうか?

で,その「ポーリング」のタイミングというのを FileSystemWatcher のイベントから決めるとかすればどうなのでしょう?

指定秒数待機し、イベントをまとめてしまう

ではなくて,例えば
「指定秒数待機してから → その別手段で指定フォルダ内のファイルを列挙(して前回と比較)」
みたくすればどうでしょうか.

「指定秒数」の間は単にイベントは無視してしまえばそれで重複は回避できそうですし,複数のファイルが投入されたとしても(その秒数以内に投入が完了するならば,ですが)問題ないのではないでしょうか.

投稿2024/06/19 09:46

編集2024/06/19 09:47
fana

総合スコア12187

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

fana

2024/06/19 09:50

質問内容はそういう話ではない(FileSystemWatcherだけでやる方法の話をしている)ようにも思える. そのような場合にはガン無視していただきたく.
fana

2024/06/19 09:54

おっと,「ガン無視」だと,他の人に「違う,そうじゃない」旨が伝わらないので良くないかもですね. お手数ですが「そういう話をしてるんじゃねぇんだよ,素人はすっこんでろ」的な明言が必要かもです.
yajiyaji

2024/06/20 05:51

ご回答ありがとうございます。 FileSystemWatcherに限定しているわけではないので、大変参考になります。 別の手段で例えばリストにファイル名一覧を保存しておき、FileSystemWatcherのイベント発生時に差分を取得するということですよね。その方向性でも実現できそうに思います。 別の方からもご回答いただいており、両方を試すのにお時間をいただいてしまうと思います。 どういった方法を採用するにせよ、進展がございましたら改めてコメントいたします。
fana

2024/06/20 06:18

> 別の方からもご回答いただいており、両方を試すのにお時間をいただいてしまうと思います 私は「Rxって何? ガンダム?」みたいな素人なので,他回答の内容に関してはさっぱりわかりませんが, あなたから見て有望そうな話を試せばよいだけだと思いますよ. (「全部を実際に試す義務」みたいなのは無いハズなので,ある方法で問題が解決したならばそこで終了して良い)
yajiyaji

2024/06/20 07:24

寛大なお言葉、ありがとうございます。 今回は、TN8001様の方法で解決しましたのでご報告いたします。 FileSystemWatcherでファイル名を取得することに拘泥してしまっていたので、別の方法を検討するという視点はとても参考になりました。 ご親切なコメント、ありがとうございました。 (余談ですが、私もRxと聞くとガンダムを連想します)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問