Threads med Borland C++ Builder (ver. 3)

3. marts 1999

Eksempel 1. Start og stop af threads

Formålet med dette eksempel er at vise, dels hvordan man definerer threads i et program, dels hvordan man aktiverer og stopper threads.

Først dannes et hovedprogram (main thread) på sædvanlig vis. Lav f.eks. et skærmbillede med to knapper, som man kan bruge til at aktivere og stoppe sin thread med:

Dernæst skal man have lavet sin thread. Det gøres ved i menuen at vælge File | New og derefter vælge Thread object (begrebet thread object er ulogisk, da man faktisk opretter en thread class). Man bliver bedt om at give den nye klasse et navn, hvorefter der dannes et programmodul (unit) med følgende indhold i cpp-filen (klassen har her fået navnet TExempel1Thread):

#include "Unit2.h"

#pragma package(smart_init)

//---------------------------------------------------------------------------

// Important: Methods and properties of objects in VCL can only be

// used in a method called using Synchronize, for example:

//

// Synchronize(UpdateCaption);

//

// where UpdateCaption could look like:

//

// void __fastcall TExempel1Thread::UpdateCaption()

// {

// Form1->Caption = "Updated in a thread";

// }

//---------------------------------------------------------------------------

__fastcall TExempel1Thread::TExempel1Thread(bool CreateSuspended)

: TThread(CreateSuspended)

{

}

//---------------------------------------------------------------------------

void __fastcall TExempel1Thread::Execute()

{

//---- Place thread code here ----

}

//---------------------------------------------------------------------------

Man har nu til opgave at udfylde disse rammer med den funktionalitet, som man ønsker sig af sin thread. I dette tilfælde vil vi blot lade tråden melde sig, når den starter. Samtidig nulstilles et tælleværk, som derefter inkrementeres indtil tråden stoppes, hvorefter tællerens aktuelle værdi udskrives. Dette kan f.eks. gøres således:

__fastcall TExempel1Thread::TExempel1Thread(bool CreateSuspended)

: TThread(CreateSuspended)

{

Priority = tpNormal;

FreeOnTerminate = false;

}

Egenskaben Priority tildeles en værdi svarende til den prioritet, man ønsker at tildele tråden. Denne prioritet bestemmer bl.a. hvor meget processortid tråden får tildelt.

Egenskaben FreeOnTerminate bruges til at styre, om programkoden skal slettes fra lageret, når tråden er termineret.

//---------------------------------------------------------------------------

void __fastcall TExempel1Thread::Execute()

{

long i = 0;

ShowMessage("Thread activated.");

while (!Terminated)

{ i++; }

ShowMessage(i);

return;

}

Egenskaben Terminated kan sættes af andre tråde (primært af programmets main thread) ved kald af metoden Terminate. På den måde kan andre tråde bede denne tråd om at stoppe; men det er op til programmøren af denne tråd at indlægge en test af, om egenskaben Terminated er blevet sat. I eksemplet her bruges testen som slutbetingelse for loopet.

I programmets main thread skal der indsættes en erklæring af et objekt af typen TEksempel1Thread, som man nu har defineret. Det kan gøres således:

TExempel1Thread * proces2 = new TExempel1Thread(true);

Parameteren til constructoren er sat til true, hvilket betyder at aktiveringen af processen først sker, når man beder om det ved kald af metoden Resume. Ellers begynder processen at køre straks når objektet er erklæret.

Tråden, som nu har fået navnet proces2 aktiveres ved tryk på knappen Start thread. Selve aktiveringen sker ved at kalde metoden Resume:

void __fastcall TForm1::Button1Click(TObject *Sender)

{

proces2->Resume();

}

Tilsvarende afsluttes tråden ved tryk på knappen Stop thread:

void __fastcall TForm1::Button2Click(TObject *Sender)

{

proces2->Terminate();

}

 

Eksempel 2. Producer og consumer med TCriticalSection

I dette eksempel skal der dannes to tråde (threads), producer og consumer. Tråden producer får til opgave at indlæse tekster én ad gangen, som skal gemmes i en global variabel med navnet Buffer. Tråden consumer får til opgave at hente indholdet af den globale variabel Buffer, og udskrive det på skærmen - i dette konkrete tilfælde i en ListBox på det skærmbillede, der hører til programmets main thread. Skærmbilledet er designet således:

Koden for den tilhørende main thread kommer til at se således ud:

//---------------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

#include "Unit1.h"

#include "Unit2.h"

#include "Unit3.h"

#include <syncobjs.hpp>

//---------------------------------------------------------------------------

#pragma package(smart_init)

#pragma resource "*.dfm"

AnsiString Buffer;

int Count = 0;

bool Status = true;

TCriticalSection *Lock = new TCriticalSection;

TForm1 *Form1;

TThreadProducer * Producer = new TThreadProducer(false);

TThreadConsumer * Consumer = new TThreadConsumer(false);

//---------------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

}

//---------------------------------------------------------------------------

I forhold til det første eksempel er der nogle forskelle. For det første skal der erklæres nogle globale variabler, som skal være tilgængelige fra de to tråde producer og consumer. Det er igennem disse globale variabler, at de to tråde skal udveksle data.

Variablen Buffer skal indeholde den tekst, som indlæses af tråden producer. Variablen Count bruges til at signalere, om Buffer er tom (0) eller fuld (1). Variablen Status bruges til at signalere, at tråden Producer er termineret.

Fælles for de tre variabler er, at der ikke må være tilgang til dem fra begge tråde samtidig, og derfor må adgangen til dem kontrolleres. I dette eksempel kontrolleres adgangen ved hjælp af et objekt af typen TcriticalSection. Denne klasse kræver inklusion af modulet syncobjs.

Producer

Det kræver meget omtanke at skabe de algoritmer, der skal udgøre de to tråde. Problemet er, at man hele tiden skal tage hensyn til den aktuelle status for den anden tråd. I dette tilfælde er problemerne især koncentreret om adgangen til variablen Buffer. Når producer tråden har indlæst en tekst, skal det undersøges om Buffer er tom. Hvis Buffer er tom, gemmes den indlæste tekst, ellers må producer tråden vente på at Buffer bliver tom.

//---------------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

#include "Unit2.h"

#include <syncobjs.hpp>

#pragma package(smart_init)

extern AnsiString Buffer;

extern int Count;

extern bool Status;

extern TCriticalSection *Lock;

__fastcall TThreadProducer::TThreadProducer(bool CreateSuspended)

: TThread(CreateSuspended)

{

Priority = tpNormal;

}

//---------------------------------------------------------------------------

void __fastcall TThreadProducer::Execute()

{

AnsiString Input;

bool Result;

int bufferCount;

Result = InputQuery("Producer", "Indtast en tekst", Input);

while (Result)

{

do

{

Lock->Acquire();

bufferCount = Count;

if (bufferCount == 0)

{

Count++;

Buffer = Input;

}

Lock->Release();

} while (bufferCount > 0);

Input = "";

Result = InputQuery("Producer", "Indtast en tekst", Input);

}

Lock->Acquire();

Status = false;

Lock->Release();

return;

}

//---------------------------------------------------------------------------

Algoritmens yderste loop gentager behandlingen indtil indlæsningen afbrydes af operatøren ude ved skærmen. Det inderste loop har til formål at få tråden til at vente indtil Buffer er tom.

Consumer

I denne tråd er der medtaget et eksempel på synkronisering, som er nødvendig, når man fra en tråd vil referere til elementer i den grafiske grænseflade, der tilhører main thread.

//---------------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

#include "Unit3.h"

#include "Unit1.h"

#include <syncobjs.hpp>

#pragma package(smart_init)

extern AnsiString Buffer;

extern int Count;

extern bool Status;

extern TCriticalSection *Lock;

__fastcall TThreadConsumer::TThreadConsumer(bool CreateSuspended)

: TThread(CreateSuspended)

{

Continue = true;

}

//---------------------------------------------------------------------------

void __fastcall TThreadConsumer::AddToList()

{

Form1->ListBox1->Items->Add(Output);

}

void __fastcall TThreadConsumer::Execute()

{

do

{

do

{

Lock->Acquire();

Continue = Status;

bufferCount = Count;

if (bufferCount > 0)

{

Output = Buffer;

Count--;

}

Lock->Release();

if (bufferCount > 0)

Synchronize(AddToList);

} while (Continue && bufferCount == 0);

} while (Continue);

return;

}

Algoritmen er også i dette tilfældet opbygget af to loops. Det yderste loop har til formål at gentage behandlingen indtil den anden tråd har termineret. Denne tilstand signaleres via variablen Status.

 

Eksempel 3. Producer og consumer med TEvent

Dette eksempel løser samme opgave som eksempel 2; men løsningen er baseret på anvendelsen af objekter af typen TEvent i stedet for TCriticalSection. Ideen bag anvendelsen af event objekter er at lade de forskellige processer bruge dem til at signalere, når forskellige hændelser er indtruffet.

I det konkrete eksempel kan man lade producer tråden signalere, når Buffer er fuld. Tilsvarende kan man lade consumer tråden signalere, når Buffer er tom. Det er nødvendigt med to event objekter, fordi det er selve signaleringen, der er interessant i forhold til programlogikken. Det er nemlig muligt at lade en tråd vente på, at signalet bliver sat af en anden tråd.

Main thread

//---------------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

#include "Unit1.h"

#include "Unit2.h"

#include "Unit3.h"

#include <syncobjs.hpp>

//---------------------------------------------------------------------------

#pragma package(smart_init)

#pragma resource "*.dfm"

AnsiString Buffer;

int Count = 0;

TEvent *Full = new TEvent(NULL, true, false, "Full");

TEvent *Empty = new TEvent(NULL, true, true, "Empty");

TForm1 *Form1;

TThreadProducer * Producer = new TThreadProducer(true);

TThreadConsumer * Consumer = new TThreadConsumer(true);

//---------------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

Producer->Resume();

Consumer->Resume();

}

//---------------------------------------------------------------------------

Variablen Count spiller en rolle, når man skal afslutte programmet og dermed også consumer tråden. For at aktivere consumer tråden er man nødt til at signalere, at Buffer er fuld, selvom det ikke er tilfældet, når tråden skal afsluttes.

De to event objekter defineres i main thread, fordi de skal være globale i forhold til de øvrige tråde. Constructoren til event objekterne har 4 parametre:

  1. EventAttributes specificerer objektets egenskaber. NULL opretter et objekt med et sæt default egenskaber.
  2. ManualReset specificerer, om event objektets signal udelukkende skal kunne sættes explicit, eller om det også kan ske automatisk. Når parameteren er true, er signalet sat indtil man slukker det med ResetEvent metoden.
  3. InitialState angiver om event objektets signal skal være tændt eller slukket efter initialiseringen.
  4. Name parameteren kan bruges til at knytte event objektet til et eksisterende event objekt.

 

 

Producer

//---------------------------------------------------------------------------#include <vcl.h>

#pragma hdrstop

#include "Unit2.h"

#include "Unit3.h"

#include <syncobjs.hpp>

#pragma package(smart_init)

extern AnsiString Buffer;

extern int Count;

extern TEvent *Full;

extern TEvent *Empty;

extern TThreadConsumer * Consumer;

__fastcall TThreadProducer::TThreadProducer(bool CreateSuspended)

: TThread(CreateSuspended)

{

Priority = tpNormal;

}

//---------------------------------------------------------------------------

void __fastcall TThreadProducer::Execute()

{

AnsiString Input;

bool Result;

Result = InputQuery("Producer", "Indtast en tekst", Input);

while (Result)

{

if (Empty->WaitFor(INFINITE) == wrSignaled)

{

Empty->ResetEvent();

Count++;

Buffer = Input;

Full->SetEvent();

}

Input = "";

Result = InputQuery("Producer", "Indtast en tekst", Input);

}

Full->SetEvent();

return;

}

//---------------------------------------------------------------------------

Koden i såvel producer som consumer tråden er enklere, når der anvendes event objekter til synkronisering.

Loopet i producer tråden gentages indtil indlæsningen stoppes af operatøren ved skærmen. Tråden bliver sat til at vente (uendeligt længe) på at Empty signalet bliver slået til. Når det sker, slår man Empty signalet fra, tæller Count op, flytter data til Buffer og sætter signalet Full, som samtidig er signalet til Consumer tråden om, at der nu er data at behandle i Buffer.

Som afslutning på tråden bliver Full signalet sat, uden at Count bliver talt op. Det er den metode, der er valgt til at kommunikere til consumer tråden, at der ikke er mere data til behandling.

 

Consumer

//---------------------------------------------------------------------------#include <vcl.h>#pragma hdrstop#include "Unit3.h"#include "Unit1.h"#include <syncobjs.hpp>#pragma package(smart_init)

extern AnsiString Buffer;

extern int Count;

extern TEvent *Full;

extern TEvent *Empty;

__fastcall TThreadConsumer::TThreadConsumer(bool CreateSuspended)

: TThread(CreateSuspended)

{

}

//---------------------------------------------------------------------------

void __fastcall TThreadConsumer::AddToList()

{

Form1->ListBox1->Items->Add(Output);

}

void __fastcall TThreadConsumer::Execute()

{

while (!Terminated)

{

if (Full->WaitFor(INFINITE) == wrSignaled)

{

if (Count > 0)

{

Full->ResetEvent();

Output = Buffer;

Count--;

Empty->SetEvent();

Synchronize(AddToList);

}

else

Terminate();

}

}

return;

}

//---------------------------------------------------------------------------

Behandlingen i consumer tråden gentages indtil tråden modtager Full signaleret med 0 i Count. Ellers hentes data ud fra bufferen, Count tælles ned og Empty signalet sættes som besked til producer tråden om, at der igen er fri bane til Buffer.

 

Eksempel 4. Anvendelse af kø som buffer

Formålet med dette eksempel er at vise, hvordan man som buffer kan anvende en datastruktur med plads til flere elementer. I dette tilfælde er der valgt datastrukturen kø som buffer. Denne datastruktur er i denne sammenhæng meget velegnet på grund af, at elementerne behandles first-in first-out. For at give producer tråden en chance for at bygge nogle linier op på forhånd, er skærmbilledet udvidet med en knap, som man skal trykke på, når man vil starte consumer processen.

Main thread

Herunder er vist de vigtigste ændringer i koden til main thread. Køen erklæres på basis af en template.

#include "..\include\Queue.h"

Queue<AnsiString, 20> Buffer;

TThreadProducer * Producer = new TThreadProducer(false);

TThreadConsumer * Consumer = new TThreadConsumer(true);

void __fastcall TForm1::Button1Click(TObject *Sender)

{

Consumer->Resume();

}

 

 

Producer

Der er indført en ny lokal variabel Full til styring af adgangen til køen. Meningen er at lade producer tråden vente, indtil der igen er plads i køen.

//---------------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

#include "Unit2.h"

#include "..\include\queue.h"

#include <syncobjs.hpp>

#pragma package(smart_init)

extern Queue<AnsiString, 20> Buffer;

extern int Count;

extern bool Status;

extern TCriticalSection *Lock;

__fastcall TThreadProducer::TThreadProducer(bool CreateSuspended)

: TThread(CreateSuspended)

{

Priority = tpNormal;

}

//---------------------------------------------------------------------------

void __fastcall TThreadProducer::Execute()

{

AnsiString Input;

bool Result;

bool Full;

Result = InputQuery("Producer", "Indtast en tekst", Input);

while (Result)

{

do

{

Lock->Acquire();

Full = Buffer.Full();

if (!Full)

{

Buffer.Add(Input);

}

Lock->Release();

} while (Full);

Input = "";

Result = InputQuery("Producer", "Indtast en tekst", Input);

}

Lock->Acquire();

Status = false;

Lock->Release();

return;

}

 

Consumer

Der skal tilføjes kode, så behandlingen gentages, så længe der er data i køen.

//---------------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

#include "Unit3.h"

#include "Unit1.h"

#include "..\include\queue.h"

#include <syncobjs.hpp>

#pragma package(smart_init)

extern Queue<AnsiString, 20> Buffer;

extern int Count;

extern bool Status;

extern TCriticalSection *Lock;

bool Empty;

__fastcall TThreadConsumer::TThreadConsumer(bool CreateSuspended)

: TThread(CreateSuspended)

{

Continue = true;

}

//---------------------------------------------------------------------------

void __fastcall TThreadConsumer::AddToList()

{

Form1->ListBox1->Items->Add(Output);

}

void __fastcall TThreadConsumer::Execute()

{

do

{

do

{

Lock->Acquire();

Empty = Buffer.Empty();

Continue = Status;

if (!Empty)

{

Output = Buffer.Remove();

}

Lock->Release();

if (!Empty)

Synchronize(AddToList);

} while (!Empty);

} while (Continue);

return;

}