How to launch several operations side-by-side in .net?
Asked Answered
P

4

5

I have an application that takes too long to run, and I want to introduce threading / parallelization / whatever.

Specifically, the code retreives several thousand mails, then sends them. Today, the code looks like this (a bit simplified) :

Dim mails = centreInteretService.GetEmails()
For Each m in mails
    m.Body = GetMailContent(m)
    If MailSendable(m) Then
        SendMail(m)
    End If
Next

I want to try sending multiple mails in parallel. I would like to try with 2 threads in parallel. More specifically, I would want to put the whole loop in a thread (getmailcontent + sendmail).

I thought of something like this :

Dim mails1 As New List(Of MailSerialiserCI)
Dim mails2 As New List(Of MailSerialiserCI)
Dim nbFirstList As Integer = CInt(Math.Ceiling(nbTotal / 2))
mails1 = mails.Take(nbFirstList)
mails2 = mails.Skip(nbFirstList)

Dim smt1 As New MailSender.MailSenderThreaded()
smt1.mails = mails1
smt1.nbTotal = nbTotal
Dim threadMails1 As ThreadStart = New ThreadStart(AddressOf smt1.SendMails)
Dim th1 As Thread = New Thread(AddressOf threadMails1)
th1.Start()

Dim smt2 As New MailSender.MailSenderThreaded()
smt2.mails = mails2
smt2.nbTotal = nbTotal
Dim threadMails2 As ThreadStart = New ThreadStart(AddressOf smt2.SendMails)
Dim th2 As Thread = New Thread(AddressOf threadMails2)
th2.Start()

And MailSenderThreaded is like this :

Public Class MailSenderThreaded
    Public mails As List(Of MailSerialiserCI)
    Public nbTotal As Integer
    Public Sub SendMails()
        LoopMails(Me.mails, Me.nbTotal)
    End Sub
End Class

But the lines with New Thread(AdressOf x) give me an error : no applicable function x matching delegate System.Threading.ParameterizedThreadStart.

I tried searching here and there, but I can only find either solutions that require a lot more knowledge than what I have ; or threading basics ; or .NET 4 stuff, but we are still in .NET 3.5...

Do you have a simple solution that I could try ?

Thanks

Preteritive answered 2/1, 2012 at 11:57 Comment(0)
K
4

If the body of your loop is thread-safe, you could just use Parallel.ForEach

In C#, it would look like this:

var mails = centreInteretService.GetEmails();

Parallel.ForEach( mails, new ParallelOptions { MaxDegreeOfParallelism = 2 }, m =>
    {
        m.Body = GetMailContent(m);
        if ( MailSendable(m) ) SendMail(m);
    }
);

EDIT: .NET 3.5!

I think this is about the simplest solution in .NET 3.5:

( Sorry it's in C# - I don't know VB. I hope you can read it. )

...
List<Mail> mails = centreInteretService.GetEmails();
var mailer = new Mailer( mails );
mailer.Run();
...

public class Mailer
{
    const int THREAD_COUNT = 2;
    List<Thread> _Threads = new List<Thread>();

    List<Mail> _List = null;
    int _Index = -1;

    public Mailer( List<Mail> list )
    {
        _List = list;
    }

    public void Run()
    {
        for ( int i = 0 ; i < THREAD_COUNT ; i++ )
        {
            _Threads.Add( StartThread() );
        }

        foreach ( var thread in _Threads ) thread.Join();
    }

    Thread StartThread()
    {
        var t = new Thread( ThreadMain );
        t.Start();
        return t;
    }

    void ThreadMain()
    {
        for ( ; ; )
        {
            int index = Interlocked.Increment( ref _Index );
            if ( index >= _List.Count ) return;
            ThreadWork( _List[ index ] );
        }
    }

    void ThreadWork( Mail mail )
    {
        mail.Body = GetMailContent(mail);
        if ( MailSendable(mail) ) SendMail(mail);
    }
}
Kella answered 2/1, 2012 at 12:0 Comment(2)
Looks interesting, but only available since .net 4, and we are still in .net 3.5...Preteritive
Update: added solution for .NET 3.5Kella
C
4

Have you tried this?

Dim mails = centreInteretService.GetEmails()
For Each m in mails.ASParallel()
    m.Body = GetMailContent(m)
    If MailSendable(m) Then
        SendMail(m)
    End If
Next

This will use 1 thread for each core in the computer. If you want to use only 2, then you can do:

Dim mails = centreInteretService.GetEmails()
For Each m in mails.AsParallel().WithDegreeOfParallelism(2)
    m.Body = GetMailContent(m)
    If MailSendable(m) Then
        SendMail(m)
    End If
Next

EDIT: As you're limited to .Net 3.5, I recomend you the method used by Rob Volk in this post of his blog. I used it two years ago with no problems. It's in C#, so you'll need to translate it (no more than 10 lines of code).

Capon answered 2/1, 2012 at 12:0 Comment(2)
AsParallel will only query the enumerable in parallel, the actual work will be performed sequentially. Consider using Parallel class for parallelising workload.Battledore
Even with the limitation @Battledore mentioned, it's only available in .net 4, and we are limited to .net 3.5...Preteritive
K
4

If the body of your loop is thread-safe, you could just use Parallel.ForEach

In C#, it would look like this:

var mails = centreInteretService.GetEmails();

Parallel.ForEach( mails, new ParallelOptions { MaxDegreeOfParallelism = 2 }, m =>
    {
        m.Body = GetMailContent(m);
        if ( MailSendable(m) ) SendMail(m);
    }
);

EDIT: .NET 3.5!

I think this is about the simplest solution in .NET 3.5:

( Sorry it's in C# - I don't know VB. I hope you can read it. )

...
List<Mail> mails = centreInteretService.GetEmails();
var mailer = new Mailer( mails );
mailer.Run();
...

public class Mailer
{
    const int THREAD_COUNT = 2;
    List<Thread> _Threads = new List<Thread>();

    List<Mail> _List = null;
    int _Index = -1;

    public Mailer( List<Mail> list )
    {
        _List = list;
    }

    public void Run()
    {
        for ( int i = 0 ; i < THREAD_COUNT ; i++ )
        {
            _Threads.Add( StartThread() );
        }

        foreach ( var thread in _Threads ) thread.Join();
    }

    Thread StartThread()
    {
        var t = new Thread( ThreadMain );
        t.Start();
        return t;
    }

    void ThreadMain()
    {
        for ( ; ; )
        {
            int index = Interlocked.Increment( ref _Index );
            if ( index >= _List.Count ) return;
            ThreadWork( _List[ index ] );
        }
    }

    void ThreadWork( Mail mail )
    {
        mail.Body = GetMailContent(mail);
        if ( MailSendable(mail) ) SendMail(mail);
    }
}
Kella answered 2/1, 2012 at 12:0 Comment(2)
Looks interesting, but only available since .net 4, and we are still in .net 3.5...Preteritive
Update: added solution for .NET 3.5Kella
B
1

As you mentioned that both GetMailContent and Send take time and that you are limited to .NET 3.5, you could try implementing your own Producer-Consumer concurrent pattern.

Pull-based approach

GetMailContent works in a separate thread, once 1 mail content retrieved, it puts the object into your custom producer queue. Send works, is in its own thread, and constantly queries producer queue for a new item. Once available it dequeues it and sends away.

Push-based approach

GetMailContent works in separate thread and constructs the object. Once done with one, it notifies the Send method, which works in another thread, of a new item to be send. This is a traditional Observer pattern.

All this will require good synchronisation. You should be able to find/implement a non-blocking synchronisation which is usually faster than alternative blocking ones.

Battledore answered 2/1, 2012 at 12:50 Comment(1)
Thanks, but isn't there another, simpler way ? I'll amend my question.Preteritive
R
0

To use thread, you need to be sure about which part of the process is to be put in thread. As proposed by you, to put the SendMail(m) in a thread, you need to be sure that this will effectively better the performance. If this is the only part that takes up most of the time, you can put this method in a thread. Or simply put the loop as a parelle loop. See http://msdn.microsoft.com/en-us/library/system.threading.tasks.parallel.foreach.aspx

Rifling answered 2/1, 2012 at 12:6 Comment(3)
No, actually, it's the GetMailContent that takes some time, so I thought to put both the operations (getcontent and send) in a thread.Preteritive
From your code, I am not seeing any use of GetMailContent. Are you sure, you are using it and possibly not shown the same here?Rifling
Yes, GetMailContent calls a page that takes a few seconds to load ; it's what retreives the mail body, so I'm sure I call it. I edited my question to add the MailSenderThreaded class.Preteritive

© 2022 - 2024 — McMap. All rights reserved.