Multiple Threads in ASP.NET (VB)
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.


Comments
No comments yet. Be the first!