Ir para o conteúdo

ThreadPool: Programação com Multi-Threading

Introdução

Para compreender as multi tarefas, irei comparar com uma actividade prática. Imagine que tem uma máquina de café e dispõem de duas pessoas, se a maquina consegue tirar até quatro cafés em simultâneo, e uma fila de 20 pessoas esperando por café.

A maquina poderá funcionar sem parar até que todas as pessoas sejam servidas. Então será óbvio que cada pessoa se encarregue de tirar dois cafés em simultâneo, o que torna um processo mais rápido do que se fosse apenas uma pessoa a servir. Deverá ter em atenção que cada pessoa não poderá tirar mais do que dois cafés de cada vez.

Programando com multi-thread, a situação fica similar, podemos desenvolver várias tarefas em simultâneo, no entanto, este processo terá de ser controlado dependendo de dois principais factores: número máximo de tarefas que podem ser executadas em simultâneo e o número total de tarefas.

Compreender a mitologia é essencial para entender o conceito de execução de tarefas com multi-threads.

Desenvolvimento

ThreadPool.vb
Esta classe foi desenvolvida com o objectivo de controlar o número total de threads em execução em simultâneo.

'========================================
'       Codificado por Carlos.DF
'           fLaSh - 2010-01
'         c4rl0s.pt@gmail.com
'   carlosferreiracarlos@hotmail.com
'========================================
Imports System.Threading
Public Class ThreadPool
    Inherits System.Object
    Implements IDisposable

    ''' <summary>
    ''' Numerador para o estado geral dos Trabalhos
    ''' </summary>
    ''' Unknown: este estado não será a partida utilizado, mas melhor trabalhar por o suguro
    ''' <remarks></remarks>
    Public Enum Status As Byte
        Unknown = 0
        Running = 1
        Paused = 2
        Canceled = 3
        Ending = 4
        Finished = 5
    End Enum

    ' Envento da classe
    Public Event OnFinished()

    ' Declarações locais
    Private __MaxThreads As Integer
    Private __RunningThreads As Integer
    Private __ActiveThreads As List(Of Thread)
    Private __Status As Status
    Private __AllPushed As Boolean
    Private __IsPaused As Boolean

    ''' <summary>
    ''' Construtor da classe
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New(ByVal MaxThread As Integer)
        If MaxThread <= 0 Then
            Throw New Exception("ThreadPool:New - O número maximo de Threads tem de ser superior a 0.")
        End If
        __MaxThreads = MaxThread
        __ActiveThreads = New List(Of Thread)
    End Sub

    ''' <summary>
    ''' Retorna o total de threads a correr em simultaneo..
    ''' </summary>
    ''' <remarks></remarks>
    Public ReadOnly Property RunningThreads() As Integer
        Get
            Return __RunningThreads
        End Get
    End Property

    ''' <summary>
    ''' Retorna o numero maximo de threads que podem correr em simultaneo..
    ''' </summary>
    ''' <remarks></remarks>
    Public ReadOnly Property MaxThread() As Integer
        Get
            Return __MaxThreads
        End Get
    End Property

    ''' <summary>
    ''' Retorna o estado actual da classe
    ''' </summary>
    ''' <remarks></remarks>
    Public ReadOnly Property ThreadsStatus() As Status
        Get
            Return __Status
        End Get
    End Property

    ''' <summary>
    ''' Simple metodo para pause ou resumir o trabalho..
    ''' </summary>
    ''' <remarks>No case de Pause=True, entrara em pause no proximo thread..</remarks>
    Public Property Pause() As Boolean
        Get
            Return __IsPaused
        End Get
        Set(ByVal value As Boolean)
            __IsPaused = value
        End Set
    End Property

    ''' <summary>
    ''' Abre novo thread e icrementa o total de threads activos
    ''' </summary>
    ''' <param name="oThread">Opcional o objecto do thread</param>
    ''' <remarks></remarks>
    Public Sub Open(Optional ByVal oThread As Thread = Nothing)
        SyncLock Me
            If __AllPushed Then Return
            Interlocked.Increment(__RunningThreads)
            If oThread IsNot Nothing Then __ActiveThreads.Add(oThread)
            __Status = Status.Running
            __IsPaused = False
        End SyncLock
    End Sub

    ''' <summary>
    ''' Fecha um thread e decrementa o total de threads activos
    ''' </summary>
    ''' <param name="oThread">Opcional o objecto do thread</param>
    ''' <remarks></remarks>
    Public Sub Close(Optional ByVal oThread As Thread = Nothing)
        SyncLock Me
            Interlocked.Decrement(__RunningThreads)
            If oThread IsNot Nothing Then __ActiveThreads.Remove(oThread)
            ' Defeni o status da classe
            If __AllPushed Then
                If __RunningThreads > 0 Then
                    __Status = Status.Ending
                Else
                    __Status = Status.Finished
                    ' Corre o envento final..
                    RaiseEvent OnFinished()
                End If
            End If
        End SyncLock
    End Sub

    ''' <summary>
    ''' Informa a Classe que não existe mais trabalhos em lista..
    ''' </summary>
    ''' <remarks>Este Sub é importante, terá de ser chamado no final do processo dos trabalhos,
    ''' para dar a aconhecer que terminou os trabalhos..</remarks>
    Public Sub AllJobsPushed()
        SyncLock Me
            Me.__AllPushed = True
            Me.__IsPaused = False
        End SyncLock
    End Sub

    ''' <summary>
    ''' Termina tudo o processo..
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Abort()
        SyncLock Me
            __Status = Status.Canceled
            Me.AllJobsPushed()
            For Each oThread As Thread In __ActiveThreads.ToArray()
                Try
                    Me.Close(oThread)
                    oThread.Abort()
                    'Interlocked.Decrement(__RunningThreads)
                Catch ex As Exception
                    Debug.WriteLine("Erro ao cancelar um thread: " & ex.Message)
                End Try
            Next
        End SyncLock
    End Sub

    ''' <summary>
    ''' Espera por o thread para proximo trabalho, caso ja esteja o numero maximo de threads em simultaneo..
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub WaitForThreads()
        'Cria um cliclo infinito..
        Do
            ' Sincroniza a classe
            SyncLock Me
                ' Verifica uma das duas opções..
                Select Case True
                    '     Simples, caso os threads a correr seja maior que o numero maximo de threads, 
                    '    então termina o ciclo para dar lugar ao inicio de novo thread, ou no caso de o processo
                    '    estar a terminar 'Ending' ou 'Finished'..
                    Case ((__RunningThreads < __MaxThreads) Or _
                         ((__Status = Status.Ending) Or (__Status = Status.Finished)) Or (__Status = Status.Canceled)) And _
                         (Not __IsPaused)
                        Exit Do
                    Case Else
                        ' Ou então, pausa o processo e continua o ciclo infinito..
                        Threading.Thread.Sleep(100)
                End Select
            End SyncLock
        Loop
    End Sub

    ''' <summary>
    ''' Espera que todos os thread activos terminem o seu trabalho..
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub WaitForThreadsDone()
        'Cria um cliclo infinito..
        Do
            ' Sincroniza a classe
            SyncLock Me
                ' Verifica uma das duas opções..
                Select Case True
                    '     Simples, caso os threads a correr seja maior que o numero maximo de threads, 
                    '    então termina o ciclo para dar lugar ao inicio de novo thread..
                    Case (__RunningThreads = 0) Or (__Status = Status.Finished) And _
                         (Not __IsPaused)
                        Exit Do
                    Case Else
                        ' Ou então, pausa o processo e continua o ciclo infinito..
                        Threading.Thread.Sleep(100)
                End Select
            End SyncLock
        Loop
    End Sub

#Region " Suporte para a Interface IDisposable "

    Private __IsDisposed As Boolean

    Protected Overridable Sub Dispose(ByVal bDisposing As Boolean)
        If Not Me.__IsDisposed Then
            If bDisposing Then
                If Not Me.__Status = Status.Finished Then
                    Me.Abort()
                End If
            End If
        End If
        Me.__IsDisposed = True
    End Sub

    ''' <summary>
    ''' Este código foi adicionado pelo Visual Basic para implementar corretamente o padrão 'Dispose'.
    ''' </summary>
    ''' <remarks>Não altere este código. Coloque no código do Sub Dispose (ByVal bDisposing As Boolean) acima.</remarks>
    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

#End Region

End Class

Exemplo prático

Crie um novo projecto do tipo Console Application, adicione a classe ThreadPool.vb indicada a cima, e coloque este código no modulo que o VS irá adicionar.

Imports System.Threading
Module mMain

    ' Objecto para o controle dos threads em execução em simultaneo..
    Private __ThreadPool As ThreadPool

    Public Sub Main()

        System.Console.WriteLine("Prima qualquer tecla para inicar o teste.")
        System.Console.ReadKey()

        'Inicia o teste
        Call IniciaTrabalho()

        System.Console.WriteLine("Prima qualquer tecla para sair.")
        System.Console.ReadKey()

    End Sub

    Private Sub IniciaTrabalho()

        Const TOTAL_TAREFAS As Integer = 100
        Const MAX_THREADS As Integer = 20

        ' Escreve na janela 
        System.Console.WriteLine(New String("*", 80))
        System.Console.WriteLine( _
                String.Format("**Lista de trabalhos iniciada, Max. threads em simultaneo: {0}, total de tarefas {1}", MAX_THREADS, TOTAL_TAREFAS) _
        )

        ' Inicia a instancia da classe para o controle dos threads em execução em simultaneo..
        __ThreadPool = New ThreadPool(MAX_THREADS)

        ' Percorre todas as listas de tarefeas..
        For iLoop As Integer = 1 To TOTAL_TAREFAS

            ' Inicia novo thread..
            Dim oThread As New Thread(AddressOf FazTarefa)
            oThread.IsBackground = True
            oThread.Name = "Pos: " & iLoop.ToString

            ' Cria a instancia da classe usada por o thread..
            Dim oJob As New ThreadObj(oThread, iLoop)

            ' Inicia thread..
            oThread.Start(oJob)

            ' Abre o Thread..
            __ThreadPool.Open(oThread)

            ' Aguarda por uma slot livre.. caso necessario..
            __ThreadPool.WaitForThreads()

        Next

        ' Inidica que todos os 'trabalhos' foram completos..
        __ThreadPool.AllJobsPushed()

        ' Espera que o threads activos terminem o seu trabalho..
        'Aqui poderia utilizar desta forma.. mas como prentende saber os threads que faltam terminar irei usar outro metodo..
        '__ThreadPool.WaitForThreadsDone()

        ' No entanto, para perceber melhor o seu conseito, irei controlar manualmente os threads que ainda
        'não terminaram a sua tarefa..
        Do
            ' Reporta o progresso
            Threading.Thread.Sleep(1000)
            ' Escreve na janela Debug do VS
            System.Console.WriteLine( _
                    String.Format("A terminar os trabalhos, threads activos: {0}", __ThreadPool.RunningThreads) _
            )
        Loop While __ThreadPool.RunningThreads > 0

        ' Escreve na janela 
        System.Console.WriteLine(New String("*", 80))
        System.Console.WriteLine( _
                 "**Lista de trabalhos concluida" _
        )

    End Sub

    Private Sub FazTarefa(ByVal t As ThreadObj)

        ' Cria uma pause a aleatoriamente ( 1 ~ 5 segundo )
        Dim oRandom As New Random()
        Dim iSleep As Integer = oRandom.Next(1000, 5000)

        ' Faz a pause
        Thread.Sleep(iSleep)

        ' Fecha o thread (IMPORTANTE)
        __ThreadPool.Close(t.Thread)

        ' Escreve na janela Debug do VS
        System.Console.WriteLine( _
                String.Format(" Trabalho {0} terminou tarefa, total threads em execução {1}", t.ID, __ThreadPool.RunningThreads) _
        )



    End Sub

    ''' <summary>
    ''' Esta classe serve como contentor de codigo do objecto da tarefa
    ''' Deverá a estender como pretender..
    ''' </summary>
    ''' <remarks></remarks>
    Private Class ThreadObj

        Private __Thread As Threading.Thread
        Private __ID As Integer

        Public Sub New(ByVal t As Threading.Thread, ByVal id As Integer)
            __Thread = t
            __ID = id
        End Sub

        Public ReadOnly Property Thread() As Threading.Thread
            Get
                Return __Thread
            End Get
        End Property

        Public ReadOnly Property ID() As Integer
            Get
                Return __ID
            End Get
        End Property

    End Class

End Module

Conclusão

O conceito multi-tread é sem dúvida o futuro do desenvolvimento de aplicações, actualmente os processadores actuais são constituídos com mais do que um core, assim podemos tirar o máximo proveito dos mesmos, criando aplicações mais rápidas e com mais performance!