Link to home
Start Free TrialLog in
Avatar of D B
D BFlag for United States of America

asked on

How Do I Instantiate Classes in Separate Threads from Code in WIndows Form

I have the following code currently in place. It is within a Windows form. I am not sure exactly how to do it, but I would like to create each JobClass object in a separate thread. I've done some reading on threading and thread pools and, like an issue I just resolved with delegates, it is still a little above my level of comprehension.

One issue that I am trying to resolve, and unless I am mistaken, using threads will do it, is that each class can use System.Threading.Thread.Sleep() and currently, when it does, it causes the whole application to sleep. I want just that class to sleep.

If I create the classes in separate threads do I have to do anything special to access their properties and methods and events in my parent form?

Also, if I create each class in a separate thread, is there a 'proper' way to shut down my application.

For Each XMLNode In XMLDoc.DocumentElement.ChildNodes
  Select Case XMLNode.Name
    Case "COPYJOB"
      Dim JobClass As New cImportFile
        For Each XMLNodes In XMLNode.ChildNodes
          With JobClass
            ... ' Add properties to object
          End With
          AddHandler JobClass.NewFileProcessedEvent, AddressOf Me.OnNewFileProcessedEvent
         AddFile(JobClass) ' This adds the class to an array
  End Select
Next
Avatar of VBRocks
VBRocks
Flag of United States of America image

You can do it like this:

For Each XMLNode In XMLDoc.DocumentElement.ChildNodes
  Select Case XMLNode.Name
    Case "COPYJOB"
      Dim JobClass As New cImportFile
        For Each XMLNodes In XMLNode.ChildNodes
          With JobClass
            ... ' Add properties to object
          End With

        Dim thread As New Threading.Thread(AddressOf JobClass.NewFileProcessedEvent)
        thread.Start()

         AddFile(JobClass) ' This adds the class to an array

  End Select
Next




Avatar of D B

ASKER

Dim thread As New Threading.Thread(AddressOf JobClass.NewFileProcessedEvent)

gives the error:
'AddressOf' operand must be the name of a method (without parentheses).

I would use this (once I get it working) instead of:
AddHandler JobClass.NewFileProcessedEvent, AddressOf Me.OnNewFileProcessedEvent ?

What ties the event being raised in the class back to the event handler in my form (OnNewFileProcessedEvent)?
This is because when you declare the method (sub) to be executed, you can't include ().

For example:

    'This will throw an error:
    Dim thread As New Threading.Thread(AddressOf JobClass.NewFileProcessedEvent())

    'This will not throw an error:
    Dim thread As New Threading.Thread(AddressOf JobClass.NewFileProcessedEvent)

Remove the "NewFileProcessedEvent" from the the cImportFile (JobClass) class, and put the
OnNewFileProcessedEvent inside the cImportFile (JobClass) class.  Then, execute the thread
like this:

    Dim thread As New Threading.Thread(AddressOf JobClass.OnNewFileProcessedEvent)
    thread.start()

So just as an example, the cImportFile should look something like this:

Public Class cImportFile

    Public Sub OnNewFileProcessedEvent()
   
    End Sub

End Class

Avatar of D B

ASKER

But I want the event to be processed in the form, not in the class. That is why it is an event, so it can be broadcast back to the form.

Also, I do not have:
Dim thread As New Threading.Thread(AddressOf JobClass.NewFileProcessedEvent())

If you look at the code you provided, and the code I gave in my example of what is causing the error, you will see there are no parantheses. It is:
Dim thread As New Threading.Thread(AddressOf JobClass.NewFileProcessedEvent)
which is the example you gave.
Avatar of D B

ASKER

For more info, the event in the class (cImportFile) sends back arguments that are used in the form to populate a listview control.
If you want the event to be processed in the form, then the OnNewFileProcessedEvent method needs
to reside in the form.  So the code to execute it would be:

    Dim thread As New Threading.Thread(AddressOf Me.OnNewFileProcessedEvent)
    thread.start()

The thing is, since you are using AddHandler to add a handler to the NewFileProcessedEvent event that
is in the cImportFile class, then assumably you are going to use the RaiseEvent within the cImportFile
class.  When you do, the OnNewFileProcessedEvent, which is one the form will get executed.

It gets a little tricky, so let me give you another example:


Public Class cImportFile

    Public Event NewFileProcessedEvent()

    Public Sub StartProcessing()

        'When you get ready to fire your event, do it like this:
        '    This will execute the method that subscribed to it using AddHandler on the form.
        RaiseEvent NewFileProcessedEvent()

    End Sub

End Class



'And on your form:
Public Class Form2
    Private Sub Button1_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles Button1.Click
       
        'Call the sub that starts the thread
        ExecuteThread()

    End Sub


    Private Sub ExecuteThread()
        Dim JobClass As New cImportFile()

        'Add your event handler
        AddHandler JobClass.NewFileProcessedEvent, AddressOf OnNewFileProcessedEvent

        Dim thread As New Threading.Thread(AddressOf JobClass.StartProcessing)
        thread.Start()

    End Sub

    'This is the event that subscribed to the NewFileProcessedEvent event
    Private Sub OnNewFileProcessedEvent()
        MsgBox("Event fired on separate thread")

    End Sub
End Class



AddHandler JobClass.NewFileProcessedEvent, AddressOf Me.OnNewFileProcessedEvent
Just a note, that last line of that post didn't belong there, so just delete it:

    AddHandler JobClass.NewFileProcessedEvent, AddressOf Me.OnNewFileProcessedEvent

Avatar of D B

ASKER

I already have everything in palce and working. The RaiseEvent code in the class, the event handler on the form, etc. I create a delegate in the class:
    Public Delegate Sub NewFileProcessedEventHandler(ByVal sender As Object, ByVal e As FileProcessedEventArgs)
    Public Event NewFileProcessedEvent As NewFileProcessedEventHandler

And am using the following in the Form's event handler:
        If (Me.InvokeRequired) Then
            Me.Invoke(New cImportFile.NewFileProcessedEventHandler(AddressOf OnNewFileProcessedEvent), New Object() {source, e})
        Else
            ' Add data to listview
        End If

The only thing I want to do is change the instantiation of the cImportFile (JobClass) objects to be in separate threads.

Also,
1) I want to get validation that I am on the right path, that if I do so, using System.Threading.Thread.Sleep() in an instance of JobClass will only affect that particular object.
2) Is there anythig special I need to do to exit my application?
3) Are there any specific requirements to accessing properties and methods of the JobClass objects?
Avatar of D B

ASKER

I am not sure if I am being unclear here of what I want to accomplish, but I would like to try once more to see if we can get things going:
I have a Windows application. The program consists of a windows form and two class modules. ClassModule 1 (dbClass) handles only database activity (writing reccords to SQL Server.) It is best to think of it being out of the picture.
The form code, upon opening, parses an XML file. This file contains parent and child nodes.  specific parent node value determines I need to creare another instance of a class object (clsFile). clsFile instantiates a SystemFileWatcher within the class and through some properties that were set in the form module, peacefully waits for the appropriate file to magically show up in the watched directory and then does some additional processing. Part of that processing is to raise an event back to the form (which I've used addHandler clsFile(HandlerMethon, FormHandler in the form module). The event handler sends data back to the form and the FormHandler code populates a ListView control with data passed by the event handler.

All works wonderfully--as expected. With a single exception. Normally, the SFW fires as soon as it detects a file in the referenced directory. However, my additional processing cannot continue until all data has been moved to the location and the file closed by the host process that is creating it. It cas take up to 5-minutes to completely spool the file over.

To handle this 'problem' I catch the error as an IOExcetion, and use the code:
    System.Threading.Thread.Sleep(cConstants.FILEWATCH_SLEEP_SECONDS * 1000)
to make the class slppe for some time so that it can try again later. The problem here is that while the clss is in sleep mode, everything hangs up. Right now there are 12 and later 16 of these classes that have been created to watch different files with different filters in different folders. If ONE is sleeping then none of the others will detect arrival of a new file in another directory. There is a 999.94% chance that the class processing the file would not get a second file whild processing the first, but there is a very good change that FileClass6 would need to process a file.

My understanding (what little I have left of it) leds me to believe that when I instantiate the class from the form code, I can do so in a separate thread, thus any processor extensive activity, or sleep() commands perpormed on that class will not affect the others, or the parent activity. One it has done its proseccing, it just goes back to witting there waiting for a file (which might not be for another 24 hours)

This works. Exactly how it is supplose to. With one small excetption.

The code parts that are being exposed are (in the Form):
     'OnNewFileProcessedEvent90 is the event code on my form.
     'NewFileProcessedEvent is the event raised in the class.
     'AddClass(jobClass) adds the class to a colection array.
     AddHandler JobClass.NewFileProcessedEvent, AddressOf Me.OnNewFileProcessedEvent
     AddFile(JobClass)

clsFile has the following code:

' Create a delegate for the event handler to it will be thread safe for the form.
    Public Delegate Sub NewFileProcessedEventHandler(ByVal sender As Object, ByVal e As FileProcessedEventArgs)
' The event handler
    Public Event NewFileProcessedEvent As NewFileProcessedEventHandler

' the code to perform the sleep located in a different method.
System.Threading.Thread.Sleep(cConstants.FILEWATCH_SLEEP_SECONDS * 1000)

the code to actually raise the event:
RaiseEvent NewFileProcessedEvent(Me, New FileProcessedEventArgs(SourceFilePath, e.Name, ImportStatus, DateProcessed))

I want a safe way to start this application and have it instantiate each clsFile object into a separate thread so they won't be hanging each other up.

If I need to do something outside what it is I am thinking I need to do, someone needs to unscrew my thinking cap and replace it with a new one that can better understand what I am trying to do.

Regards,
Doug
Avatar of D B

ASKER

I am going to republish my original question along with a little more into so see if someone can help me get off the ground here. My knowledge of multi-threading scroes about .036 on the Ritzvolkosowki scal oe 0-10. Needleess to say, if one of the Beatles was around I'd be asking to hold his hand :-)

Now that the humor almost always grams your attention, here is the best I can do to describe my currently working and operational program. Minor changes preferable, to keep changes to a working product at a minimum. btw: there is no disclaimer here that it will continue to work properly. In fact, I think I know what to do to break it, IBut I don't want to.

Windows application:
2) Windowd Forms (FormApp, FormStat)
3) Class modules (cImportJob, cDataAccessr, cConstants)
1) XML File

FormAaa is startup. Form_Load() reads are parses an XMl file. Based on node/child-node values, a cImpirtJob class is instantiated, properties are added to it (based on child node values) an addHadler is added to it and added to a collection array. Right now it is a dumb class and does nothing.
After all nodes of the WML file have been parsed, a loop sets a property on each class to "activate it"

Activating it primarilly means enabling a StstemFileWatcher object with the path, mask, and some other properites set based on valued parsed from the XML file.

Oder of action:
0_ wait..wait...wait...wait...wait...wait...
1) file is detected by the FSW and kicks off a function, passing some parameters.
2) the file is opened in a stream reader and certain files stats are gatered by sequentially reading the data.
3) the stats gathered are written to SQL Server table.
4) the file picked up by the watcher is moved from the source to a destination directory.
5) an event is raised that the form picks up to add a row to a listview control.
6) go back to step 1

The problem is: Process 2. The host FTP process can take up to 5 minutes to finish spooling the file. When that happens, a TRY/CATCH loop catches an exception error. I sleep for about 10 seconds, then try again. When the sleep is executed, the entire application *all 8 classes and the application form) go to sleep.

Normally this would not be a problem except that the host system could try to send a second file to a different directory that one of the other classes would normally pick up. However, because the class is sleeping, the file gets missed.

I originally thought I would need to instantiate the entire class within the form code, but someone said they were not sure that could be done.

Someone else advised kicking off the function in the claass that calls the timer in a serarate thread and that should take care of it, even though that function subsequently calls ther functions in the same class. The thinking was that if the 'base' class was on another thread, and functions that were called by it would also be on that new thread.

The structure of my class code is as follows. OnFileAdded() is called by the filesestem watcher that is instantiated in the class (one per class). After displaying a small windiw inticating the file is being processed, it calls GetRecordCounts() which is the 'trouble' method where the sleep() can occur. Assuming proper termination of that method, MoveFile() and WriteFileStatistics() are subsequently called, then the event raised.

It is my understanding (from a C++ developer who does not know .NET that I should be able to create GetRecordCounts() in a separate thread and that will do away with my problems and in factkeep other potential problems from orruccing, since now another file can come across on the same class while one is being processed and it will not cause any problems.

Questions:
1) Can this be done:
2) How (please use crayons and broad lines-it is still very new to me):
3) Can it be done more efficiently that instantiating the classess in a new thread when they are created (if that can be done:
3) WHat is required to determine a class has one or more of these threads running and then safely shut them down before destroying the class when the program terminates )I don't want to leave a 1-/2 copied file hanging out there).


 Private Sub OnFileAdded(ByVal source As Object, ByVal e As FileSystemEventArgs)
        Dim intRC As Integer
        Dim frmFileInfo As New frmProcessingInfo
        Dim dtProcessDate As Date = Now()

        mblnIsProcessing = True

        With frmFileInfo
            .FilePath = mstrSourceFilePath
            .FileName = e.Name
            .Show()
            .Refresh()
        End With

        If GetRecordCounts(e.Name) Then
            If MoveFile(e.Name) Then
                intRC = WriteFileStatistics(e.Name)
                Select Case intRC
                    Case Is < 0
                        mlngImportStatus = FileProcessedEventArgs.ImportStatuses.ImportStatusDatabaseRecordAlreadyExists
                    Case 0
                        mlngImportStatus = FileProcessedEventArgs.ImportStatuses.ImportStatusDatabaseUpdateUnsuccessful
                    Case Else
                        mlngImportStatus = FileProcessedEventArgs.ImportStatuses.ImportStatusSuccess
                End Select
            Else
                mlngImportStatus = FileProcessedEventArgs.ImportStatuses.ImportStatusUnableToMoveFile
            End If
            RaiseEvent NewFileProcessedEvent(Me, New FileProcessedEventArgs(mstrSourceFilePath, e.Name, mlngImportStatus, dtProcessDate))
        Else
            mlngImportStatus = FileProcessedEventArgs.ImportStatuses.ImportStatusUnableToRead
        End If

        mblnIsProcessing = False
        frmFileInfo.Close()
        frmFileInfo = Nothing
    End Sub

    ' Open the new file and get record counts (header and detail). If an exception occurs
    ' attempting to open the file, it is likely because the file is still being streamed
    ' to the directory by the host process. In that case, wait 15-seconds and try again.
    ' The process of attempting to open the file and wait on error will loop up to 20
    ' times (5 minutes) before reporting failure.
    Protected Function GetRecordCounts(ByVal FileName As String) As Boolean
        Dim intCounter As Integer = 0
        Dim strInputFile As String = Path.Combine(mstrSourceFilePath, FileName)

        mlngHeaderCount = 0
        mlngDetailCount = 0

        Do
            Try
                Using sr As StreamReader = New StreamReader(strInputFile)
                    Dim strLine As String
                    Do
                        strLine = sr.ReadLine()
                        If strLine.StartsWith(mstrHeaderIdentifier) Then
                            mlngHeaderCount += 1
                        Else
                            mlngDetailCount += 1
                        End If
                    Loop Until sr.EndOfStream
                    sr.Close()
                End Using
                Exit Do
            Catch E As Exception
                If intCounter < cConstants.FILEWATCH_SLEEP_ITERATIONS Then
                    System.Threading.Thread.Sleep(cConstants.FILEWATCH_SLEEP_SECONDS * 1000)
                    intCounter += 1
                Else
                    Return False
                End If
            End Try
        Loop
        Return True
    End Function

    ' Move the file from the source to the destination directory. An error typically means
    ' the file already exists in the destination directory, although it could mean the
    ' destination directory/disk is full, or another type of read/write error occurred.
    ' For now, I am only reporting that the file copy was unsuccessful.
    Protected Function MoveFile(ByVal FileName As String) As Boolean
        Dim strSource As String = Path.Combine(mstrSourceFilePath, FileName)
        Dim strDestination As String = Path.Combine(mstrDestinationFilePath, FileName)

        Try
            If System.IO.File.Exists(strDestination) Then Return False
            File.Move(strSource, strDestination)
            Return True
        Catch ex As System.IO.IOException
            Return False
        End Try
    End Function

    ' Add file name, path and statistics to the database.
    ' Returns PK of the new added row. Success is positive non-zero return value. A
    ' return code of -1 indicates the file already exists in the database.
    Protected Function WriteFileStatistics(ByVal FileName As String) As Long
        Dim DataAccessor As New cDataAccessor

        With DataAccessor
            .SetProcedure("sp_add_import_file_statistics")
            .AddParam("@FilePath", ParameterDirection.Input, SqlDbType.VarChar, mstrDestinationFilePath, 512)
            .AddParam("@FileName", ParameterDirection.Input, SqlDbType.VarChar, FileName, 255)
            .AddParam("@Group", ParameterDirection.Input, SqlDbType.BigInt, mlngImportGroup)
            .AddParam("@HeaderCount", ParameterDirection.Input, SqlDbType.Int, mlngHeaderCount)
            .AddParam("@DetailCount", ParameterDirection.Input, SqlDbType.Int, mlngDetailCount)
            .Execute(False)
            Return .ReturnCode
        End With
    End Function
                           
I am interested in trying to help you.  I use multi-threading quite a bit, infact just about every time I work
with data from a database.  However, your previous response didn't seem like you were interested
in changing your approach, since you already have your "code in place."  

When I get a chance, I'll take a look at what you have posted.  If you find an answer with your other
post, please be sure to let me know so I don't waste time trying to solve it.

Thanks!

Avatar of D B

ASKER

VBRocks: There is no other post. I just kind of gave a little more detail into the specifics of what I am trying to do. I also reread my post and have come to realize that I do not want to post questions at 2 in the morning when I can hardly keep my eyes open :-)

I would not be opposed to rewriting it. It is a pretty simple application in what it does and as long as it continues to process the way I want it to, I am fine with it. Having to make as few modifications as possible is always the preferred method.

However, that said, and as I noted, I am a multi-threading baby and if my whole concept is wrong and it should be approached from a different perspective, shoot away. The key points are:

1) There can be any number of watchers going (each defined by a node in the XML file). Each one will process a different file (based on filter property) in a directory (there can be more than one monitoring the same directory).
2) I don't want one process to stall another.
3) There is a HIGH probability that two DIFFERENT filewatchers could fire in close proximity to each other (timewise).
4) There is less probability (but I assume a possibility) that more than one file could come across on the same watcher in close proximity to each other (timewise).
5) Whatever I do, I need a safe way to shut everything down. I don't want to start destroying objects that are in the middle of processing a file.
One question for your:  In class clsFile, after you have created a new instance of it, and set some
properties, is there a sub or function that you call to "activate it", so to speak?  If there is, can you
show me what it looks like?

Thanks.


Avatar of D B

ASKER

There is a property called Enablejob in the class.

Public Property EnableJob() As Boolean
    Get
        Return IsEnabled
    End Get
    Set(ByVal value As Boolean)
        IsEnabled = value
        FileWatcher.EnableRaisingEvents = IsEnabled
    End Set
End Property                

See: https://filedb.experts-exchange.com/incoming/ee-stuff/4398-FileRecordCounts.zip 
I've zipped and uploaded the code.
Perfect.  Let me take a look at it.

Thanks.
Ok, I'd like to upload it to you so you can take a look at it.  How do you upload a file?

Thanks.
Avatar of D B

ASKER

So basically, the code:
                'ImportFileObject.ProcessExistingFiles()

was changed to:
                Dim thread As New Threading.Thread(AddressOf ImportFileObject.ProcessExistingFiles)
                thread.IsBackground = True
                thread.Start()

However, I would question some of your logic.

The method ProcessExistingFiles() in the class does not really "kick off" the process. It is there as a "cleanup method" in case the program was not running at the time a file was transfered, or if for some other reason, it did not get processed. If you notice, in my original code, I called it prior to enabling the system file watcher.

The real processing does not start until I set the EnableProcessing() property to True. There is really no possibility at all of any contention while running the code in ProcessExistingFiles() because any existing files are always going to be processed sequentially, and since they already exist, there would be no problem with file locking in the GetRecordCounts() function. At the time ProcessExistingFiles() is called, the file watcher is not enabled and no file events will be caught by the class (although it would be possible to have EnableProcessing=True and call ProcessExistingFiles()--which is something I should prevent from happening.)

Let me know what you think?
Well, your statements from above indicate that you are running into a bottle neck when you begin moving
a file, as stated here:

    All works wonderfully--as expected. With a single exception. Normally, the SFW fires as soon as it  
    detects a file in the referenced directory. However, my additional processing cannot continue until all
    data has been moved to the location and the file closed by the host process that is creating it. It cas
    take up to 5-minutes to completely spool the file over.

The file moving takes place in the MoveFile sub, which is called from the OnFileAdded sub.  One final
suggestion would be to move your code out of the OnFileAdded sub to a different sub, perhaps one
named "FileAdded", and then create a new thread and call the "FileAdded" sub from the OnFileAdded
sub.    For example:

    Protected Sub OnFileAdded(ByVal source As Object, ByVal e As FileSystemEventArgs)

        Dim thread As New Threading.Thread( _
            New Threading.ParameterizedThreadStart(AddressOf FileAdded))

        thread.IsBackground = True
        thread.Start(New Object() {source, e})

    End Sub

    Private Sub FileAdded(ByVal obj as Object)

         Dim source As Object = obj(0)
         Dim e As FileSystemEventArgs = obj(1)

        Dim RC As Integer
        Dim FileInfo As New frmProcessingInfo
        Dim DateProcessed As Date = Now()

        IsProcessing = True

        With FileInfo
            .FilePath = SourceFilePath
            .FileName = e.Name
            .Show()
            .Refresh()
        End With

        'And the rest of your code...

    End Sub


Bottom line, What you need to do is identify an entry point for the thread.  If what I have suggested doesn't work for you, then you need to do a little thinking and figure out what does.

Good luck.



Avatar of D B

ASKER

That can be true, but if a bottleneck occurs, it is in GetRecordCounts() when I attempt to open the stream reader. If the file is still being written by the host process, the stream reader will not be able to open it. That is where I use System.Threading.Thread.Sleep(). That is what can lock the application and keep other SystemFileWatchers from firing.

I do not call MoveFile() until after GetRecordCounts(), and by the time I get out of GetRecordCounts() there should be no file locking issues. I've been doing some more reading on threads and delegates, etc. I believe GetRecordCounts() is what needs to be put on a separate thread. I am going to take a deeper look at it. Unfortunately, this is more of a low-priority side project so I am not able to stay on top of it, so I read a little, code even less, forget what I've read, then start the cycle over again :-(

I do thank you for your assistance, and am sure what you have provided along with some persistence will lead me to a solution. Like I said, it is working, and as long as I just get one file at a time, spaced out at good intervals, there would not be any problems. I just want to make allowances for what "might" happen so that if it does, it won't hang everything up, or miss processing a file.
ASKER CERTIFIED SOLUTION
Avatar of D B
D B
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial