I'm writing a batch publishing program in VB.NET using VS2008. My program is a Windows form.

The program spawns a process called "xtop". The process converts one CAD format to another. The conversion process demands heavy CPU cycles.

The user will add "jobs" to the batch. The batch program will send one job to the publisher at a time. Each job is to be published using a worker thread in the background to make sure the UI (Windows form) doesn't get locked up. Its important that users can add jobs to the batch 24/7 while jobs are publishing in the background.

I've never written a multi-threaded app in VB.NET. I'm having some difficulty formulating a strategy.

I was planning to call the background worker thread using:

Dim pubThread As Thread = New Thread(AddressOf Me.backgroundPublishing)
pubThread.Start()

Then in a private sub called backgroundPublishing() I was going to write this:

Using p As New Process
                With p.StartInfo
                    .FileName = proe
                    .Arguments = cmd
                    .UseShellExecute = False
                    .CreateNoWindow = True
                    .RedirectStandardInput = True
                    .RedirectStandardOutput = True
                End With
                p.Start()
                p.EnableRaisingEvents = True
                Debug.WriteLine(cmd)
                p.WaitForExit()
                p.Close()
End Using

The problem is that the main thread controls the batching. It basically loops through all the jobs that have been added to the batch (a list box).

I was planning to "freeze" the loop with a nested while loop that "watches" my "xtop" process until the background worker process is done. This will prevent multiple jobs from being sent to the worker thread (and multiple "xtop" processes from spawning).

While Process.GetProcessesByName("xtop").Length > 0
                'xtop process running
End While

Thus, once the first job is run (and the corresponding "xtop" process ends in the worker thread) program control resumes with and the next job is sent to the worker thread.

There is one major problem though. The While loop "feezes" the UI (Windows form) so that new jobs can't be added. Which, of course, defeats the purpose of sending the worker process to its own thread.

Can someone please explain a good way to program something like this? I'd simply like to make the UI accessible at all times while running CPU intensive CAD conversions in the background. Thank you!

add application.doevents in your background process

this will allow users to interact with the form while the background process runs.

Comments
never add application.doevents to your code. Let the framework pump messages

Application.Doevents can cause some very unexpected results and problems of it's own. I'm not going to say it doesn't have it's place, but you want to be careful with it, it is not a replacement for proper threading design.

OP it sounds like what you need to implement is a call back function to let the main thread know when the batch processing thread is finished so it can submit another job (spawn another thread).

That sounds pretty simple, but so much can happen when you start dealing with asynchronous operations that you really do need to in most cases have a solid understanding of threading and how to avoid the pitfalls.

i think what he wanted to do was have the background process run 24/7 while they are able to submit jobs at the same time...

what i would probably do, and have before
setup a db, when they enter a job have it save the job to a table

have a seperate application that runs hidden every minute or so load, check the table for new entries, if found submit the jobs.

will avoid confusion and errors in the system. just an idea

Well you should probably use the thread pool and stay away from creating threads with the Thread class and the background worker. Unfortunately I primarily do C# development and could whip this up a lot faster but its taken me 30 minutes to find all the VB.NET equivelants for this stuff and i'm out of time :P

This handles a queue and opens up solitaire. You should probably add events to the VB.NET CADQueue class below to signal when processing completes but I don't know how to do that off hand. Something else that might be handy is cleaning up the timer declared in that class by implementing IDisposable , and handling forced shutdowns of your application, etc. This should get you started:

Imports System.Threading
Imports System.Timers
Imports System.Runtime.Remoting.Messaging
Imports System.Diagnostics

Public Class Form1
  Private worker As CADQueue

  Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    worker.AddJob("C:\file1.txt")
    worker.AddJob("C:\file2.txt")
    worker.AddJob("C:\file3.txt")
    worker.AddJob("C:\file4.txt")
    worker.AddJob("C:\file5.txt")
    worker.AddJob("C:\file6.txt")
    worker.AddJob("C:\file7.txt")
    worker.AddJob("C:\file8.txt")
  End Sub

  Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    worker = New CADQueue()
  End Sub
End Class

Public Class CADQueue
  Private runQueueLock As Integer
  Private Const MAX_PROCESSES As Integer = 2 'Maximum number of processes to spawn at once
  Private threadCnt As Integer
  Private jobs As List(Of String)
  Private timer As System.Timers.Timer

  Public ReadOnly Property ThreadCount() As Integer
    Get
      Return Thread.VolatileRead(threadCnt)
    End Get
  End Property

  Public ReadOnly Property PendingJobCount() As Integer
    Get
      SyncLock jobs
        Return jobs.Count
      End SyncLock
    End Get
  End Property

  Public Sub New()
    threadCnt = 0
    runQueueLock = 0
    jobs = New List(Of String)
    timer = New System.Timers.Timer(500)  '0.5 second
    timer.AutoReset = True
    AddHandler timer.Elapsed, AddressOf OnTimerElapsed
    timer.Start()
  End Sub

  Private Sub OnTimerElapsed(ByVal source As Object, ByVal e As ElapsedEventArgs)
    RunQueue()
  End Sub

  Public Sub AddJob(ByVal fileName As String)
    SyncLock jobs
      jobs.Add(fileName)
    End SyncLock
  End Sub

  Private Function GetNextJob() As String
    'This will fail if you call it with 0 pending jobs
    Dim result As String
    SyncLock jobs
      result = jobs(0)
      jobs.RemoveAt(0)
    End SyncLock
    Return result
  End Function

  Private Sub RunQueue()
    'This compare exchange basically makes sure we're not stacking up calls from the timer.
    'Since our timer executes every 0.5s it is very likely that the timer event will elapse
    'before the previous call to it completed. What this code does is see if the timer
    'is still executing from the last elapsed event. If it is executing then it returns
    'right away.

    Dim exch = Interlocked.CompareExchange(runQueueLock, 1, 0)
    If (exch <> 0) Then  'This is already running on another thread
      Return
    End If

    Try
      While (Thread.VolatileRead(threadCnt) < MAX_PROCESSES) And (Me.PendingJobCount > 0)
        Dim fName As String = GetNextJob() 'Pulls a job out of the queue
        Dim del As Action(Of String) = New Action(Of String)(AddressOf RunCadProgram)  'Create our delegate

        del.BeginInvoke(fName, New AsyncCallback(AddressOf CleanupCallback), fName)  'Run cad on another thread

        'Since the other threads execution is at the mercy of the scheduler we need to increment our
        'thread count here so we dont spawn too many process
        Interlocked.Increment(threadCnt)
      End While
    Finally
      Interlocked.Exchange(runQueueLock, 0)  'Indicate we're all done here
    End Try

  End Sub

  Private Sub RunCadProgram(ByVal fName As String)
    Using p As New Process()
      p.StartInfo.FileName = "sol.exe" 'Solitaire instead of your CAD program
      p.StartInfo.Arguments = """" + fName + """"
      p.Start()
      p.WaitForExit()
    End Using
  End Sub

  Private Sub CleanupCallback(ByVal ar As IAsyncResult)
    Dim fName As String = CType(ar.AsyncState, String)
    Dim result As AsyncResult = CType(ar, AsyncResult)
    Dim del As Action(Of String) = CType(result.AsyncDelegate, Action(Of String))
    Try
      del.EndInvoke(ar)
    Catch ex As Exception
      'Any exception from RunCadProgram() is re-thrown/re-raised here
    Finally
      Interlocked.Decrement(threadCnt)
    End Try
  End Sub

End Class

I have attached the project for a runnable example (if you have solitaire :P)

Edited 6 Years Ago by sknake: n/a

I apologize for not responding to all of your help sooner. I've been on vacation for the holidays ... now its back to work. Happy New Year!

The same day I submitted this help request, I tried jlego's suggestion to add application.doevents in my while loop. That seemed to help a lot. However, to be honest, I still have some testing to do as the timing wasn't 100% copastetic where I left things. Your understanding of my project (in your second post) is 100% correct (and you stated it more succinctly than I did). I will keep you DB idea in mind (its a cool idea).

coat - thanks for the tip on using application.doevents. I'll keep an eye on things and make sure the new threads are behaving properly. This seems to be a fairly simple program (at least to me). I'm only initiating one extra thread at a time ... if all goes according to plan.

sknake - thanks for your suggestion and all the code. I need to take this offline and digest it a bit! Another cool idea.

Will report back as soon as I have a chance to write some code ... still catching up.

Again, thanks for the top notch support! I really appreciate it.

This article has been dead for over six months. Start a new discussion instead.