본문 바로가기
엑셈 기업문화/엑셈 사람들

[방기남]진리를 향한 끊임없는 열망

by EXEM 2009. 3. 20.


엑셈 개발팀의 주력 개발툴은 델파이(Delphi)다.

사용자 삽입 이미지
델파이는 매번 새로운 버전을 내놓을 때마다 당시 신기술을 접목시켜 발표하여 왔고, 최근 DELPHI 2009 버전을 출시하면서 유니코드, 제네릭, Ajax & Silverlight 기반 웹 어플리케이션 등 새로운 기술로 다시 한 번 다가왔다.

사실, 이렇게 나날이 발전해 나가는 기술과 뒷받침하는 개발툴을 제대로 사용한다는 것은 참 어렵기만하다.
늘 사용해왔었고 검증된 방법으로만 개발하기 때문이랄까...새로운 뭔가를 찾아보지만 결국 다람쥐 쳇바퀴 도는 듯한 느낌만 든다.

엑셈 개발팀에 입사한지 어느덧 4개월이 되어간다. 하지만, 엇그제 일 같다. UDB팀에 합류한 이후 계속해서 달려온 느낌이다. 많은 것을 새로 배웠고, 모니터링 툴과 UDB에 대한 이해를 높일 수 있는 좋은 기회가 되었다.
엑셈의 모든 지식인들이 그렇듯 지식에 대한 갈망은 끝이 없는 것 같다. 머리속엔 UDB에 대한 생각이 가득하다.
-_-;;

서론이 길었다.
자, 델파이 이야기를 했으니 델파이를 이용한 스킬을 하나 다뤄본다.

Thread

쓰레드(THREAD)는 Window Programming 에서도 고급 주제에 속한다고 한다. 사실, 쓰레드의 사용이 어려운 것이 아니라 잘못된 사용으로 인한 시스템의 부하, 좀 더 심각하게 말하면 시스템의 다운으로 이어지는 경우가 발생하기 때문에 어려운 것이 아닐까 생각한다.

그럼 쓰레드란 무엇일까?
쉽게 말하면 동시에 여러가지 일을 행할 수 있는 능력이라 할 수 있다. 예를 들면, 우리가 사용하고 있는 Windows XP 또는 Vista 등의 운영체제에서 어떤 작업을 한다고 하자. 한가지 일만 하는가? 그렇지 않다. 단지, 사용자가 한번에 여러가지 작업을 하지 못할뿐이다. 운영체제는 사용자가 현재 작업하는 것들을 모두 소화하고도 여유를 부릴만큼의 능력을 가지고 있다.

이제부터 실질적인 내용으로 들어가보자.

Windows 3.x 버전까지는 비선점형 멀티테스킹NON-PREEMPTIVE MULTITASKING을 지원했으며, Windows95가 출시되면서 비로서 선점형 멀티테스킹 PREEMPTIVE MULTITASKING을 지원했다.

그렇다면, 비선점형 멀티테스킹과 선점형 멀티테스킹은 무엇일까?
바로 프로그램의 제어권이 어디에 있느냐에 대한 것이 답이된다.
과거 비선점형 멀티테스킹 방식은 프로그램이 제어권을 가지고 있어서 해당 프로그램이 무한루프에 빠지거나 다운되는 경우 그 제어권을 다른 프로그램에게 넘겨줄 수 없기 때문에 재부팅을 해야했었다. 이러한 문제를 해결한 것이 바로 선점형 멀티테스킹 방식이다. 제어권은 운영체제가 가지고 있고 실행중인 프로그램이 다운되더라도 운영체제가 그 제어권을 다른 프로그램으로 옮겨 시스템 전체가 다운되지 않도록 한 것이다.

운영체제내에서 실행되는 어플리케이션APPLICATION의 인스턴스INSTANCE를 프로세스PROCESS라고 한다. 메모장을 두 개 열어 두었다면 두 개의 프로세스가 운영체제상에서 실행되고 있는 것이다.
운영체제는 프로세스를 로드할 때 쓰레드를 함께 만들게 된다. 이 쓰레드를 메인쓰레드라고 한다. 다시말해서, 어떤 프로그램이든 실행과 동시에 운영체제는 쓰레드를 생성하는 것이다.

델파이에서 쓰레드를 사용하기 위해서는 TThread 클래스를 상속받아 새로운 쓰레드 객체를 정의한 하고, 사용하기 위해 몇가지 함수를 override 해야한다. TThread 클래스는 classes.pas 파일에 정의되어 있고 다음과 같다.

TThread = class
  private
{$IFDEF MSWINDOWS}
    FHandle: THandle;
    FThreadID: THandle;
{$ENDIF}
{$IFDEF LINUX}
    // ** FThreadID is not THandle in Linux **
    FThreadID: Cardinal;
    FCreateSuspendedSem: TSemaphore;
    FInitialSuspendDone: Boolean;
{$ENDIF}
    FCreateSuspended: Boolean;
    FTerminated: Boolean;
    FSuspended: Boolean;
    FFreeOnTerminate: Boolean;
    FFinished: Boolean;
    FReturnValue: Integer;
    FOnTerminate: TNotifyEvent;
    FSynchronize: TSynchronizeRecord;
    FFatalException: TObject;
    FExternalThread: Boolean;
    procedure CallOnTerminate;
    class procedure Synchronize(ASyncRec: PSynchronizeRecord; QueueEvent: Boolean = False); overload;
    class function GetCurrentThread: TThread; static;
{$IFDEF MSWINDOWS}
    function GetPriority: TThreadPriority;
    procedure SetPriority(Value: TThreadPriority);
{$ENDIF}
{$IFDEF LINUX}
    // ** Priority is an Integer value in Linux
    function GetPriority: Integer;
    procedure SetPriority(Value: Integer);
    function GetPolicy: Integer;
    procedure SetPolicy(Value: Integer);
{$ENDIF}
    procedure SetSuspended(Value: Boolean);
  protected
    procedure CheckThreadError(ErrCode: Integer); overload;
    procedure CheckThreadError(Success: Boolean); overload;
    procedure DoTerminate; virtual;
    procedure Execute; virtual; abstract;
    procedure Queue(AMethod: TThreadMethod); overload;
    procedure Synchronize(AMethod: TThreadMethod); overload;
    procedure Queue(AThreadProc: TThreadProcedure); overload;
    procedure Synchronize(AThreadProc: TThreadProcedure); overload;
    property ReturnValue: Integer read FReturnValue write FReturnValue;
    property Terminated: Boolean read FTerminated;
  public
    constructor Create(CreateSuspended: Boolean);
    destructor Destroy; override;
    procedure AfterConstruction; override;
    procedure Resume;
    procedure Suspend;
    procedure Terminate;
    function WaitFor: LongWord;
    class procedure Queue(AThread: TThread; AMethod: TThreadMethod); overload;
    class procedure RemoveQueuedEvents(AThread: TThread; AMethod: TThreadMethod);
    class procedure StaticQueue(AThread: TThread; AMethod: TThreadMethod);
    class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload;
    class procedure StaticSynchronize(AThread: TThread; AMethod: TThreadMethod);
    class procedure Queue(AThread: TThread; AThreadProc: TThreadProcedure); overload;
    class procedure Synchronize(AThread: TThread; AThreadProc: TThreadProcedure); overload;
    property ExternalThread: Boolean read FExternalThread;
    property FatalException: TObject read FFatalException;
    property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate;
    property Finished: Boolean read FFinished;
{$IFDEF MSWINDOWS}
    property Handle: THandle read FHandle;
    property Priority: TThreadPriority read GetPriority write SetPriority;
{$ENDIF}
{$IFDEF LINUX}
    // ** Priority is an Integer **
    property Priority: Integer read GetPriority write SetPriority;
    property Policy: Integer read GetPolicy write SetPolicy;
{$ENDIF}
    property Suspended: Boolean read FSuspended write SetSuspended;
{$IFDEF MSWINDOWS}
    property ThreadID: THandle read FThreadID;
{$ENDIF}
{$IFDEF LINUX}
    // ** ThreadId is Cardinal **
    property ThreadID: Cardinal read FThreadID;
{$ENDIF}
    property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
    class property CurrentThread: TThread read GetCurrentThread;
  end;

 위 코드는 DELPHI 2009 에서 사용되는 classes.pas 파일에서 추출한 내용이다.
주의깊게 봐야할 메소드는 Create, Execute, Resume, Synchronize 정도이다. Create 생성자는 쓰레드에 대한 메모리와 리소스를 할당하고 쓰레드를 시작하고 실행할 수 있다.

constructor TThread.Create(CreateSuspended: Boolean);
{$IFDEF LINUX}
var
  ErrCode: Integer;
{$ENDIF}
begin
  inherited Create;
  AddThread;
  FSuspended := CreateSuspended and not FExternalThread;
  FCreateSuspended := CreateSuspended and not FExternalThread;
  if not FExternalThread then
  begin
{$IFDEF MSWINDOWS}
    FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
    if FHandle = 0 then
      raise EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(GetLastError)]);
{$ENDIF}
{$IFDEF LINUX}
    sem_init(FCreateSuspendedSem, False, 0);
    ErrCode := BeginThread(nil, @ThreadProc, Pointer(Self), FThreadID);
    if ErrCode <> 0 then
      raise EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(ErrCode)]);
{$ENDIF}
  end else
  begin
{$IFDEF MSWINDOWS}
    FHandle := Windows.GetCurrentThread;
    FThreadId := Windows.GetCurrentThreadId;
{$ENDIF}
  end;
end;

쓰레드 클래스의 생성자에서는 TObject의 Create 를 호출하고 쓰레드가 실제 실행되도록 쓰레드 함수(ThreadProc)의 주소 값이 파라미터로 넘어간다. Create 생성자는 CreateSuspend 라는 Boolean 형 파라미터가 하나 있는데, True 상태로 생성하면 override 되는 Create 생성자 안에서 맨 마지막에 Resume 메소드를 호출하도록 해야 한다.

Execute 메소드는 추상 메소드로 선언되어 있는데, 우리는 이 메소드를 다시 override 한 다음 사용해야 한다. 실제 쓰레드는 Execute 메소드 안에 작성되거나 작업을 수행할 메소드를 Execute 메소드 안에서 호출한다.

간단하게 예를 들어보자.

unit Unit2;

interface

uses

Classes, Windows;

type

TBeeperThread = class(TThread)

private

protected

  procedure Execute; override;

end;

implementation

 

procedure TBeeperThread.Execute;

begin

while not Terminated then

begin

  beep(200, 200);

  sleep(1000);

end;

end;

 

end.


unit Unit1;
interface
uses
Classes, Windows, SysUtils, Messages, Graphics, Forms, Dialogs, StcCtrls;
type
TForm1 = class(TForm)
  Button1: Tbutton;
  procedure Button1Click(Sender: TObject);
private
public
end;
var
  Form1: TForm1;
implementation

uses
   Unit2;
var
   BeeperThread: TBeeperThread;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
BeeperThread := TBeeperThread.Create(False);
end;

end.

BeeperThread := TBeeperThread.Create(False);
생성자의 파라미터가 False 이므로 쓰레드를 생성하는 즉시 실행한다. 위에서 말한 것처럼 True 를 파라미터로 넘겨준다면 Resume 메소드를 호출해서 명시적으로 쓰레드를 실행할 수 있다.

쓰레드를 종료하기 위해 BeeperThread.Terminated; 코드를 추가한다. Terminated 메소드는 쓰레드 객체의 Terminated 속성값이 True 상태인 경우에만 실제로 쓰레드가 멈추고 종료된다.

쓰레드의 실행을 잠시 멈추기위해 BeeperThread.Suspend; 그리고
쓰레드의 실행을 다시 시작하기위해 BeeprThread.Resume; 메소드를 호출할 수 있다.

프로세스내의 각 쓰레드는 부모 프로세스와 관련된 우선 순위를 부여받게 되는데, 이 때 사용되는 속성이 Priority 속성이다. 이 속성을 사용하면 Windows API 함수 GetPriorityClass, SetPriorityClass 등을 직접 호출하지 않아도 되며, 우선 순위를 정하는 기준은 다음과 같다.

1

tpIdle

시스템이 유휴 상태일 때만 쓰레드가 실행 

2

tpLowest

Normal 우선 순위보다 두 단계 아래 

3

 tpLower  

Normal 우선 순위보다 한 단계 아래 

 4 

tpNormal 

보통의 우선 순위 

5

tpHigher

Normal 우선 순위보다 한 단계 위 

6

 tpHighest

Normal 우선 순위보다 두 단계 위 

7

tpTimeCritica 

가장 높은 우선 순위 

BeeperThread.Priority := tpLower;
위와 같이 사용한다.

이제, 다음으로 알아두어야 할 것이 있다면 멀티쓰레드에서 흔히 나올 수 있는 문제에 대한 해결방법으로 동기화라는 것이 있다. 쉬운 예를 들면, 두개의 쓰레드가 있고 동일한 데이터에 접근할 때 발생하는 아주 당연한 문제가 있다. 처음 쓰레드는 어떤 값을 기록하고 있고, 두 번째 쓰레드는 값을 읽어오거나 지운다고 할 때, 기록하는 순서와 읽거나 지우는 순서가 서로 맞지 않아서 AV 에러를 발생시키게 되는것이다.

Critical Section
Mutex
Semaphore
Event

문제를 해결하기 위한 네 가지의 메커니즘 중에 좀 더 논리적인 접근을 위해 Critical Section, Mutex 를 사용한다.
Critical Section 은, 어떤 쓰레드가 어떤 데이터에 대한 작업을 하는 도중에는 다른 쓰레드가 작업중인 데이터에 접근하지 못하도록 하는 방법을 제공한다. Mutex 역시 Critical Section 과 유사하지만 여러 프로세스 안에서 동시에 사용이 가능하다는 점이 Critical Section 과 다르다.

요즘 본인이 속해있는 UDB 팀에서는 RealTime, Performance Analyzer 의 Repository 버전을 개발했다. 그 동안 통신으로 주고 받았던 것을 데이터베이스에서 읽어와 보여지게 하는 작업인데, 여기서도 쓰레드를 사용하여 동시에 여러 쿼리문을 보내고 결과를 얻을 수 있다.
일반적으로 쿼리 컴포넌트의 Open 메소드를 사용하면 결과가 넘어올 때까지 기다려야 하고, 그 시간 동안에는 같은 프로세스내에서 다른 작업을 할 수 없었던 것이 보통이지만, 쓰레드를 사용하면 동시에 여러 쿼리에 대한 결과를 받을 수 있고, 또한 결과를 받기까지 대기하는 시간이 없어지게 된다.

간단하게 소스를 보면서 설명한다.

unit QryThread;

interfac

uses
  Classes, DB, DBTables, SysUtils, Forms;
 
type
  TQryThread = class(TThread)
  private
    FSession: TSession;
    FQry: TQuery;
    FDataSource:= DataSource;
    FDatabase: TDatabase;
    FQryExcept: Exception;
    DatabaseName: String;
   
    procedure ConnectDataSource;
    procedure ShowQryError;
  public
    constructor Create(Session: TSession; Qry: TQuery; DBName: String; SQLString: String; DataSource: TDataSource); virtual;
  protected
    procedure Execute; override;
  end;
 
var
  QryThread: TQryThread;

implementation

constructor TQryThread.Create(Session: TSession; Qry: TQuery; DBName: String; SQLString: String; DataSource: TDataSource); virtual;
begin
  inherited Create(True);
  FSession := Session;
  FQry: Qry;
  FDatabaseName := DBName;
 
  FQry.Close;
  FQry.SessionName := FSession.SessionName;
  FQry.DatabaseName := DBName;
  FQry.SQL.Clear;
  FQry.SQL.Assign(SQLString);
  FreeOnTerminate := True;
  Resume;
end;

procedure TQryThread.Execute;
begin
  try
    FQry.Open;
    Synchronize(ConnectDataSource);
  except
    FQryExcept := ExceptObject as Exception;
    Synchronize(ShowQryError);
  end;
end;

procedure TQryThread.ConnectDataSource;
begin
  FDataSource.DataSet := FQry;
end;

procedure TQryThread.ShowQryError;
begin
  Application.ShowException(FQryExcept);
end;

end.

데이터베이스를 연결하기 위한 기본적인 내용을 파라미터로 받는다.
Synchronize 메소드는 쓰레드가 일시적으로 어플리케이션의 메인 쓰레드의 일부가 되도록 해주어 VCL에 접근할 수 있도록 해준다. 다시 말하면, 쓰레드가 돌고있는 도중에 어떠한 시각적 표시를 해주기 위해 Synchronize 를 사용할 수 있다는 것이다. 하지만, 여기에도 문제가 있다. Synchronize 를 사용하여 시각적 표시를 할 수 있지만 Synchronize 메소드가 사용되고 마치기 전까지는 쓰레드 역시 실행을 중지하고 대기한다는 것이다.

만약 쓰레드로 데이터베이스를 열고 자료를 가져오는 도중 UI 에 대한 부분 처리를 하고자 할 때, 생각보다 오래 걸리게 되는 경우가 발생할 수 있다는 것이다.

이제 연결되어 쿼리를 보내는 부분은 준비되었으니, 간단하게 사용하는 방법을 알아본다.

unit Main;

interface

uses
  ... Db, QryThread;

type
  TFMain = class(TForm)
  private
    FQryThread: TQryThread;
    procedure RunQry(Session: TSession; Qry: TQuery; DBName: String; SQLString: String; DataSource: TDataSource);
  end;

var
  Main: TFMain;

procedure TFMain.RunQry(Session: TSession; Qry: TQuery; DBName: String; SQLString: String; DataSource: TDataSource);
begin
  FQryThread := TQryThread.Create(Session, Qry, DBName, SQLString, DataSource);
end;

// 버튼이 있다고 가정하고 소스에는 추가하지 않았다.
procedure TFMain.Button1Click(Sender: TObject);
begin
  RunQry(FirstSession, FirstQry, FirstDBName, SQLMemo1.Text, FirstDataSource);
  RunQry(SecondSession, SecondQry, SecondDBName, SQLMemo1.Text, SecondDataSource);
end;

end.

버튼을 눌렀을 때, 두 개의 쿼리는 각각 서로 다른 생성자를 파라미터로 넘기고 실행한 다음 결과값을 리턴해온다.

사실상 쓰레드는 안전하지 않다.
쓰레드를 잘못 사용하면 오히려 시스템이 느려질 수도 있고, 시스템이 다운되는 경우도 발생하게 된다. 가급적이면 쓰레드의 사용빈도를 줄이는게 좋겠지만 동시처리를 위해서 사용된다. 지금의 Repository 버전 실시간은 한번에 여러 개의 쿼리 결과를 받아와 처리하도록 되어있다. 네트워크에 문제가 없고, 자료를 읽어오는 데이터베이스에 문제가 없다면 Repository 버전의 UDB 제품은 모두 잘 돌아갈 것이다.

아직도 가야할 길이 멀고, 넘어야 할 산이 많은 것 같다. 하지만, 가야할 길도 다 가보고, 넘어야 할 산도 다 넘게 된 다음 또 다시 발견하게 될 다음 문제가 기다려진다.

댓글