Introduction
We have to admit, multithreading is hard to understand. On the other hand, if you want to do more in less time, MT is the way to go. I would like to present a concept/pattern I developed to make MT easier in some scenarios.
Background
Under the introduction to CQRS in our company I wanted to fill all dependent properties on a DTO in parallel.
Using the Code
The 'old way' of doing this in a sequential manner would be something like this, very simplified of course
Public Class Person
Public Property Id As Integer
Public Property Name As String
Public Property Email As IEnumerable(Of EMail)
Public Property Address As IEnumerable(Of Address)
End Class
Public Class EMail
Public Property Id As Integer
Public Property PersonId As Integer
Public Property Adress As String
End Class
Public Class Address
Public Property Id As Integer
Public Property PersonId As Integer
Public Property Address As String
End Class
Public Class GetPersonInfo
Public ConnectionInfo As New ServerConnectionInfo With {.Address = "sql", .Database = "person", .UserName = "sa", .Password = "*******"}
Public shared Sub FillPerson(id as integer,byref p as person)
Dim cmd As New CommandInfo
cmd.Parameters.Add("Id", DbType.Int32, Query.Id)
cmd.CommandText = "Select * from Person where Id = @Id "
Data.Exec(ConnectionInfo, cmd, Result)
End Sub
Public Shared Sub FillEmail(id as integer,byref l as iEnumerable(of Email))
Dim list As New List(Of EMail)
Dim cmd As New CommandInfo
cmd.Parameters.Add("Id", DbType.Int32, Query.Id)
cmd.CommandText = "Select * from HrElectronicAddress where unitId = @Id "
Data.Exec(ConnectionInfo, cmd, list)
l = list
End Sub
Public Shared Sub FillAddress()
Dim list As New List(Of Addresss)
Dim cmd As New CommandInfo
cmd.Parameters.Add("Id", DbType.Int32, Query.Id)
cmd.CommandText = "Select * from HrAddress where unitId = @Id "
Data.Exec(ConnectionInfo, cmd, list)
l = list
End Sub
End Class
Dim p as new Person
dim Id as integer = 1
GetPersonInfo.FillPerson(Id,p))
GetPersonInfo.FillEmail(Id,p.Email))
GetPersonInfo.FillAddress(Id,p.Address))
My goal was to re-use much of the same logic for filling, but be able to do it in parallel.
First of all, we have to do some wrapping of the functions.
This is just an Interface and a baseclass to be able to identify the multithreaded queryhandlers.
Public Interface IParalellQuery
ReadOnly Property InnerResult As Object
Property InnerQuery As Object
End Interface
Public MustInherit Class ParalellQueryBase(Of TQuery As IAmAQuery, TResult As New)
Implements IParalellQuery
Public Query As TQuery
Public Result As TResult
Public Sub New()
Result = New TResult
End Sub
Friend Property InnerQuery As Object Implements IParalellQuery.InnerQuery
Set(value As Object)
Query = CType(value, TQuery)
End Set
Get
Return Query
End Get
End Property
Public ReadOnly Property InnerResult As Object Implements IParalellQuery.InnerResult
Get
Return Result
End Get
End Property
End Class
Public Class GetPersonInfoQuery
Inherits QueryBase
Public Property Id As Integer
Public Overrides ReadOnly Property Name As String
Get
Return "GetPersonInfo"
End Get
End Property
End Class
The same as above but prepared for multithreading..
Public Class GetPersonInfoQueryHandler
Inherits ParalellQueryBase(Of GetPersonInfoQuery, Person)
Public ConnectionInfo As New ServerConnectionInfo With {.Address = "sql", .Database = "person", .UserName = "sa", .Password = "*******"}
Public Sub FillPerson()
Dim cmd As New CommandInfo
cmd.Parameters.Add("Id", DbType.Int32, Query.Id)
cmd.CommandText = "Select * from Person where Id = @Id "
Data.Exec(ConnectionInfo, cmd, Result)
End Sub
Public Sub FillEmail()
Dim list As New List(Of EMail)
Dim cmd As New CommandInfo
cmd.Parameters.Add("Id", DbType.Int32, Query.Id)
cmd.CommandText = "Select * from HrElectronicAddress where unitId = @Id "
Data.Exec(ConnectionInfo, cmd, list)
Result.Email = list
End Sub
Public Sub FillAddress()
Dim list As New List(Of Addresss)
Dim cmd As New CommandInfo
cmd.Parameters.Add("Id", DbType.Int32, Query.Id)
cmd.CommandText = "Select * from HrAddress where unitId = @Id "
Data.Exec(ConnectionInfo, cmd, list)
Result.Addresss = list
End Sub
End Class
The Result property used, is from the baseclass, and is of the correct type thanks to the generic type parameter.
The parameters needed inside the Sub's is passed through the GetPersoninfoQuery
class. In our case, containing just the Id of the person.
How to invoke this?
Dim res As Person
Dim q As New GetPersonInfoQuery
q.Id = 1
res = q.Execute(Of Person)()
Here is the main code for calling the subs on the handler.
If MultiHandlers.ContainsKey(q.GetType) Then
Dim handler = MultiHandlers(q.GetType)
Dim target As Query.IParalellQuery = CType(handler.CreateInstance, IParalellQuery)
target.InnerQuery = q
handler.Methods.AsParallel.ForAll(Sub(m)
m.Invoke(target, {})
End Sub)
Return target.InnerResult
End If
Some explanations needed here.
All classes implementing the IMultiquery interface is found in the application and there the system builds and index to know which class handles which Query. The target
her is an instace of the GetPersonInfoQueryHandler.
Alle public sub's on the handler
type is extracted using reflection and inserted into the Methods
property of the handler
, then using the AsParallel.ForAll
extension method to invoke all sub on the target.
Points of Interest
Did this actually improve anything? Well the speed is better.
<Test> Public Sub SpeedTest()
Dim tinfo As New Utils.TimingInfos
Dim res As Person
Dim q As New GetPersonInfoQuery
q.Id = 1
res = CType(Query.Handling.ExecuteQuery(q), Person)
Using New Utils.InlineTimer("Paralell", tinfo)
For x = 0 To 100
res = CType(Query.Handling.ExecuteQuery(q), Person)
Next
End Using
Using New Utils.InlineTimer("Seq", tinfo)
For x = 0 To 100
Dim t As New GetPersonInfoQueryHandler
t.Query = q
res = t.Result
t.FillEmail()
t.FillPerson()
t.FillAddress()
Next
End Using
Debug.Print(tinfo("Paralell").List.Sum().ToString)
Debug.Print(tinfo("Seq").List.Sum().ToString)
End Sub
There is of course some time overhead when doing this, but the overhead is eaten up when in my case 3 sub/functions is called. The timesavings will be greater the longer each sub takes.
I belive this approach is a way to abstract some of the difficulties of multithreading in some given scenarios.
Any comments to this approach is very welcome.