TreeNode変更でTaskキャンセル への返答
投稿で使用できる特殊コードの説明。(別タブで開きます。)
以下の返答は逆順(新しい順)に並んでいます。
投稿者 るきお  (社会人)
投稿日時
2020/5/28 21:41:15
実行中のタスクと、タスクの外側で _cts を共有しているところに難しさがあって、キャンセル操作時に _cts の New が先に実行されてしますと、タスク内で _cts を参照するところで、タスク開始時の _cts とは違うものを参照することになってしまいます。
それで、Newするまでの間に少し猶予 Task.Delay(20).Wait() を入れてみました。
ご認識の通り時間で指定しているところが良くないところなんですよね。
試してはいませんが、タスク側で処理が終わった時点でフラグをたてて、フラグが立っているのを確認してから_cts を New するなど時間に依存しないロジックが書ければそちらの方が良いと思います。
とりあえず Task.Delay(20).Wait() の 20 を大きくするだけでも安定性は上がるので、この処理のクリティカル具合によってはこれでも妥協できるかもしれませんが・・・。
それで、Newするまでの間に少し猶予 Task.Delay(20).Wait() を入れてみました。
ご認識の通り時間で指定しているところが良くないところなんですよね。
試してはいませんが、タスク側で処理が終わった時点でフラグをたてて、フラグが立っているのを確認してから_cts を New するなど時間に依存しないロジックが書ければそちらの方が良いと思います。
とりあえず Task.Delay(20).Wait() の 20 を大きくするだけでも安定性は上がるので、この処理のクリティカル具合によってはこれでも妥協できるかもしれませんが・・・。
投稿者 mayopee  (社会人)
投稿日時
2020/5/28 21:33:37
るきお様、実験して戴き、ありがとうございます。
結果は提示してもらったコードで100回位、実行してみましたが、僕のより安定はしていますが、
「1」と「2」の列挙が並列実行される場面が2回程、発生しました。
ポイントは、「_cts.Cancelでキャンセルしても、メッセージキューに溜まっている前回の列挙情報が
処理される時間を待つ」ということだと思っているのですが........
るきお様も、Task.Delay(20).Wait()を入れておられますね。
僕の最初のコードでも SendKeys.SendWait("{ESC}")の後に、Await Task.Delay(1000) と入れていた
のですが、希望動作とならず断念しました。
あとはForループの中で、キャンセルされているか監視のため、Task.Delayを入れられていますね。
ここらが、ポイントだとすると、PC性能に依存し、環境により結果が異なるという話になりそうです。
待機時間を変更して、もう少し、追試してみます。
結果は提示してもらったコードで100回位、実行してみましたが、僕のより安定はしていますが、
「1」と「2」の列挙が並列実行される場面が2回程、発生しました。
ポイントは、「_cts.Cancelでキャンセルしても、メッセージキューに溜まっている前回の列挙情報が
処理される時間を待つ」ということだと思っているのですが........
るきお様も、Task.Delay(20).Wait()を入れておられますね。
僕の最初のコードでも SendKeys.SendWait("{ESC}")の後に、Await Task.Delay(1000) と入れていた
のですが、希望動作とならず断念しました。
あとはForループの中で、キャンセルされているか監視のため、Task.Delayを入れられていますね。
ここらが、ポイントだとすると、PC性能に依存し、環境により結果が異なるという話になりそうです。
待機時間を変更して、もう少し、追試してみます。
投稿者 るきお  (社会人)
投稿日時
2020/5/28 19:21:30
私の苦手なところなので満額回答ではありませんがご容赦を。
これで【希望動作】になっているように思いますが、確認できますか?
これで【希望動作】になっているように思いますが、確認できますか?
Imports System.Threading
Public Class Form4
Private _TV As New TreeView With {.Dock = DockStyle.Fill}
Private _LB As New ListBox With {.Dock = DockStyle.Fill}
Private _cts As CancellationTokenSource
Private Sub Form4_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Dim sp As New SplitContainer With {.Dock = DockStyle.Fill}
Dim root As New TreeNode("Task Cancel")
Dim tn1 As New TreeNode("1")
Dim tn2 As New TreeNode("2")
Dim tn3 As New TreeNode("ListBox Clear")
root.Nodes.AddRange({tn1, tn2, tn3})
_TV.Nodes.Add(root)
sp.Panel1.Controls.Add(_TV)
sp.Panel2.Controls.Add(_LB)
Controls.Add(sp)
root.Expand()
_TV.SelectedNode = root
AddHandler _TV.AfterSelect, AddressOf TreeView_AfterSelect
KeyPreview = True
AddHandler Me.KeyDown, Sub(o, k)
If k.KeyCode = Keys.Escape Then
_cts?.Cancel()
End If
End Sub
End Sub
Private Async Sub TreeView_AfterSelect(sender As Object, e As TreeViewEventArgs)
If _cts Is Nothing Then
_cts = New CancellationTokenSource
Else
_cts.Cancel()
Task.Delay(20).Wait()
_cts = New CancellationTokenSource
End If
Select Case e.Node.Text
Case "1"
Await Task.Run(Sub()
For Each item In Enumerable.Range(1, 10000)
Task.Delay(1).Wait()
If _cts.Token.IsCancellationRequested Then
Invoke(Sub() _LB.Items.Add("1 キャンセル ★★★★★★★★★★★★★★★★★★★★★★★★"))
Return
End If
SetText($"Node1({item})")
Next
End Sub, _cts.Token)
Case "2"
Await Task.Run(Sub()
'Try
For Each item In Enumerable.Range(1, 10000)
Task.Delay(1).Wait()
If _cts.Token.IsCancellationRequested Then
Invoke(Sub() _LB.Items.Add("2 キャンセル ★★★★★★★★★★★★★★★★★★★★★★★★"))
Return
End If
SetText($"Node2({item})")
Next
End Sub, _cts.Token)
Case "ListBox Clear"
_LB.Items.Clear()
End Select
End Sub
Private Sub SetText(s As String)
If InvokeRequired Then
Invoke(Sub() SetText(s))
Else
_LB.Items.Add(s)
End If
End Sub
End Class
投稿者 mayopee  (社会人)
投稿日時
2020/5/28 14:52:33
環境:VB2019,.Net4.7.2,WinForm
エクスプローラーのようなツールを作成しています。左ペインにフォルダTreeViewがあり、
TreeNode変更で選択フォルダ内のファイルに対して様々な処理を行います。
ファイル列挙時に途中キャンセルを実装したいのですが、希望動作になりません。
希望動作は「列挙途中でキャンセル指示がでれば列挙を中止して、新しい列挙を開始する」
としたいのです。
実際とは異なりますが、再現可能なコードを掲載するので、試して戴けないでしょうか?
EscキーでTaskキャンセルとしています。
↓のコードで 「1」=>「2」=>「1」=>「2」=>「Task Cancel」とNodeを素早くクリックした時、
以下の【NGパターン】となります。
【希望動作】
1の列挙開始=>1のキャンセル=>2の列挙開始=>2のキャンセル=>1の列挙開始=>.....
【NGパターン】
1の列挙開始=>2の列挙開始=>1のキャンセル=>2の列挙再開=>1の列挙開始=>.....
続けて、「ListBox Clear」を押して同じ操作をすると、2回目は希望動作になります。
メモリにキャッシュされる感じで動作も軽くなります。理由はわかりません。
又、Escキーの押下で以下のようにすると希望動作にはなります。
「1」=>「Escキー」=>「2」=>「Escキー」=>「1」=>「Escキー」=>「2」=>「Escキー」
TreeNodeの変更で上記【希望動作】とするには、どうすれば良いでしょうか?
TreeNodeの変更はTreeViewのAfterSelectをトリガとしています。
エクスプローラーのようなツールを作成しています。左ペインにフォルダTreeViewがあり、
TreeNode変更で選択フォルダ内のファイルに対して様々な処理を行います。
ファイル列挙時に途中キャンセルを実装したいのですが、希望動作になりません。
希望動作は「列挙途中でキャンセル指示がでれば列挙を中止して、新しい列挙を開始する」
としたいのです。
実際とは異なりますが、再現可能なコードを掲載するので、試して戴けないでしょうか?
EscキーでTaskキャンセルとしています。
↓のコードで 「1」=>「2」=>「1」=>「2」=>「Task Cancel」とNodeを素早くクリックした時、
以下の【NGパターン】となります。
【希望動作】
1の列挙開始=>1のキャンセル=>2の列挙開始=>2のキャンセル=>1の列挙開始=>.....
【NGパターン】
1の列挙開始=>2の列挙開始=>1のキャンセル=>2の列挙再開=>1の列挙開始=>.....
続けて、「ListBox Clear」を押して同じ操作をすると、2回目は希望動作になります。
メモリにキャッシュされる感じで動作も軽くなります。理由はわかりません。
又、Escキーの押下で以下のようにすると希望動作にはなります。
「1」=>「Escキー」=>「2」=>「Escキー」=>「1」=>「Escキー」=>「2」=>「Escキー」
TreeNodeの変更で上記【希望動作】とするには、どうすれば良いでしょうか?
TreeNodeの変更はTreeViewのAfterSelectをトリガとしています。
Imports System.Threading
Public Class Form1
Private _TV As New TreeView With {.Dock = DockStyle.Fill}
Private _LB As New ListBox With {.Dock = DockStyle.Fill}
Private _cts As CancellationTokenSource
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim sp As New SplitContainer With {.Dock = DockStyle.Fill}
Dim root As New TreeNode("Task Cancel")
Dim tn1 As New TreeNode("1")
Dim tn2 As New TreeNode("2")
Dim tn3 As New TreeNode("ListBox Clear")
root.Nodes.AddRange({tn1, tn2, tn3})
_TV.Nodes.Add(root)
sp.Panel1.Controls.Add(_TV)
sp.Panel2.Controls.Add(_LB)
Controls.Add(sp)
root.Expand()
_TV.SelectedNode = root
AddHandler _TV.AfterSelect, AddressOf TreeView_AfterSelect
KeyPreview = True
AddHandler Me.KeyDown, Sub(o, k)
If k.KeyCode = Keys.Escape Then
_cts?.Cancel()
End If
End Sub
End Sub
Private Async Sub TreeView_AfterSelect(sender As Object, e As TreeViewEventArgs)
Try
SendKeys.SendWait("{ESC}")
'Await Task.Delay(1000)
_cts = New CancellationTokenSource()
Select Case e.Node.Text
Case "1"
Await Task.Run(Sub()
Try
For Each item In Enumerable.Range(1, 10000)
_cts?.Token.ThrowIfCancellationRequested()
SetText($"Node1({item})")
Next
Catch ex As OperationCanceledException
Invoke(Sub() _LB.Items.Add("1 キャンセル ★★★★★★★★★★★★★★★★★★★★★★★★"))
End Try
End Sub)
Case "2"
Await Task.Run(Sub()
Try
For Each item In Enumerable.Range(1, 10000)
_cts?.Token.ThrowIfCancellationRequested()
SetText($"Node2({item})")
Next
Catch ex As OperationCanceledException
Invoke(Sub() _LB.Items.Add("2 キャンセル ★★★★★★★★★★★★★★★★★★★★★★★★"))
End Try
End Sub)
Case "ListBox Clear"
_LB.Items.Clear()
End Select
Finally
_cts?.Dispose()
_cts = Nothing
End Try
End Sub
Private Sub SetText(s As String)
If InvokeRequired Then
Invoke(Sub() SetText(s))
Else
_LB.Items.Add(s)
End If
End Sub
End Class
あれから、色々と待機時間を変えて試行錯誤したのですが、やはり時間で待機する方法では
失敗する場合があり、実務では使えないと判断しました。
るきお様のご指摘の通リ、フラグ管理にしてみたら、今のところ100%成功しています。
これで、当面運用したいと思います。色々、ご指導ありがとうございました。
テスト用に使ったコードをアップしておきます。