I have an application that trawls for files on a network and then uploads them to a cloud service. It's not the fastest thing so I started to wonder whether I should finally try to get my head around threading.
What I want to do is have one thread that crawls the network looking for files to process and adding them to a queue, and then a number of uploader threads that read the queue and if there's work to do, they pull the task off the queue.
The reason for having more than one thread handling the uploading is that this part of the process is slow and a whole batch is likely to take days or even weeks based on initial tests. At the moment and without any threading, I have routine that finds all the files to upload and then an uploader routine uploads them.
So I'm assuming that if I can somehow have a single queue/list that is accessible to all threads, the thread that adds files to the list can be doing that whilst multiple uploader threads are pulling them off the list and processing them individually. It sounds like it should be possible but I can't see how. BTW most of the 'Sleep' statements in this test code are purely there for test purposes.
I found this sample which was helpful: https://cjhaas.com/2009/06/25/creating-a-simple-multi-threaded-vb-net-application/
And I based my test project on that.
The code below is complete but you will need to create a form with a progress bar, button and the labels. It builds and works fine and includes 3 threads, one for adding content to the queue (dummy data at the moment), one that processes the queue (just has a timer for now) and one to monitor the progress and update a progress bar.
I'm pleased that I got this far as I'M A COMPLETE NOVICE, but I can't work out what I would need to do to this code to make it spawn more that one worker thread AND be able to share the same queue; I tried peeling the queue out as a separate class but there were issues getting the thread to communicate with the queue class (I failed to make a note of the problems otherwise I would have added them for completeness).
Are there some easy changes that could be made to this to allow me to have more than one uploading worker thread? Or is there a better example I should look to adapt for my needs?
Option Explicit On
Option Strict On
Imports System.Threading
Public Class Form1
Private MonitorThread As Thread
Private WorkerThread As Thread
Private QueueThread As Thread
Private W As Worker
Private Delegate Sub UpdateUIDelegate()
Private Delegate Sub WorkerDoneDelegate()
Private Sub Monitor()
Do While WorkerThread.ThreadState <> ThreadState.Stopped 'Loop until the Worker thread (and thus the Worker object's Start() method) is done
UpdateUI() 'Update the progress bar with the current value
Thread.Sleep(250) 'Sleep the monitor thread otherwise we'll be wasting CPU cycles updating the progress bar a million times a second
Loop
WorkerDone() 'If we're here, the worker object is done, call a method to do some cleanup
End Sub
Private Sub UpdateUI()
If Me.InvokeRequired Then 'See if we need to cross threads
Me.Invoke(New UpdateUIDelegate(AddressOf UpdateUI), New Object() {}) 'If so, have the UI thread call this method for us
Else
'Me.ProgressBar1.Value = curIndex 'Otherwise just update the progress bar
'Me.ProgressBar1.Value = W.CurRun 'Otherwise just update the progress bar
Me.ProgressBar1.Maximum = W.Qtotal
Me.ProgressBar1.Value = W.Qtotal - W.Remaining 'Otherwise just update the progress bar
Me.Lbl_CurrentRun.Text = (W.Qtotal - W.Remaining).ToString
Me.Lbl_Total.Text = "Total = " + W.Qtotal.ToString
Me.Lbl_Remaining.Text = "Remaining = " + W.Remaining.ToString
Me.Lbl_Date.Text = "Date = " + W.MSGdate.ToLongTimeString
Me.Lbl_Path.Text = "Message = " + W.MSGpath
End If
End Sub
Private Sub WorkerDone()
If Me.InvokeRequired Then 'See if we need to cross threads
Me.Invoke(New WorkerDoneDelegate(AddressOf WorkerDone)) 'If so, have the UI thread call this method for us
Else
Me.Button1.Enabled = True 'Otherwise just update the button
Me.ProgressBar1.Value = W.Qtotal - W.Remaining ' Update these to ensure they show the final data
Me.Lbl_CurrentRun.Text = (W.Qtotal - W.Remaining).ToString
Me.Lbl_Remaining.Text = "Remaining = " + W.Remaining.ToString
End If
End Sub
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Me.Button1.Enabled = False 'Disable the button
W = New Worker() 'Create our Worker object
QueueThread = New Thread(AddressOf W.PopulateQueue) 'Create a queue thread and tel it where to start when we call the start method
WorkerThread = New Thread(AddressOf W.Start) 'Create our Worker thread and tell it that when we start it it should call our Worker's Start() method
MonitorThread = New Thread(AddressOf Monitor) 'Create our Monitor thread and tell it that when we start it it should call this class's Monitor() method
QueueThread.Start() 'Start the queue thread which adds items to the queue
'Wait a while to allow some items to be added to the queue
System.Threading.Thread.Sleep(500)
WorkerThread.Start() 'Start the worker thread
MonitorThread.Start() 'Start the monitor thread which updates the progress bar
End Sub
End Class
Public Class Worker
Private iQtotal As Integer = 0 ' The total items processed
Private iRemaining As Integer ' Remaining items in the queue
Dim SortedList As New List(Of sMSG)()
Public ReadOnly Property Qtotal() As Integer
Get
Return Me.iQtotal
End Get
End Property
Public ReadOnly Property Remaining() As Integer
Get
'SortedList.TrimExcess()
Return SortedList.Count
End Get
End Property
Public ReadOnly Property MSGdate() As Date
Get
Return SortedList.FirstOrDefault.mDate
End Get
End Property
Public ReadOnly Property MSGpath() As String
Get
Return SortedList.FirstOrDefault.sFullpath
End Get
End Property
Public Sub PopulateQueue()
Dim tempMSG As New sMSG 'We will assemble the message in here before adding to the queue
tempMSG.sFullpath = "Path"
tempMSG.sLocation = "Location"
tempMSG.sLocationGUID = "GUID"
tempMSG.sLocationServerID = "666"
' This loop adds test data to the queue. It will be replaced with file system crawler
For i = 1 To 50
tempMSG.mDate = Date.Now
tempMSG.sFullpath = "Path " + i.ToString
SortedList.Add(tempMSG)
iQtotal = iQtotal + 1
Threading.Thread.Sleep(50)
SortList()
Next i
End Sub
Public Sub SortList()
SortedList = (From obj In SortedList Select obj Order By obj.mDate Descending).ToList()
End Sub
Public Sub New()
End Sub
Public Sub Start()
Dim nextMSG As sMSG
While SortedList.Count > 0
nextMSG = SortedList.FirstOrDefault 'Get the first item off the list
'' Do the upload of it...
System.Threading.Thread.Sleep(150)
SortedList.Remove(nextMSG) 'Remove it from the queue
End While
End Sub
End Class
Public Class sMSG
Public sFullpath As String
Public sLocation As String
Public sLocationGUID As String
Public sLocationServerID As String
Public mDate As Date
End Class
#######
So following the advice from Craig and djv I started to look at changing the code to use the ConcurrentBag object.
Unfortunately there is no way to remove items from the list/queue with the ConcurrentBag. Fortunately I came across the ConcurrentQueue and this works just fine.
The following code is not pretty but it works and in my tests I was able to see the performance improvements from the additional threads. It needs more work to perhaps manage how many threads are spawned but that's for later.
My thanks to by #Craig and #djv for trying to help and I hope this is of use to someone.
Option Explicit On
Option Strict On
Imports System.Threading
Imports System.Collections.Concurrent
Public Class Form1
Private MonitorThread As Thread
Private WorkerThread1 As Thread
Private WorkerThread2 As Thread
Private WorkerThread3 As Thread
Private QueueThread As Thread
Private Delegate Sub UpdateUIDelegate()
Private Delegate Sub BatchIsDOneDelegate()
Private tStart As Date
Private cq As New ConcurrentQueue(Of sMSG)
Public iQTotal As Integer
Private Sub Monitor()
Do While cq.Count > 0 'Loop until the queue is empty
UpdateUI() 'Update the progress bar with the current value
Thread.Sleep(250) 'Sleep the monitor thread otherwise we'll be wasting CPU cycles updating the progress bar a million times a second
Loop
BatchIsDOne() 'If we're here, the batch is done, call a method to do some cleanup
End Sub
Private Sub UpdateUI()
If Me.InvokeRequired Then 'See if we need to cross threads
Me.Invoke(New UpdateUIDelegate(AddressOf UpdateUI), New Object() {}) 'If so, have the UI thread call this method for us
Else
Me.ProgressBar1.Maximum = iQTotal
Me.ProgressBar1.Value = iQTotal - cq.Count 'Otherwise just update the progress bar
Me.Lbl_CurrentRun.Text = (iQTotal - cq.Count).ToString
Me.Lbl_Total.Text = "Total = " + iQTotal.ToString
Me.Lbl_Remaining.Text = "Remaining = " + cq.Count.ToString
Me.Lbl_Elapsed.Text = "Elapsed = " + (Date.Now - tStart).ToString
End If
End Sub
Private Sub BatchIsDOne()
If Me.InvokeRequired Then 'See if we need to cross threads
Me.Invoke(New BatchIsDOneDelegate(AddressOf BatchIsDOne)) 'If so, have the UI thread call this method for us
Else
Me.Button1.Enabled = True 'Otherwise just update the button
Me.ProgressBar1.Value = iQTotal - cq.Count ' Update these to ensure they show the final data
Me.Lbl_CurrentRun.Text = (iQTotal - cq.Count).ToString
Me.Lbl_Remaining.Text = "Remaining = " + cq.Count.ToString
End If
End Sub
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Me.Button1.Enabled = False 'Disable the button
tStart = Date.Now 'Note the start time
QueueThread = New Thread(AddressOf PopulateQueue) 'Create a queue thread and tel it where to start when we call the start method
WorkerThread1 = New Thread(AddressOf ProcessNextMSG) 'Create 1st Worker thread and tell it that when we start it it should call our Worker's Start() method
WorkerThread2 = New Thread(AddressOf ProcessNextMSG) 'Create 2nd Worker thread and tell it that when we start it it should call our Worker's Start() method
WorkerThread3 = New Thread(AddressOf ProcessNextMSG) 'Create 2nd Worker thread and tell it that when we start it it should call our Worker's Start() method
MonitorThread = New Thread(AddressOf Monitor) 'Create our Monitor thread and tell it that when we start it it should call this class's Monitor() method
QueueThread.Start() 'Start the queue thread which adds items to the queue
System.Threading.Thread.Sleep(500) 'Wait a while to allow some items to be added to the queue just for test purposes
WorkerThread1.Start() 'Start the worker thread
WorkerThread2.Start() 'Start the worker thread
WorkerThread3.Start() 'Start the worker thread
MonitorThread.Start() 'Start the monitor thread which updates the progress bar
End Sub
Public Sub PopulateQueue()
Dim tempMSG As New sMSG 'We will assemble the message in here before adding to the queue
tempMSG.sFullpath = "Path"
tempMSG.sLocation = "Location"
tempMSG.sLocationGUID = "GUID"
tempMSG.sLocationServerID = "666"
' This loop adds test data to the queue. It will be replaced with a file system crawler
For i = 1 To 50
tempMSG.mDate = Date.Now
tempMSG.sFullpath = "Path " + i.ToString
cq.Enqueue(tempMSG) ' Add the MSG to the queue
iQTotal = iQTotal + 1
Threading.Thread.Sleep(50)
Next i
End Sub
Public Sub ProcessNextMSG()
Dim nextMSG As New sMSG
' While the queue is not empty, take a message from the top and process it
While cq.IsEmpty = False
If cq.TryDequeue(nextMSG) = False Then
' It failed to get next message from queue
Else
'' Do the uploading of it...
System.Threading.Thread.Sleep(350) 'Just to mimmick the uploading for now
End If
End While
End Sub
End Class
Public Class sMSG
Public sFullpath As String
Public sLocation As String
Public sLocationGUID As String
Public sLocationServerID As String
Public mDate As Date
End Class
I found that a ConcurrentQueue was what I needed and have pasted the full working code into the thread above. Thanks to both #Craig and #djv for trying to help.
Related
When running my code I seem to encounter deadlocks while trying to update a GUI element from within one of the parallel tasks.
I've tried surrounding the Output function with "Synclock me" to try to ensure that only one task is trying to update the control at a time.
Private Sub RunParallel(records as list(of DataRecord), ou as String)
Dim ParallelOptions As New ParallelOptions
ParallelOptions.MaxDegreeOfParallelism = 10
Parallel.ForEach(records, ParallelOptions, Sub(myrecord)
ProcessRecord(myrecord, ou)
End Sub)
Output("Done...." & vbCrLf)
End Sub
Private Sub ProcessRecord(ByVal record As DataRecord, ByVal ou As String)
'Output($"BromcomID = {record("ID")}, Forename = {record("Forename")}{vbCrLf}")
Dim ud As New UserDetails With {
.EmployeeID = record("ID"),
.SamAccountName = record("SamAccountName"),
.GivenName = record("Forename"),
.Surname = record("Surname")
}
If Not CreateUser(ou, ud) Then
'Threading.Thread.Sleep(2000)
' Output($"Error creating {ud.EmployeeID}{vbCrLf}")
End If
End Sub
Private Sub Output(ByVal s As String)
SyncLock Me
If Me.InvokeRequired Then
Invoke(Sub()
Outbox.AppendText(s)
Outbox.SelectionStart = Len(Outbox.Text)
Outbox.ScrollToCaret()
Outbox.Select()
End Sub)
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Len(Outbox.Text)
Outbox.ScrollToCaret()
Outbox.Select()
End If
End SyncLock
End Sub
The code as supplied seems to run, but if I uncomment out the Output calls in the ProcessRecord() function, it hangs and never gets exits the Parallel.foreach
--- Update
After playing around with suggestions and comments on here I still can't get it to work correctly.
If I take out all of the output from ProcessRecord it seems to work correctly. However with the following code, it now seems to run each ProcessRecord sequentially (not 10 at a time as I intended), and then hangs after the last one.
Output("Dispatching" & vbCrLf)
Dim ParallelOptions As New ParallelOptions With {
.MaxDegreeOfParallelism = 10
}
Parallel.ForEach(recordList, ParallelOptions, Sub(myrecord)
ProcessRecord(myrecord, ou)
End Sub)
'For Each myrecord As DataRecord In recordList
' Task.Factory.StartNew(Sub() ProcessRecord(myrecord, ou))
'Next
Output("Done...." & vbCrLf)
End Sub
Private Sub ProcessRecord(ByVal record As DataRecord, ByVal ou As String)
Dim ud As New UserDetails With {
.EmployeeID = record("ID"),
.SamAccountName = record("SamAccountName"),
.GivenName = record("Forename"),
.Surname = record("Surname"),
.DisplayName = $"{record("Forename")} {record("Surname")} (Student)"}
If Not CreateUser(ou, ud) Then
' Output($"Error creating {ud.EmployeeID}{vbCrLf}")
End If
Output($"BromcomID = {record("ID")}, Forename = {record("Forename")}{vbCrLf}")
End Sub
Private Sub Output(ByVal s As String)
If Me.InvokeRequired Then
Invoke(Sub()
Output(s)
End Sub)
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Outbox.TextLength
Outbox.ScrollToCaret()
Outbox.Select()
Outbox.Refresh()
End If
End Sub
If I use the commented out Task.Factory code everything seems to work perfectly, except I cant control how many tasks at a time are launched, and I can't wait till all of them have finished, the for loop just launches all the tasks, and then carries on with the Output("Done....) line.
The synclock statements didn't seem to affect anything either way.
Give this a try
Private Sub Output(ByVal s As String)
If Me.InvokeRequired Then
Me.Invoke(Sub() Output(s))
'Me.BeginInvoke(Sub() Output(s))
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Outbox.TextLength
Outbox.ScrollToCaret()
Outbox.Select()
Outbox.Refresh()
End If
End Sub
There may be an issue if you have events tied to Outbox, like text changed. Tested Output method with
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim nums As New List(Of Integer)
For x As Integer = 1 To 500
nums.Add(x)
Next
'because it is in a button, run from a task
Dim t As Task
t = Task.Run(Sub()
Parallel.ForEach(nums, Sub(num)
Output(num.ToString & Environment.NewLine)
End Sub)
End Sub)
End Sub
If you want to go ahead with using a Task-based approach, then you certainly can control how many are launched at a time, and wait for all of them to finish. It requires some additional code for the manual management. This is discussed in some detail in Microsoft documentation: https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern
It's not necessarily a bad thing to initiate all of the tasks immediately, then you'll be leaving it to the thread pool to take care of how many to run at a time.
If you want greater control, you can use the "throttling" design from the link. In your "pending" queue, store delegates/lambdas that will themselves kick off Task.Run. Then, as you dequeue from the "pending" queue into the "active" list, you can Invoke on the delegate/lambda to get the Task and Await Task.WhenAny on the "active" list.
One potential benefit of doing things this way is that the work in each top-level Task can be split between UI work running on the UI thread and processor-limited work running on the thread pool.
(I'm not suggesting that this is necessarily the best option for you, just trying to expand on what you should be looking at doing if you really want to pursue using Task instead of Parallel.)
I can get a progress bar to increment inside the Do loop but it massively affects the speed of the loop. How can i keep track of progress of the Do Until loop without affecting it?
Do Until temp = pbTemp2
temp+= 1
progressbar1.increment(1) '<--- i dont want this in here but still need to track the progress
loop
The middle ground between what Visual Vincent proposed and a BackgroudWorker.
(I'm assuming this is related to .NET WinForms).
Crate a Thread to perform some work, and use a SynchronizationContext to queue the results the process to the UI context.
The SynchronizationContext will then dispatch an asynchronous message to the synchronization context through a SendOrPostCallback delegate, a method that will perform its elaboration in that context. The UI thread in this case.
The asynchronous message is sent using SynchronizationContext.Post method.
The UI thread will not freeze and can be used to perform other tasks in the meanwhile.
How this works:
- Define a method which will interact with some UI control:
SyncCallback = New SendOrPostCallback(AddressOf Me.UpdateProgress)
- Initialize a new Thread, specifying the worker method that the thread will use.
Dim pThread As New Thread(AddressOf Progress)
- Starting the thread, it's possible to pass a parameter to the worker method, in this case is the maximum value of a progress bar.
pThread.Start(MaxValue)
- When the worker method needs to report back its progress to the (UI) context, this is done using the asychronous Post() method of the SynchronizationContext.
SyncContext.Post(SyncCallback, temp)
Here, the thread is started using a Button.Click event, but it could
be anything else.
Imports System.Threading
Private SyncContext As SynchronizationContext
Private SyncCallback As SendOrPostCallback
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button11.Click
SyncContext = WindowsFormsSynchronizationContext.Current
SyncCallback = New SendOrPostCallback(AddressOf Me.UpdateProgress)
Dim MaxValue As Integer = ProgressBar1.Maximum
Dim pThread As New Thread(AddressOf Progress)
pThread.IsBackground = True
pThread.Start(MaxValue)
End Sub
Private Sub UpdateProgress(state As Object)
ProgressBar1.Value = CInt(state)
End Sub
Private Sub Progress(parameter As Object)
Dim temp As Integer = 0
Dim MaxValue As Integer = CType(parameter, Integer)
Do
temp += 1
SyncContext.Post(SyncCallback, temp)
Loop While temp < MaxValue
End Sub
Have some main form on which I am calling file downloading from FTP. When this operation is raised i want to see new form as ShowDialog and progress bar on it to be shown meantime, then show the progress and close new form and back to main form. My code is working however, when it will process is started my main form freezes and after while new form is appearing and then closing. What I would like to correct is to show this new form to be showed straightaway after process is executed. Can you take a look and tell me whats wrong?
This is out of my main form the download process called:
Dim pro As New FrmProgressBarWinscp(WinScp, myremotePicturePath, ladujZdjeciaPath, True)
FrmProgressBarWinscp is as follows:
Public Class FrmProgressBarWinscp
Property _winScp As WinScpOperation
Property _remotePicture As String
Property _ladujZdjecia As String
Property _removesource As String
Public Sub New()
InitializeComponent()
End Sub
Sub New(winscp As WinScpOperation, remotePicture As String, ladujzdjecia As String, removesource As Boolean)
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
_winScp = winscp
_remotePicture = remotePicture
_ladujZdjecia = ladujzdjecia
_removesource = removesource
ShowDialog()
End Sub
Sub Run()
Try
Cursor = Cursors.WaitCursor
_winScp.GetFile(_remotePicture, _ladujZdjecia, _removesource)
ProgressBar1.Minimum = 0
ProgressBar1.Maximum = 1
ProgressBar1.Value = 0
Do
ProgressBar1.Value = WinScpOperation._lastProgress
ProgressBar1.Refresh()
Loop Until ProgressBar1.Value = 1
Cursor = Cursors.Default
'Close()
Catch ex As Exception
Finally
If _winScp IsNot Nothing Then
_winScp.SessionDispose()
End If
System.Threading.Thread.Sleep(10000)
Close()
End Try
End Sub
Private Sub FrmProgressBarWinscp_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Run()
End Sub
End Class
Winscp my own class and used methods:
...
Function GetFile(source As String, destination As String, Optional removeSource As Boolean = False)
Dim result As Boolean = True
Try
session.GetFiles(source, destination, removeSource).Check()
Catch ex As Exception
result = False
End Try
Return result
End Function
Private Shared Sub SessionFileTransferProgress(sender As Object, e As FileTransferProgressEventArgs)
'Print transfer progress
_lastProgress = e.FileProgress
End Sub
Public Shared _lastProgress As Integer
...
Further discussion nr 3:
Main form:
Dim tsk As Task(Of Boolean) = Task.Factory.StartNew(Of Boolean)(Function()
Return WinScp.GetFile(myremotePicturePath, ladujZdjeciaPath, True)
End Function)
Dim forma As New FrmProgressBar
forma.ShowDialog()
Progress bar form:
Public Class FrmProgressBar
Public Sub New()
InitializeComponent()
End Sub
Sub Run()
Try
Do
ProgressBar1.Value = WinScpOperation._lastProgress
ProgressBar1.Refresh()
Loop Until ProgressBar1.Value = 1
Cursor = Cursors.Default
Catch ex As Exception
Finally
MsgBox("before sleep")
System.Threading.Thread.Sleep(10000)
MsgBox("after sleep sleep")
Close()
End Try
End Sub
Private Sub FrmProgressBarWinscp_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Run()
End Sub
End Class
Point nr. 4:
Dim tsk As Task(Of Boolean) = Task.Factory.StartNew(Of Boolean)(Function()
Return WinScp.GetFile(myremotePicturePath, ladujZdjeciaPath, True)
End Function)
Dim pic As New Waiting
pic.ShowDialog()
Task.WaitAll(tsk)
pic.Close()
Point 5:
Dim pic As New Waiting
pic.ShowDialog()
Dim tsk As Task = Task.Factory.StartNew(Sub() WinScp.GetFile(myremotePicturePath, ladujZdjeciaPath, pic, True))
Task.WaitAll(tsk)
'pic.Close()
In some other class (maybe didn't mentioned before this method is placed in diffrent class - my custom one)
Public Function GetFile(source As String, destination As String, formclose As InvokeCloseForm, Optional removeSource As Boolean = False) As Boolean
Dim result As Boolean = True
Try
session.GetFiles(source, destination, removeSource).Check()
Catch ex As Exception
result = False
End Try
formclose.RUn()
Return result
End Function
Interface:
Public Interface InvokeCloseForm
Sub RUn()
End Interface
Waiting form :
Public Class Waiting
Implements InvokeCloseForm
Public Sub RUn() Implements InvokeCloseForm.RUn
Me.Close()
End Sub
End Class
The Session.GetFiles method in blocking.
It means it returns only after the transfer finishes.
The solution is to:
Run the WinSCP transfer (the Session.GetFiles) in a separate thread, not to block the GUI thread.
For that see WinForm Application UI Hangs during Long-Running Operation
Handle the Session.FileTransferProgress event.
Though note that the event handler will be called on the background thread, so you cannot update the progress bar directly from the handler. You have to use the Control.Invoke to make sure the progress bar is updated on the GUI thread.
For that see How do I update the GUI from another thread?
A trivial implementation is below.
For a more version of the code, see WinSCP article Displaying FTP/SFTP transfer progress on WinForms ProgressBar.
Public Class ProgressDialog1
Private Sub ProgressDialog1_Load(
sender As Object, e As EventArgs) Handles MyBase.Load
' Run download on a separate thread
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf Download))
End Sub
Private Sub Download(stateInfo As Object)
' Setup session options
Dim mySessionOptions As New SessionOptions
With mySessionOptions
...
End With
Using mySession As Session = New Session
AddHandler mySession.FileTransferProgress,
AddressOf SessionFileTransferProgress
' Connect
mySession.Open(mySessionOptions)
mySession.GetFiles(<Source>, <Destination>).Check()
End Using
' Close form (invoked on GUI thread)
Invoke(New Action(Sub() Close()))
End Sub
Private Sub SessionFileTransferProgress(
sender As Object, e As FileTransferProgressEventArgs)
' Update progress bar (on GUI thread)
ProgressBar1.Invoke(
New Action(Of Double)(AddressOf UpdateProgress), e.OverallProgress)
End Sub
Private Sub UpdateProgress(progress As Double)
ProgressBar1.Value = progress * 100
End Sub
End Class
You may want to disable the progress form (or its parts) during the operation, if you want to prevent the user from doing some operations.
Use the .Enabled property of the form or control(s).
Easier, but hacky and generally not recommendable solution, is to call the Application.DoEvents method from your existing SessionFileTransferProgress handler.
And of course, you have to update the progress bar from the the SessionFileTransferProgress as well.
Private Shared Sub SessionFileTransferProgress(
sender As Object, e As FileTransferProgressEventArgs)
'Print transfer progress
ProgressBar1.Value = e.FileProgress
Application.DoEvents
End Sub
And the progress bar's .Minimum and .Maximum must be set before the Session.GetFiles.
But do not do that! That's a wrong approach.
And still, you need to disable the forms/controls the same way as in the correct solution above.
I have a thread that runs background jobs and is required to update the GUI once in a while. My program has been designed so that when the user clicks off of a form, the thread and background operations still run, yet the controls have been disposed (for memory management purposes).
I have been using Invoke() and "If Control.Created = True" to make sure that the thread can successfully update the controls without running into any exceptions. However, when the form is recreated, all "Control.Created" values are false and Invoke() fails with "{"Invoke or BeginInvoke cannot be called on a control until the window handle has been created."}"
My guess is that this has something to do with the fact that when the form is recreated it is assigned different handles and that the "Invoke()" is looking at the old handle. SO my question is, how do I fix this?
EDIT: As per requested, the code for opening the form and where the bg thread works from
Opening the DropLogMDIalt form is simply
FormCTRL.Show()
The Background Thread runs when the control is modified so that the NumericUpDown is more than 0 (so that there is something to countdown from)
Private Sub NLauncherTerminateInput_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles DLScanInterval.ValueChanged
If DLScanInterval.Created = True Then
DLTimerControlValue = DLScanInterval.Value
If DLTimerControlValue = 0 Then
CancelDropLogTimer()
Else
If DLScanIntervalControl.Active = False Then
BeginDropLogTimer()
End If
End If
End If
End Sub
Public Sub BeginDropLogTimer()
Dim N As New Threading.Thread(AddressOf DropLogTimerIntervalThreadWorker)
N.Start()
DLScanIntervalControl.ThreadID = N.ManagedThreadId
DLScanIntervalControl.Active = True
End Sub
Public Sub CancelDropLogTimer()
DLScanIntervalControl.Active = False
End Sub
Public Sub DropLogTimerIntervalThreadWorker()
DLScanTimerSecondsLeft = DLTimerControlValue * 60
Dim s As Integer = DLTimerControlValue
Do Until 1 = 2
DLScanTimerSecondsLeft = DLTimerControlValue * 60
Do Until DLScanTimerSecondsLeft <= 0
If Not (DLTimerControlValue = 0 Or DLScanIntervalControl.CancelPending = True) Then
Else
Exit Sub
End If
If Not DLTimerControlValue = s Then
DLScanTimerSecondsLeft = DLTimerControlValue * 60
s = DLTimerControlValue
End If
Dim ToInvoke As New MethodInvoker(Sub()
Timer(DLScanTimerSecondsLeft, ":", DLScanIntervalTB)
End Sub)
If (Me.IsHandleCreated) Then
If (InvokeRequired) Then
Invoke(ToInvoke)
Else
ToInvoke()
End If
End If
Threading.Thread.Sleep(1000)
DLScanTimerSecondsLeft -= 1
Loop
CompareScan = True
PerformScan()
Loop
End Sub
The thread is simply called by declaring a new thread.thread, however, I have created a class and a variable that the thread uses to check if it should still be running or not (similarly to how a backgroundworker would) this is illustrated by the "DLScanIntervalControl.CancelPending"
The form is then closed later by
Form.Close()
It can be reopened if the user clicks on a label that then uses the same method as shown above (FormCTRL.Show())
From MSDN:
"If the control's handle does not yet exist, InvokeRequired searches up the control's parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, the InvokeRequired method returns false."
In other words, you need to verify that the handle is created and then check if invoke is required.
If(Me.IsHandleCreated) Then
If(Me.InvokeRequired) Then
'...
Else
'...
End If
End If
I had a similar error when trying to use delegates to update controls on a form in another thread. I found that the handle is only created when it's "needed". I'm not sure what constitutes "needed", but you can force it to create the handle by accessing the Handle property of the object.
What I had done in my application is this:
' Iterate through each control on the form, and if the handle isn't created yet, call the
' Handle property to force it to be created
For Each ctrl As Control In Me.Controls
While Not ctrl.IsHandleCreated
Dim tmp = ctrl.Handle
tmp = Nothing
End While ' Not ctrl.IsHandleCreated
Next ' ctrl As Control In Me.Controls
It's rather ghetto, but it may help you here (If you still need the help)
I think the issue here has nothing to do with invoking, but references. Following this pseudocode...
Dim A As New Form1
A.Show()
''Spawn background thread with a reference to A
A.Dispose()
Dim B As New Form1
B.Show()
The thread is attempting to refer to the first instance of Form1 above which is disposed and will always stay that way.
If you want the thread to be able to update any form then you need to give the thread a (synchronised) way to refer to the form...
Public Class Worker
Private Target As Form1
Private TargetLock As New Object
Public Sub SetTargetForm(Frm as Form1)
SyncLock TargetLock
Target = Frm
End SyncLock
End Sub
Public Sub DoWork() ''The worker thread method
''Do work as usual then...
SyncLock TargetLock
If Target IsNot Nothing AndAlso Target.IsHandleCreated Then
If Target.InvokeRequired
Target.Invoke(...)
Else
...
End If
End If
End SyncLock
End Sub
End Class
This way, when a new form is available, you can inform the worker thread using SetTargetForm() and it will update the appropriate one.
Of course, you'd be better off refactoring the "Update UI" checks and invoke calls into a different method for simplicity/maintainability but you get the point.
Note that I haven't got an IDE to hand so there may be typos.
One final point... I'd question the value of disposing a form for memory management purposes. Controls are fairly lightweight in terms of memory and it's far more common for an object used by the form to be a memory hog than the form itself. Are you sure you're getting a real benefit for this added complexity?
I'm trying to create a class that will create a Relogin after certain time but after the first Relogin it keeps populating. Heres my Code:
Private Shared timer As Timer
Public Shared Event DoSomething As Action(Of Integer)
Private Shared _timesCalled As Integer = 0
Public Shared Sub Start()
AddHandler DoSomething, AddressOf EventHandler
timer = New System.Threading.Timer(AddressOf timer_Task, Nothing, 0, 1000)
End Sub
Public Shared Sub [Stop]()
timer.Dispose()
End Sub
Private Shared Sub timer_Task(State As Object)
_timesCalled += 1
If _timesCalled = 15 Then 'Should Raise event every 15s
RaiseEvent DoSomething(_timesCalled)
End If
End Sub
Private Shared Sub EventHandler(ByVal EventNumber As Integer)
My.Application.Dispatcher.Invoke(New Action(AddressOf OpenLogin))
End Sub
Private Shared Sub OpenLogin() 'This event fires multiple times after the first Event
Dim x As New MainWindow
x.ShowDialog() 'Dialog stops code from continuing.
x = Nothing
_timesCalled = 0
End Sub
Open_Login() fires multiple times after the first or second time. Doesn't seem to cause the same problem when I replace "MainWindow" object with a messagebox. Please Help. Thank you.
Notwithstanding the fact that your issue seems to be solved - using an unsynchronised counter is not a reliable way to have an event fired every predetermined period.
The timer event itself fires from a separate .NET managed thread and subsequently, the _timesCalled variable can be accessed from multiple threads. So it is possible that while you are re-setting _timesCalled=0 from your main thread another thread from the default threadpool is about to overwrite this with _timesCalled=14.
In your specific example it is simpler and more straightforward to reschedule the timer event after you’ve finished handling one. That way you can also account for the time it took you to process the event and the timer inaccuracies and lag.
Public Shared Sub Start()
...
' assuming this runs only once
timer = New System.Threading.Timer(AddressOf timer_Task, Nothing, 15000, Timeout.Infinite)
End Sub
Private Shared Sub timer_Task(State As Object)
RaiseEvent DoSomething(_timesCalled)
End Sub
Private Shared Sub OpenLogin()
Dim x As New MainWindow
x.ShowDialog()
x = Nothing
' Reschedule the timer again here, adjust the 15000 if necessary, maybe prefer timer.ChangeTime(...) instead
End Sub
Figured out it was my coding. Everytime MainWindow would load it would run Start() creating a new instance of Timer. Correct issue. Thanks for viewing