Multiple Threads in ASP.NET (VB)

16 Oct 2011

I’ve been using ASP.NET for years and never thought about the usage of threads within the .NET site. It’s not that I thought it was impossible, it’s just that I hadn’t even considered it.

Earlier this year I did a few experiments with threads and more recently I built upon this and put a generic task manager system in an eCommerce platform I am building, after seeing how nopCommerce implemented one. The first task I built upon this was a thread which loops every 10 minutes and cleans out any inactive customer account sessions from the database.

The easiest way to do this would have been a simple SQL Server Stored Procedure and use SQL Agent to fire it every 10 minutes. The first problem with this is that I don’t necessary have access to full SQL Server and the express version doesn’t have SQL Agent. The second problem is down to management and deployment - I wanted a self contained application that doesn’t require the setting up of SQL Agent tasks and the maintainable of these outside of the main application.

So my dream...

  • I wanted a generic task system so multiple types of task thread can be programmed and controlled by the same system.
  • I wanted an area in the administration side of website that could see the state of the tasks, and a log of the task activities.
  • I wanted the ability to start and stop these tasks in the administration area.

The main layout of my solution is based on the nopCommerce implementation with some tweaks:

  • Create a TaskManager class using the Singleton pattern
  • Create a TaskThread class that will be inherited from with the different types of task threads required
  • On Application_Start create an instance of this class
  • With the initialization of this class it create instances of the task threads (inheriting from TaskThread) and adds them to a field in this single instance so they can be tracked and given start/stop commands
  • Application_Start then calls a Start function in the TaskManager which loops through each TaskThread and starts them
  • Application_End calls a Stop function in the TaskManager before calling dispose

One problem I had to get past was that the .NET platform might be ran on an IIS Web Farm scenario with multiple worker threads. This would in turn lead to double the amount of threads. To get around this I used a Mutex so that only one work thread can have the tasks running at a time. Here is the main code:

The TaskManager Class

Imports System.Threading

Namespace Tasks

    Public Class TaskManager

        Private Shared ReadOnly _TaskManager As New TaskManager()

        Private ReadOnly _TaskThreads As New List(Of TaskThread)

        Private Sub New()
        End Sub

        Public Sub Initialize()

            If Configuration.getBooleanSetting("Tasks.CleanOldSessions.Enabled", True) Then
                _TaskThreads.Add(New TaskCleanOldSessions())
            End If

        End Sub

        Public Sub StartTaskManager()

            ' Use a Mutex so only one instance of the task manager can be run at a time
            Dim createdNewMutex As Boolean = True
            Using mut As New Mutex(True, "makitTaskManager", createdNewMutex)

                If createdNewMutex Then

                    ' Start each thread created during initialize in turn
                    For Each taskThread As TaskThread In _TaskThreads
                        taskThread.InitTimer()
                    Next

                    ' Stop the garbage collecting the mutex, which would be bad
                    GC.KeepAlive(mut)

                End If

            End Using

        End Sub


        Public Sub StopTaskManager()

            Using mut As New Mutex(True, "makitTaskManager")

                For Each taskThread As TaskThread In _TaskThreads
                    taskThread.Dispose()
                Next

                ' The mutex can now be released, due to it not being used any more
                mut.ReleaseMutex()

            End Using

        End Sub


        Public Shared ReadOnly Property Instance() As TaskManager
            Get
                Return _TaskManager
            End Get
        End Property

    End Class

End Namespace

The TaskThread Class

Imports System.Threading

Namespace Tasks

    Public MustInherit Class TaskThread
        Implements IDisposable

        Protected _timer As Timer
        Protected _disposed As Boolean
        Protected _started As DateTime
        Protected _isRunning As Boolean

        Protected Sub New()
        End Sub

        Protected Overridable Sub Run()
            _started = DateTime.Now
            _isRunning = True

            ' Overriden class does the actual task here before setting isRunning to false

        End Sub


        Public Sub InitTimer()

            ' Create a timer which executes the timer handler sub between intervals
            If _timer Is Nothing Then
                _timer = New Timer(New TimerCallback(AddressOf TimerHandler), Nothing, Interval, Interval)
            End If

        End Sub


        Private Sub TimerHandler(ByVal state As Object)

            Try

                ' Stop the timer whilst the task runs, otherwise if the task is slow then will get an overlap
                _timer.Change(-1, -1)

                ' Run the actual task
                Run()

                ' Now reset the timer back to the interval again, so it will execute again in 10 minutes
                _timer.Change(Interval, Interval)

            Catch ex As Exception

                Logging.logAsError("Exception during execution of task. Caught error and task will be stopped until problem solved. Info: " & ex.ToString)

                _timer.Dispose()
                _timer = Nothing
                _disposed = True

            End Try

        End Sub

        Public ReadOnly Property Started() As DateTime
            Get
                Return _started
            End Get
        End Property

        Public ReadOnly Property IsRunning() As Boolean
            Get
                Return _isRunning
            End Get
        End Property

        Public ReadOnly Property Interval() As Integer
            Get
                Return 600000 '10 Minutes - could be configurable
            End Get
        End Property

        Public Sub Dispose() Implements IDisposable.Dispose
              If (_timer IsNot Nothing) AndAlso Not _disposed Then
                 SyncLock Me
                     _timer.Dispose()
                     _timer = Nothing
                     _disposed = True
                 End SyncLock
             End If
         End Sub
      End Class
  End Namespace

The actual example task thread class

Namespace Tasks

    Public Class TaskCleanOldSessions
        Inherits TaskThread

        Protected Overrides Sub Run()
            MyBase.Run()

            Execute()

            _isRunning = False

        End Sub

        Private Sub Execute()

            ' Get the number of hours in the past to delete sessions older than this
            Dim hoursOldToDelete As Single = Configuration.getSingleSetting("Tasks.CleanOldSessions.HoursOld", 48)

            ' Pass in the current date with the number of hours in the past removed so the sessions are deleted from the DB
            ' This returns a list of the session IDs that have been logged out for reporting purposes.
            Dim loggedOutSessions As List(Of String) = AccountRepository.clearOldAuths(Now().AddHours(-hoursOldToDelete))

            ' If some people were logged out then log the fact
            If loggedOutSessions IsNot Nothing Then

                Logging.logAsInfo("Logged out " & loggedOutSessions.Count & " sessions.")

                For Each curHUID As String In loggedOutSessions
                    Logging.logAsInfo("Logged out session " & curHUID)
                Next

            End If

            ' Make sure the memory is released
            If loggedOutSessions IsNot Nothing Then
                loggedOutSessions.Clear()
            End If
            loggedOutSessions = Nothing
            hoursOldToDelete = Nothing

            GC.Collect()

        End Sub

    End Class

End Namespace

That’s the basic premise, it has so far worked well. With the only major problem being a memory leak that was cured by implementing IDisposable in the threads and making sure all variables were cleared up before garbage collecting.

The threads are currently logging to text files, but they will be extended to link in with the admin area of the site so the history and status can be seen easily.

Posted by makit
Last revised 18 May 2012 12:13 PM

makit / Martyn Kilbryde

Professional software developer, ponderer and eccentric.

flickr

Github

LinkedIn

Stack Overflow

Twitter