VB.Net threading advice - vb.net

I have datagrid with some data and I'm making a loop through the items with for... next
Inside the loop i'm making a call to a web service with parameters from the datagrid
Because of the speed the work is very time consuming and I want to give the user the option to select how many multiple calls to the service he want.
How can I make simultaneously calls to the web service inside the loop?

There are many ways to achieve what you want so here's an example using the Task class. The key point is to iterate the underlying data source (read only) inside the background thread. When finished, move back to the UI thread and update.
Private Sub BeginAsyncOp(list As IList)
Static cachedSource As CancellationTokenSource
Static cachedId As Long
If ((Not cachedSource Is Nothing) AndAlso (Not cachedSource.IsCancellationRequested)) Then
cachedSource.Cancel()
End If
cachedSource = New CancellationTokenSource
cachedId += 1L
Dim token As CancellationToken = cachedSource.Token
Dim id As Integer = cachedId
Task.Factory.StartNew(
Sub()
Dim result As IList = Nothing
Dim [error] As Exception = Nothing
Dim cancelled As Boolean = False
Try
'Background thread, do not make any UI calls.
For Each item In list
'...
token.ThrowIfCancellationRequested(True)
Next
result = a_updated_list
Catch ex As OperationCanceledException
cancelled = True
Catch ex As ObjectDisposedException
cancelled = True
Catch ex As Exception
[error] = ex
Finally
If (id = cachedId) Then
Me.Invoke(
Sub()
If (((Not Me.IsDisposed) AndAlso (Not Me.Disposing))) Then
'UI thread.
If (Not [error] Is Nothing) Then
'...
ElseIf (Not cancelled) Then
For Each item In result
'...
Next
End If
End If
End Sub)
End If
End Try
End Sub)
End Sub

Related

Task function within form and error messages

I have a form whose role is to show the user a circular progress graphic while the user is waiting on particular stuff to be done. This is the simple code of it:
Public Class FrmCircularProgress
Sub New(progressType As DevComponents.DotNetBar.eCircularProgressType)
InitializeComponent()
CircularProgress1.ProgressBarType = progressType
StartCircular()
End Sub
Public Sub StartCircular()
Me.CircularProgress1.IsRunning = True
End Sub
Public Sub StopCircular()
Me.CircularProgress1.IsRunning = False
End Sub
End Class
Below is an example of how I use it (in this case two places)
Dim createArticle As New Artikel
'http://stackoverflow.com/questions/33030706/put-long-running-method-into-task-showing-new-form-meantime-and-closing-it-once
Dim pic As New FrmCircularProgress(eCircularProgressType.Donut)
Dim tsk As Task(Of Boolean) = Task.Factory.StartNew(Of Boolean)(Function()
'--Run lenghty task
Dim resu = False
Try
resu = createArticle.ProcessArticle(_artikelsAndTheirVariationsFinal)
'--Close form once done (on GUI thread)
Catch sqlex As Exception
pic.Invoke(Sub() MessageBox.Show(pic, sqlex.Message))
' pic.Invoke(Sub() MessageBox.Show(pic, ex.Message))
'pic.Invoke(Sub() TaskDialog.Show(pic, New TaskDialogInfo("Information", eTaskDialogIcon.BlueStop, "WizardPageDescriptionUberblick_BeforePageDisplayed", ex.ToString, eTaskDialogButton.Ok, eTaskDialogBackgroundColor.Blue, Nothing, Nothing, Nothing, "Jakis footer text", Nothing)))
Finally
End Try
pic.Invoke(New Action(Sub() pic.StopCircular()))
pic.Invoke(New Action(Sub() pic.Close()))
Return resu
End Function)
'--Show the form
pic.ShowDialog()
Task.WaitAll(tsk)
If tsk.Result = True Then
TaskDialog.Show(New TaskDialogInfo("Information", eTaskDialogIcon.BlueStop, "Infor", "New articel and every data has been added correctly", eTaskDialogButton.Ok, eTaskDialogBackgroundColor.Blue, Nothing, Nothing, Nothing, "Jakis footer text", Nothing))
'http://stackoverflow.com/questions/33030706/put-long-running-method-into-task-showing-new-form-meantime-and-closing-it-once
pic = New FrmCircularProgress(eCircularProgressType.Line)
Dim work As Task = Task.Factory.StartNew(Sub()
'--Run lenghty task
PrepareUberblick()
'--Close form once done (on GUI thread)
pic.Invoke(New Action(Sub() pic.StopCircular()))
pic.Invoke(New Action(Sub() pic.Close()))
End Sub)
'--Show the form
pic.ShowDialog()
Task.WaitAll(work)
If WYSWIG_Uberblick.Document IsNot Nothing Then
WYSWIG_Uberblick.Document.Write(String.Empty)
End If
'--Pobranie wszystkich html'ow wszystkich podsekcji artykulow (w tym wypadku numerów artykułów jako podsekcji) (dla sekcji Uberblick)
WYSWIG_Uberblick.DocumentText = _htmlFactory.GetAllUberblickHTML
Else
TaskDialog.Show(New TaskDialogInfo("Information", eTaskDialogIcon.NoEntry, "Infor", "Critical error occured", eTaskDialogButton.Ok, eTaskDialogBackgroundColor.Blue, Nothing, Nothing, Nothing, "Jakis footer text", Nothing))
e.Cancel = True
End If
ProcessArticle function:
Public Function ProcessArticle(artikel As ArticlesVariations) As Boolean
Dim result = True
Dim strcon = New AppSettingsReader().GetValue("ConnectionString", GetType(System.String)).ToString()
Using connection As New SqlConnection(strcon)
'-- Open generall connection for all the queries
connection.Open()
'-- Make the transaction.
Dim transaction As SqlTransaction
transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted)
Dim newArticleRowId As Integer = 0
Dim articleIndex As Integer = 0
Try
For Each kvp As KeyValuePair(Of Integer, Artikel) In artikel.collection
Dim ckey As Integer = kvp.Key
articleIndex = kvp.Key 'save article key
Dim data As Artikel = kvp.Value
'-- If given article contains images list (artikel_images is a list with pictures associated with article)
If Not IsNothing(artikel.collection(articleIndex).ArtikelImages) Then
For Each img In artikel.collection(articleIndex).ArtikelImages
'--Insert article's images if exists
Using cmd As New SqlCommand("INSERT INTO T_Article_Image (Path, FK_Artikel_ID, Position) VALUES (#Path, #FK_Artikel_ID, #Position)", connection)
cmd.CommandType = CommandType.Text
cmd.Connection = connection
cmd.Transaction = transaction
cmd.Parameters.AddWithValue("#Path", img.Path)
cmd.Parameters.AddWithValue("#FK_Artikel_ID", newArticleRowId)
cmd.Parameters.AddWithValue("#Position", img.Position)
cmd.ExecuteScalar()
End Using
Next
End If
'-- If given article contains articles variations list (artikel_variation_attributes is a list with variations associated with article)
If Not IsNothing(artikel.collection(articleIndex)._artikel_variation_attributes) Then
For Each var In artikel.collection(articleIndex)._artikel_variation_attributes
'--Insert article's images if exists
Using cmd As New SqlCommand("INSERT INTO T_Artikel_T_Variation (FK_Variation_VariationAttribute_ID, FK_Artikel_ID, Position) VALUES (#FK_Variation_VariationAttribute_ID, #FK_Artikel_ID, #Position)", connection)
cmd.CommandType = CommandType.Text
cmd.Connection = connection
cmd.Transaction = transaction
cmd.Parameters.AddWithValue("#FK_Variation_VariationAttribute_ID", New Variation_VariationAttribute(var.FkVariationId, var.FkVariationAttributeId).GetId())
cmd.Parameters.AddWithValue("#FK_Artikel_ID", newArticleRowId)
cmd.Parameters.AddWithValue("#Position", var.Position)
cmd.ExecuteScalar()
End Using
Next
End If
Next
transaction.Commit()
Catch ex As Exception
result = False
'-- Roll the transaction back.
Try
transaction.Rollback()
Catch ex2 As Exception
' This catch block will handle any errors that may have occurred
' on the server that would cause the rollback to fail, such as
' a closed connection.
'Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType())
'Console.WriteLine(" Message: {0}", ex2.Message)
End Try
End Try
End Using
Return result
End Function
Everything works correctly, however, when it comes to error or whatever within those methods (from our example):
Dim resu As Boolean = createArticle.ProcessArticle(_artikelsAndTheirVariationsFinal)
or this method:
PrepareUberblick()
My circural form is not closing, but it's still running (it's stuck). When I do Alt+F4 to kill my circular form, I see an error message. I assume that when an error occurs, the error message window is not going to be shown in front but it's hidden behind the circular form.
Here's the question: do you know how to fix it so when an error occurs, an error message show up in front, so user could acknowledge and then the circular form would be closed?
If you want to alert the user that something happened, you could show the MessageBox from your circular form, which should appear on top of it because it's generated on the same thread. You can show the MessageBox in the Catch
Try
'--Run lenghty task
resu = createArticle.ProcessArticle(_artikelsAndTheirVariationsFinal)
Catch ex As Exception
pic.Invoke(Sub() MessageBox.Show(ex.message))
End Try
'--Close form once done (on GUI thread)
pic.Invoke(New Action(Sub() pic.StopCircular()))
pic.Invoke(New Action(Sub() pic.Close()))
This way, the user will need to click OK on the MessageBox before the circular form closes.
Your whole issue stemmed from the fact that you were not calling MessageBox.Show() on the same thread that the form was created on. It's not clear from your example where the form is created, whether it's on the UI thread or not. In either case, whichever thread the form is created on must be the same one that the message box is raised on, in order for the MessageBox to be modal to the form. By calling pic.Invoke(Sub() MessageBox.Show(ex.message)), you ensure it's shown on the form's thread, and will be modal to it. You can typically force the parent window by using the overload which has
Public Shared Function Show (
owner As IWin32Window,
text As String
) As DialogResult
which would be
pic.Invoke(Sub() MessageBox.Show(pic, ex.message))
Also see:
Does MessageBox.Show() automatically marshall to the UI Thread?
Why use a owner window in MessageBox.Show?

Threading: Invoke gets stuck .NET

I have a system tray application. The tray application has an icon and a context menu with some options. There is a menu called status which contains below toolstripmenuitems:
Start
Restart
Stop
They are enabled/disabled according to some conditions.
My system tray application, has a background thread does continuously check some conditions and do some work. Its main loop is below:
Do Until gAppExit Or Me.thdExit
' Check some conditions and do some work
Loop
gAppExit is a global variable that indicates whether user has exited from application through the 'exit' toolstripmenuitem.
thdExit indicates whether the thread should exit from the loop (I explain it later).
When user want to restart the background thread, he clicks on restart toolstripmenuitem and below sequence is done (Restart -> Halt -> WaitFinish):
Public Function ReStart() As Integer
Dim result As Integer
If IsNothing(ThreadBgWorker) Then
Logger.Write("Task is not yet created!", LOGGING_CRITICAL_ERRORS_CATEGORY)
Return RESULT_ERROR
End If
result = Me.Halt()
If result <> RESULT_ERROR Then
result = Me.Start()
End If
Return result
End Function
Public Function Halt() As Integer
Dim result As Integer
If IsNothing(ThreadBgWorker) Then
Logger.Write("Task is not yet created!", LOGGING_CRITICAL_ERRORS_CATEGORY)
Return RESULT_ERROR
End If
Me.thdExit = True
result = Me.WaitFinish()
Return result
End Function
Public Function WaitFinish() As Integer
Dim result As Integer
If IsNothing(ThreadBgWorker) Then
Return RESULT_ERROR
End If
result = RESULT_ERROR
Try
'TODO:
Console.WriteLine("Wait thread to finish (before Join)...")
Me.ThreadBgWorker.Join()
'TODO:
Console.WriteLine("Thread finished... Continue restarting thread...")
Me.RetVal = True
result = Me.ThreadBgWorker.ManagedThreadId
Logger.Write(String.Format("Task ""{0}"" correctly stopped.", _
Me.ThreadBgWorker.Name))
Catch ex As Exception
Logger.Write(String.Format("Couldn't stop task ""{0}"": {1}", _
Me.ThreadBgWorker.Name, ex.Message), _
LOGGING_CRITICAL_ERRORS_CATEGORY)
End Try
Return result
End Function
Public Function Start() As Integer
Dim result As Integer
If IsNothing(ThreadBgWorker) Then
Logger.Write("Task is not yet created!", LOGGING_CRITICAL_ERRORS_CATEGORY)
Return RESULT_ERROR
End If
result = RESULT_ERROR
Me.thdExit = False
Try
If Me.ThreadBgWorker.ThreadState = Threading.ThreadState.Stopped Then
Me.Create()
End If
Me.ThreadBgWorker.Start() ' Start the new thread.
result = Me.ThreadBgWorker.ManagedThreadId
Logger.Write(String.Format("Task ""{0}"" correctly started.", _
Me.ThreadBgWorker.Name))
Catch ex As Exception
Logger.Write(String.Format("Couldn't start task ""{0}"": {1}", _
Me.ThreadBgWorker.Name, ex.Message), _
LOGGING_CRITICAL_ERRORS_CATEGORY)
End Try
Return result
End Function
Note that on Halt function, it awaits thread to finish by calling Me.ThreadBgWorker.Join() on function WaitFinish. Before calling WaitFinish function, thdExit is set to true in order to background thread can exit from main loop:
Do Until gAppExit Or Me.thdExit
' Check some conditions and do some work
Loop
ChangeStatusToStopped()
on exit loop, ChangeStatusToStopped() is called, and it is as below:
Private Delegate Sub ChangeStatusToStoppedDelegate()
Public Sub ChangeStatusToStopped()
' TODO:
Console.WriteLine("Changing status to stopped...")
System.Windows.Forms.Application.DoEvents()
If MainMenu.InvokeRequired Then
'TODO:
Console.WriteLine("Invoke required!")
MainMenu.Invoke(New ChangeStatusToStoppedDelegate(AddressOf ChangeStatusToStopped))
Else
'TODO:
Console.WriteLine("Invoke NOT required!")
Me.submnuStart.Enabled = True
Me.submnuReStart.Enabled = False
Me.submnuStop.Enabled = False
End If
' TODO:
Console.WriteLine("Status changed to stopped.")
End Sub
What it does is to enable Start toolstripmenuitem and disable restart and stop toolstripmenuitems in the UI.
The problem is:
Within ChangeStatusToStopped method, when MainMenu.InvokeRequired is true, it calls:
MainMenu.Invoke(New ChangeStatusToStoppedDelegate(AddressOf ChangeStatusToStopped))
and then it gets stuck there, that is, else body:
Me.submnuStart.Enabled = True
Me.submnuReStart.Enabled = False
Me.submnuStop.Enabled = False
is never executed. It seems like main thread is busy or some other problem in message pump.... Any ideas?
I have seen that line:
Me.ThreadBgWorker.Join()
in WaitFinish() function is reached before background thread exits main loop and despite thdExit has been set to true before doing Me.ThreadBgWorker.Join(), once join is performed, application gets stuck, background thread cannot exit main loop (seems application is busy or frozen).

Update a label from a task

I'm trying to implement tasks in my program. I launch a task that will produce a log file, and after, I want to update the label to say "Log sucessfully saved".
Here is my code
Private Function Createlog(ByVal mylist As List(Of classTest))
Dim sw As New StreamWriter("log_list.log")
For index = 1 To mylist.Count - 1
sw.WriteLine(mylist(index).comments)
Next
sw.Close()
Try
Me.Invoke(UpdateLabel("Log sucessfully saved"))
Catch ex As Exception
End Try
Return 1
End Function
Private Function UpdateLabel(ByVal text As String)
Label1.Text = text
Return 1
End Function
I launch the task from the Main form in the Load() :
Dim tasktest = Task(Of Integer).Factory.StartNew(Function() Createlog(theList))
(I don't know if it is better to use the factory or declare as a task and then task.Start())
I have the error on the label update :
Cross-thread operation not valid: Control 'Label1' accessed from a thread
other than the thread it was created on.
Could you please explain why it doesn't work with the invoke method ? And do you have an alternative solution ?
Thanks for your help
First, UpdateLabel should be a Sub, not a Function. Second, this line is wrong:
Me.Invoke(UpdateLabel("Log sucessfully saved"))
Read it again. You are, in order, executing the UpdateLabel function, then passing the result of that function to Me.Invoke (if you used Sub instead of Function, the compiler should have warned you about the error).
This doesn't raise any compiler errors because a Function declared without As [Type] is defaulted to As Object, that can be cast to anything. It should be:
Me.Invoke(Sub()
UpdateLabel("Log sucessfully saved")
End Sub)
To simplify, your code can be rewritten like this:
Private Sub Createlog(ByVal mylist As List(Of classTest))
Dim sw As New StreamWriter("log_list.log")
For index = 1 To mylist.Count - 1
sw.WriteLine(mylist(index).comments)
Next
sw.Close()
Me.Invoke(Sub()
Label1.Text = "Log sucessfully saved"
End Sub)
End Sub

Shared Resource in Parallel.ForEach

How do I control access to a shared resource in a Parallel.ForEach loop? I am trying to download multiple files in parallel, and I want to capture information about downloads that fail, so that the user can re-attempt the download later. However, I am worried that if more than one download fails at the same time, the application will throw an exception because one thread will attempt to access the file while it is being written to by another.
In the code below, I would like to know how to control access to the file at RepeateRequestPath. A RequestSet is a list of strings that represent IDs of the resource I am trying to download.
Dim DownloadCnt As Integer = 0
Dim ParallelOpts As New ParallelOptions()
ParallelOpts.MaxDegreeOfParallelism = 4
Parallel.ForEach(RequestSets, ParallelOpts, Sub(RequestSet)
Try
DownloadCnt += 1
Dim XmlUrl As String = String.Format("{0}{1}{2}", "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=", String.Join(",", RequestSet), "&retmode=xml&rettype=abstract")
DownloadFile(XmlUrl, String.Format("{0}\TempXML{1}.xml", XMLCacheDir, DownloadCnt))
Catch ex As WebException
Using Response As WebResponse = ex.Response
Dim statCode As Integer = CInt(DirectCast(Response, HttpWebResponse).StatusCode)
MessageBox.Show(String.Format("Failed to retrieve XML due to HTTP error {0}. Please hit the 'Retrieve XML' button to re-run retrieval after the current set is complete.", statCode))
If Not File.Exists(RepeatRequestPath) Then
File.WriteAllLines(RepeatRequestPath, RequestSet)
Else
File.AppendAllLines(RepeatRequestPath, RequestSet)
End If
End Using
End Try
End Sub)
The usual way to protect a shared resource in VB.NET is to use SyncLock.
So, you would create a lock object before the Parallel.ForEach() loop:
Dim lock = New Object
and then you would use that inside the loop:
SyncLock lock
File.AppendAllLines(RepeatRequestPath, RequestSet)
End SyncLock
Also note that you can use AppendAllLines() even if the file doesn't exist yet, so you don't have to check for that.
You need to use a semaphore to control access to a shared resource. You want only one thread to access the error file at one time, so initialize the semaphore to only allow 1 thread in. Calling _pool.WaitOne should seize the semaphore, and then release it once it finishes creating/writing to the file.
Private Shared _pool As Semaphore
_pool = = New Semaphore(0, 1)
Dim DownloadCnt As Integer = 0
Dim ParallelOpts As New ParallelOptions()
ParallelOpts.MaxDegreeOfParallelism = 4
Parallel.ForEach(RequestSets, ParallelOpts, Sub(RequestSet)
Try
DownloadCnt += 1
Dim XmlUrl As String = String.Format("{0}{1}{2}", "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=", String.Join(",", RequestSet), "&retmode=xml&rettype=abstract")
DownloadFile(XmlUrl, String.Format("{0}\TempXML{1}.xml", XMLCacheDir, DownloadCnt))
Catch ex As WebException
Using Response As WebResponse = ex.Response
Dim statCode As Integer = CInt(DirectCast(Response, HttpWebResponse).StatusCode)
MessageBox.Show(String.Format("Failed to retrieve XML due to HTTP error {0}. Please hit the 'Retrieve XML' button to re-run retrieval after the current set is complete.", statCode))
_pool.WaitOne()
Try
If Not File.Exists(RepeatRequestPath) Then
File.WriteAllLines(RepeatRequestPath, RequestSet)
Else
File.AppendAllLines(RepeatRequestPath, RequestSet)
End If
Catch ex as Exception
'Do some error handling here.
Finally
_pool.Release()
End Try
End Using
End Try
End Sub)
svick's solution is almost right. However if you need to protect access to a shared variable you also need to declare your lock object as shared at class level.
This works correctly:
Friend Class SomeClass
Private Shared _lock As New Object
Private Shared sharedInt As Integer = 0
Sub Main()
SyncLock _lock
sharedInt += 1
End SyncLock
End Sub
End Class
If you use a non-shared lock object, synclock will protect the variable only from multiple accessing threads within the same instance, not across instances.

Creating instances of forms inside a thread

i am trying to create a new instance of a form if its not allready been created the only problem is is that the instance creation is inside a thread.
Private Sub doRead(ByVal ar As System.IAsyncResult)
Dim totalRead As Integer
Try
totalRead = client.GetStream.EndRead(ar) 'Ends the reading and returns the number of bytes read.
Catch ex As Exception
'The underlying socket have probably been closed OR an error has occured whilst trying to access it, either way, this is where you should remove close all eventuall connections
'to this client and remove it from the list of connected clients.
End Try
If totalRead > 0 Then
'the readBuffer array will contain everything read from the client.
Dim receivedString As String = System.Text.Encoding.UTF8.GetString(readBuffer, 0, totalRead)
messageReceived(receivedString)
End If
Try
client.GetStream.BeginRead(readBuffer, 0, BYTES_TO_READ, AddressOf doRead, Nothing) 'Begin the reading again.
Catch ex As Exception
'The underlying socket have probably been closed OR an error has occured whilst trying to access it, either way, this is where you should remove close all eventuall connections
'to this client and remove it from the list of connected clients.
MessageBox.Show(ex.ToString)
End Try
End Sub
The problem lies when the form is created on the line .showDialog() it stops here untill the application is closed. ive tryed using .show() but then the new "Convo window just hangs"
Private Sub messageReceived(ByVal message As String)
Dim data() As String = message.Split("|"c)
Select Case data(0)
Case "MESSAGE"
Dim chatDialog As New RichTextBox
Try
If conversations.ContainsKey(data(1)) Then
Dim convoWindow As ChatWindow
convoWindow = New ChatWindow
convoWindow = conversations.Item(data(1))
chatDialog = convoWindow.RichTextBox1
Else
Try
Dim convoWindow As New ChatWindow()
conversations.Add(data(1), convoWindow)
convoWindow = conversations.Item(data(1))
convoWindow.ShowDialog()
chatDialog = convoWindow.RichTextBox1
AppendTextChatWindows(data(2), chatDialog)
Thanks
Houlahan