LazarusCompleteGuide:10.2

From 흡혈양파의 번역工房
Jump to navigation Jump to search

스레드

스레드는 프로그램이 실행되는 장소이다. 프로그램이 하나 이상의 스레드를 가질 경우 여러 액션을 동시에 실행할 수 있다. 단일 프로세서 시스템에서는 각 스레드가 짧은 시간 슬롯 동안 실행이 허용되어, 동시에 여러 개의 액션을 실행할 수 있다는 인상을 심어준다. 이 전략을 시분할방식(timesharing)이라 부른다. 멀티프로세서 시스템에서는 하나 이상의 스레드가 실제로 동시에 실행되는 것이 가능하지만, 물론 시스템 내 CPU 수는 초과하지 않는다.


멀티스레드 애플리케이션을 생성하는 수고를 감수하는 이유에는 여러 가지가 있다. 그 중 하나는 사용자가 시간이 많이 소요되는 작업을 실행하는 동안에도 작업을 계속할 수 있도록 만들기 위해서이다. 또 다른 이유는 멀티프로세스 시스템의 모든 리소스를 활용하는 방식으로 동시에 여러 프로세서 집중적인 활동들을 실행해야 할 필요성을 들 수 있겠다. 한 스레드는 항상 하나의 프로세서에 의해 실행되므로 단일 스레드의 프로그램은 멀티프로세서 성능을 활용할 수 없다.


스레드를 사용 시 단점도 있으므로 해결하고자 하는 문제에 멀티스레드가 충분한 장점을 가져다주는지 항상 고려해야 한다. 멀티스레드 애플리케이션은 디버깅이 더 까다로운데, 스레드의 활동을 실행하는 데 소요되는 시간이 다양하기 때문이다.


모든 프로세스는 최소 하나의 스레드를 가지는데, 이는 먼저 모든 유닛의 초기화를 실행한 후 메인 프로그램의 시작으로 돌아간다. 이 스레드를 메인 스레드라고 부른다. 애플리케이션은 메인 스레드가 끝날 때 종료된다. 이는 스레드 안전(thread-safe)에 해당하지 않는 라이브러리를 호출할 수 있는 유일한 스레드이다.


프로세스의 모든 스레드는 동일한 기억 공간을 공유하므로 모두 전역 변수를 읽고 쓰는 것이 가능하다. 각 스레드는 고유의 스택과 고유의 레지스터 이미지를 가지므로 고유의 로컬 변수도 가진다. 즉, 로컬 변수를 다른 스레드로 전달하는 것이 문제가 될 수 있음을 의미한다; 스레드가 그들이 선언된 프로시저를 떠나면 더 이상 유효하지 않기 때문이다.


이는 문자열과 같은 참조 횟수(reference-counted) 객체에는 문제가 되지 않는다.


유닉스 체제는 (모놀리식 윈도우와 달리) 매우 모듈식(modular) 구조를 가지며 다수의 함수들이 초기설치 시 포함되어 있지 않으므로 추후 설치를 통해서만 추가된다. 이는 유닉스 체제에 필요한 리소스가 모두 설치되어 있는지 확신할 수 없음을 의미한다. 이는 POSIX 스레드 라이브러리에도 마찬가지인데, 이는 운영체제의 실행에 기본 요구사항이 아니기 때문이다. 그러한 확장된 기능은 필요 시 유닉스 체제로 라이브러리를 추가함으로써 설치된다. 따라서 우리는 적절한 리소스 관리자가 설치되어 있는지 주목해야 한다. 이는 cthreads 유닛을 프로그램에 연결시킴으로써 처리한다. 이는 프로그램 초기설정(initialization)에 pthreads 라이브러리를 로딩할 것이다.


이를 가능하게 하려면 cthreads 유닛을 이용하는 다른 유닛들보다 먼저 cthreads 유닛을 포함시켜야 한다. 아래 uses 절의 예를 들어보겠다:

program myproject;
  uses
    {$IFDEF UNIX}
  cthreads,
    {$ENDIF}
    //... other units are declared after cthreads


TThread 클래스

프로퍼티 설명
procedure Resume; 스레드의 실행을 다시 시작한다.
procedure Suspend; 스레드를 일시정지시킨다.
procedure Terminate; 스레드에게 중단할 것을 신호로 보낸다.
procedure Synchronize (AMethod: TThreadMethod); 메인 스레드에 AMethod를 실행한다. AMethod는 객체의 타입 프로시저여야 한다.
function WaitFor: 정수; 스레드가 중단되길 기다리고 종료 상태를 리턴한다.
property FreeOnTerminate: Boolean; [rw] 스레드가 실행을 중단 시 스스로 해제(free)될 것인지를 나타낸다.
기본 값은 False이다.
property handle: TThreadID; [r] 스레드 핸들을 리턴한다.
property Priority: TThreadPriority; [rw] 스레드 우선순위를 리턴한다.
property Suspended: Boolean; [rw] 스레드가 일시정지되었는지 나타낸다.
property OnTerminate: TNotifyEvent; [rw] 스레드가 중단 시 호출되는 이벤트.
표 10.6: TThread의 가장 중요한 메소드와 프로퍼티


Execute 메소드 내 코드는 스레드가 실행 중일 때 실행된다. 스레드에 대해 로컬 저장소를 생성하려면 파생된 클래스에 필드를 추가해야 한다. TThread로부터 클래스를 파생할 때는 새 스레드를 생성하고, 해제(release)할 때는 스레드를 해제(release)한다. TThread 내 일부 메소드는 종료될 때까지 기다리는 것이 가능하다. 즉시 시작할 것인지, 이후에 시작할 것인지 나타낼 수도 있다. Terminated 프로퍼티는 그것이 종료되어야 함을 의미한다. 이 프로퍼티는 사실상 유일한 플래그로, 어떤 액션도 취하지 않는다. 수시로 확인하는 내용은 Execute 메소드 내 코드에 따라 달려 있다. Terminated가 설정될 경우 스레드는 끝나야 한다.


일시정지와 다시시작

TThread는 Suspend 메소드를 제공하긴 하지만 전혀 사용해선 안 된다! 반대로 Resume은 중단된 스레드를 다시 (안전하게) 시작하는 데에 사용할 수 있다. 하지만 Suspend를 사용해선 안 되므로 Resume 또한 무용지물이다.


Suspend와 Resume을 사용해선 안 되는 이유는 설명하기 쉽다: Suspend는 당신에게 스레드의 중단 위치의 제어를 허용하지 않으므로 경합 조건(race condition)으로 이끌 수가 있어 위험하다. 임계영역(Critical section)에 진입해 다른 스레드들이 그 영역을 필요로 할 때 영역을 이용하지 못하도록 할 가능성이 크다.


따라서 POSIX 표준은 비협력적 방식으로 스레드를 일시정지하는 방도를 제공하지 않는다. 그 결과 어떤 유닉스 체제에서도 Suspend를 이용할 수 없다. 윈도우는 Suspend를 완전히 지원하지만 프로그램을 유닉스 플랫픔으로 변환하길 원할 경우 문제를 예방하려면 사용하지 말아야겠다.


보통은 Suspend를 사용하는 대신 스레드에 잠시 중지할 것을 알리기 위해 변수를 사용할 수 있다. 대상 스레드는 이 변수를 수시로 확인하고 필요 시 중단하여 변수가 다시 시작을 알릴 때까지 중단시킨다.



우선순위

스레드는 여러 우선순위를 가질 수 있다. 우선순위가 높은 스레드는 낮은 우선순위 작업보다 더 큰 시간 구획(time slice)을 얻어 실행할 가능성이 크다. 전형적인 예로, 단순한 계산만 실행하는 CPU-bound 스레드보다 I/O 연산에 우선순위가 높게 부여되는 사례를 들 수 있겠다. 대부분 I/O-bound 스레드는 입력이나 출력 연산이 완료되길 기다리기만 하며, CPU 용량(capacity)을 활용하는 경우가 드물다. 이러한 이유로 높은 우선순위를 허용할 수 있다. 이는 다음 I/O 연산에 빠르게 응답하고 준비할 수 있게 해준다. 우선순위가 높은 완전한 CPU-bound 스레드는 CPU 용량을 대부분 소모하여 다른 낮은 우선순위로 된 작업을 차단할 것이다.


일반적으로 다중 스레드를 사용하는 것은 의도대로 높은 속도의 혜택을 얻는 데 좋은 생각이다. 잘못된 결정은 성능에 매우 부정적인 영향을 미칠 수 있다. 심지어 노력만큼 향상된 것이 없는 우선순위를 테스트하거나 미세 조정하는 것이 엄청나게 까다로울지도 모른다.

상수 설명
tpIdle 다른 프로세스가 유휴 상태(idle)일 때만 스레드가 실행된다.
tpLowest 스레드가 가장 낮은 우선순위로 실행된다.
tpLower 스레드가 낮은 우선순위로 실행된다.
tpNormal 스레드가 보통 우선순위로 실행된다.
tpHigher 스레드가 높은 우선순위로 실행된다.
tpHighest 스레드가 가장 높은 우선순위로 실행된다.
tpTimeCritical 스레드가 실시간 우선순위로 실행된다.
표 10.7: TThread.Priority에 가능한 값


동기화와 임계영역

동기화 메커니즘은 메인 스레드가 스레드 안전(thread-safe)하지 않은 라이브러리를 사용할지 모르는 여러 스레드들을 호출하도록 허용하기 위해 도입되었다. 이를 가능하게 하려면 메인 스레드가 이 코드를 실행할 때까지 스레드의 실행은 일시 정지된다. LCL은 대부분 GUI 라이브러리와 마찬가지로 스레드 안전(thread-safe)하지 않다. 따라서 모든 LCL 기반의 코드는 메인 스레드 또는 동기화 블록에 있어야 한다.


뮤텍스(Mutex)라고도 알려진 (상호배제) 임계 영역을 고려하는 것은 동기화 메커니즘을 설명하기 좋은 방법이다. 스레드를 임계 영역으로 진입시킬 때, 그 영역 내 다른 스레드가 이미 있을 경우 그것은 일시 정지된다. 이는 연산이 완료되고 다른 스레드가 동일한 임계 영역으로 진입하기 전에 결과를 이용할 수 있도록 보장한다. SyncObjs 유닛에 위치한 TCriticalSection 클래스는 두 개의 간단한 메소드, Enter 와 Leave를 제공한다. 첫 번째는 스레드가 임계 영역에 진입하고 있음을 나타내고, Leave 가 호출될 때까지 다른 작업이 들어가지 못하도록 막는다. 임계 영역으로 진입을 기다리는 스레드는 수면 상태(sleeping)에 있으며, 자신의 차례가 되면 운영체제가 자동으로 깨운다(wake).


스레드는 운영체제가 작업 전환을 실행할 때 두 개의 기계어 명령 코드(machine code instruction) 사이에서 중단될 수 있다. 대부분 명령어는 하나 이상의 기계어 명령으로 구성되어 있기 때문에 이는 오브젝스 파스칼 명령어 중간에서 발생할 수도 있다.


MyVar 는 0값으로 시작할지도 모른다.


이제 첫 번째 스레드가 실행되기 시작한다. 이는 MyVar를 읽고 그에 4를 더한다.


이제 운영체제가 작업 전환을 한다. 두 번째 스레드는 모든 코드를 실행한다. 여전히 MyVar 는 0으로 읽은 후 4를 더한다.


이제 MyVar 는 4와 같아지고, 운영체제는 첫 번째 스레드로 다시 전환하는데 이 역시 MyVar에 4를 저장한다. 문제는 첫 번째 스레드가 정확히 그 지점에서 중단되지 않을 시 MyVar가 8이 될 수도 있다는 점이다.


두 개 이상의 스레드가 실행될 경우 문제가 더 심각해지며, 여느 타이밍 문제와 마찬가지로 무엇이 잘못되었는지 찾기가 매우 힘들다.


우리는 이 문제를 방지하기 위해 TCriticalSection을 이용해 이 영역을 다르게 코딩할 수 있다. 클래스는 짧고 이해가 쉽다:

type
  TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError);

  TSynchroObject = class(TObject)
    procedure Acquire; Virtual;
    procedure Release; Virtual;
  end;

TCriticalSection = class(TSynchroObject)
  private
    CriticalSection: TRTLCriticalSection;
  public
    procedure   Acquire; Override;
    procedure   Release; Override;
    procedure   Enter;
    procedure   Leave;
    constructor Create;
    destructor  Destroy; Override;
  end;


임계 영역은 일반적인 경합 조건(race condition) 문제와 관련이 있다. 경합 조건은 다중 스레드에 의해 실행되는 순서에 따라 다른 결과가 생산될지도 모르는 코드의 섹션을 의미한다. 이 시나리오는 오류가 발생하기 쉽고, 디버깅이 까다로운 것이 보통이다. 두 개의 스레드가 아래 코드를 실행한다고 가정하자.

Local := MyVar;
Local := Local + 4;
MyVar := Local;


시간 스레드 1 로컬 (스레드 1) 스레드 2 로컬 (스레드 2) MyVar
1 Local := MyVar; 0 0
2 Local := Local + 4; 4 0
3 4 Local := MyVar; 0 0
4 4 Local := Local + 4; 4 0
5 4 MyVar := Local; 4 4
6 MyVar:= Local; 4 4
표 10.8: 두 작업 간 경합 조건이 오류로 이끔


임계 영역은 전역적이어야 하며 접근하는 전역 변수에 특정적이어야 하는데, 여기서 MyVarCS 라고 부르는 정확한 이유이다. 코드를 실행하기 전에 TCriticalSection 클래스의 인스턴스를 생성함으로써 변수를 초기화해야 한다:

MyVarCS.Enter;
try
  Local := MyVar;
  Local := Local + 4;
  MyVar := Local;
finally
  MyVarCS.Leave;
end;


이러한 새 코드는 해당 코드 블록에 하나 이상의 스레드는 절대로 있을 수 없도록 해준다. try…finally 구문은 안전 조치로, 예외가 발생 시 다른 스레드들을 위해 임계 영역이 해제되도록 보장한다.

시간 스레드 1 로컬 (스레드 1) 스레드 2 로컬 (스레드 2) MyVar
1 MyVarCS.Enter; ? 0
2 Local := MyVar; 0 0
3 Local := Local + 4; 4 0
4 4 MyVarCS.Enter; ? 0
5 MyVar := Local; 4 ? 4
6 MyVarCS.Leave; 4 ? 4
7 Local := MyVar; 4 4
8 Local := Local + 4; 8 4
9 MyVar := Local; 8 8
10 MyVarCS.Leave; 8 4
표 10.9: 같은 계산, 임계 영역으로 안정화됨


여기에 적절한 멀티스레딩을 이용한 완전하고 해석 가능한 애플리케이션 버전이 있다. 이 코드는 위의 예제와 동일하게 실행하며 항상 8에 해당하는 MyValue 값을 콘솔에 인쇄한다. 결과는 운영체제가 스레드를 실행하는 순서에 의존하지 않는다.

program criticalsection;
{$apptype console}
{$mode objfpc}{$H+}
uses {$IFDEF UNIX}
       cthreads,
     {$ENDIF}
       Classes, SyncObjs;

type TMyThread = class(TThread)
     public
       procedure Execute; override;
     end;

var  MyVar  : Cardinal;
     MyVarCS: TCriticalSection;
     Thread1, Thread2: TMyThread;

  procedure TMyThread.Execute; (* The procedure being executed with a Critical Section *)
    var Local: Cardinal;
  begin
    MyVarCS.Enter;
    try
      Local := MyVar;
      Local := Local + 4;
      MyVar := Local;
    finally
      MyVarCS.Leave;
    end;
  end;

{ program criticalsection }
begin
  MyVar   := 0;                       (* Initializes MyVar           *)
  MyVarCS := TCriticalSection.Create; (* Creates the CriticalSection *)
  Thread1 := TMyThread.Create(False); (* Creates and        *)
  Thread2 := TMyThread.Create(False); (* starts the threads *)
  Thread1.WaitFor;                    (* Waits until the threads *)
  Thread2.WaitFor;                    (* finish executing        *)
  Thread1.Free;                       (* Release the threads ... *)
  Thread2.Free;
  MyVarCS.Free;                       (* ... and the critical section *)
  WriteLn('MyVar = ', MyVar);         (* Show the result on screen    *)
end.