18. Threads

스레드는 여러 작업들을 동시에 수행할 수 있도록 하는 제어 흐름의 변형이다. 여기서 작업간에 상호 작용은 거의 없다고 가정한다.

다음의 예는 B9라는 게임으로, 5개의 스레드가 존재한다: 메인 스레드, 입력을 터미널에 그리는 스레드, 키입력을 받고 커서를 업데이트해 셀을 만드는 스레드, 게임의 상태를 업데이트하는 스레드, 각 셀의 이웃 셀을 계산하는 스레드. 플랫폼이 여러 개의 코어나 프로세서를 가지고 있다면 이 스레드들은 동시에 수행될 수 있다. 하지만 플랫폼의 코어나 프로세서 수가 충분하지 않더라도 시스템은 스레드의 실행 순서를 적절히 조절해 사용자 입장에서는 동시에 수행되는 것처럼 보이도록 한다.

#include "life.h"
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdatomic.h>
#include <limits.h>
#include "termin.h"

// The keys that are used for cursor movement
// Other keys that are used are:
// b, B, space for birth9
// -, + to slow down and accelerate
// q, Q to quit
#define GO_UP 'k'
#define GO_DOWN 'l'
#define GO_RIGHT ';'
#define GO_LEFT 'j'
#define GO_HOME 'h'

// We translate escape sequences that we may receive to these standard
// characters for further processing.

#define ESCAPE '\e'

char const*const termin_trans[UCHAR_MAX+1] = {
  [GO_UP] = ESC_UP,
  [GO_DOWN] = ESC_DOWN,
  [GO_RIGHT] = ESC_FRWD,
  [GO_LEFT] = ESC_BKWD,
  [GO_HOME] = ESC_HOME,
};

static
int update_thread(void* Lv) {
  life*restrict L = Lv;
  size_t changed = 1;
  size_t birth9 = 0;
  while (!L->finished && changed) {
    // Blocks until there is work
    mtx_lock(&L->mtx);
    while (!L->finished && (L->accounted < L->iteration))
      life_wait(&L->upda, &L->mtx);

    // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
    if (birth9 != L->birth9) life_torus(L);
    life_count(L);
    changed = life_update(L);
    life_torus(L);
    birth9 = L->birth9;
    L->iteration++;
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    cnd_signal(&L->acco);
    cnd_signal(&L->draw);
    mtx_unlock(&L->mtx);

    life_sleep(1.0/L->frames);
  }
  return 0;
}

static
int draw_thread(void* Lv) {
  life*restrict L = Lv;
  size_t x0 = 0;
  size_t x1 = 0;
  fputs(ESC_CLEAR ESC_CLRSCR, stdout);
  while (!L->finished) {
    // Blocks until there is work
    mtx_lock(&L->mtx);
    while (!L->finished
           && (L->iteration <= L->drawn)
           && (x0 == L->x0)
           && (x1 == L->x1)) {
      life_wait(&L->draw, &L->mtx);
    }
    // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
    if (L->n0 <= 30) life_draw(L);
    else life_draw4(L);
    L->drawn++;
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    mtx_unlock(&L->mtx);

    x0 = L->x0;
    x1 = L->x1;
    // No need to draw too quickly
    life_sleep(1.0/40);
  }
  return 0;
}

// This number of consecutive states that are already known (that is,
// hashed in) decides that this sequence of states is cyclic
enum { repetition = 10, };

static
int account_thread(void* Lv) {
  life*restrict L = Lv;
  while (!L->finished) {
    // Blocks until there is work
    mtx_lock(&L->mtx);
    while (!L->finished && (L->accounted == L->iteration))
      life_wait(&L->acco, &L->mtx);

    // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
    life_account(L);
    if ((L->last + repetition) < L->accounted) {
      L->finished = true;
    }
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    cnd_signal(&L->upda);
    mtx_unlock(&L->mtx);
  }
  return 0;
}

static
int input_thread(void* Lv) {
  termin_unbuffered();
  life*restrict L = Lv;
  enum { len = 32, };
  char command[len];
  do {
    int c = getchar();
    command[0] = c;
    switch(c) {
    case GO_LEFT : life_advance(L,  0, -1); break;
    case GO_RIGHT: life_advance(L,  0, +1); break;
    case GO_UP   : life_advance(L, -1,  0); break;
    case GO_DOWN : life_advance(L, +1,  0); break;
    case GO_HOME : L->x0 = 1; L->x1 = 1;    break;
    case ESCAPE  :
      ungetc(termin_translate(termin_read_esc(len, command)), stdin);
      continue;
    case '+':      if (L->frames < 128) L->frames++; continue;
    case '-':      if (L->frames > 1)   L->frames--; continue;
    case ' ':
    case 'b':
    case 'B':
      mtx_lock(&L->mtx);
      // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
      life_birth9(L); /*@\label{lab:birth9}*/
      // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      cnd_signal(&L->draw);
      mtx_unlock(&L->mtx);
      continue;
    case 'q':
    case 'Q':
    case EOF:      goto FINISH;
    }
    cnd_signal(&L->draw);  /*@\label{lab:signal}*/
  } while (!(L->finished || feof(stdin)));
 FINISH:
  L->finished = true;
  return 0;
}

enum { n = 60, m = 160 };

bool M[n][m] = {
 { 0 },
 { 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0 },
 { 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, },
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, },
 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
 { 0 },
 { 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
 { 0 },
};

int main(int argc, char* argv[argc+1]) {
  /* Uses command-line arguments for the size of the board */
  size_t n0 = 30;
  size_t n1 = 80;
  if (argc > 1) n0 = strtoull(argv[1], 0, 0);
  if (argc > 2) n1 = strtoull(argv[2], 0, 0);
  /* Create an object that holds the game's data. */
  life L = LIFE_INITIALIZER;
  life_init(&L, n0, n1, M);
  /* Creates four threads that all operate on that same object
     and collects their IDs in "thrd" */
  thrd_t thrd[4];
  thrd_create(&thrd[0], update_thread,  &L);
  thrd_create(&thrd[1], draw_thread,    &L);
  thrd_create(&thrd[2], input_thread,   &L);
  thrd_create(&thrd[3], account_thread, &L);
  /* Waits for the update thread to terminate */
  thrd_join(thrd[0], 0);
  /* Tells everybody that the game is over */
  L.finished = true;
  ungetc('q', stdin);
  /* Waits for the other threads */
  thrd_join(thrd[1], 0);
  thrd_join(thrd[2], 0);
  thrd_join(thrd[3], 0);
  /* Puts the board in a nice final picture */
  L.iteration = L.last;
  life_draw(&L);
  life_destroy(&L);
}

C에서의 스레드는 새 스레드를 시작하는 함수와 그 스레드의 종료를 기다리는 함수 2개로 다루어진다.

#include <threads.h>
typedef int (*thrd_start_t)(void*);
int thrd_create(thrd_t*, thrd_start_t, void*);
int thrd_join(thrd_t, int *);

여기서 thrd_create의 두 번째 인자는 thrd_start_t 타입의 함수 포인터로, 새 스레드의 실행 시 시작된다. thrd_t는 불명확 타입으로 새로 만들어진 스레드를 특정한다.

18.1. Simple inter-thread control.

위의 예에서 thrd_join은 스레드가 다른 스레드가 종료될 때까지 기다리도록 한다. 이는 main에서 쓰이며, main 스레드가 다른 4 스레드가 모두 종료되었을 때만 종료되도록 보장한다. 다른 예로 life 구조체의 finished 필드가 있는데, 이는 bool 타입으로서 다른 스레드들이 게임을 종료할 조건을 감지했을 때 true로 세팅된다. 신호 핸들러와 비슷하게, 이렇게 스레드 간 공유되는 변수는 매우 조심스럽게 다루어져야 한다.

Takeaway 3.18.1.1. 스레드 하나가 비원자적 객체에 쓰기를 시도하는 동시에 그 객체가 다른 스레드에 의해 읽기나 쓰기가 이루어지고 있다면, 실행의 동작은 정의되지 않는다.

사실 서로 다른 스레드간 동시에 오브젝트에 접근한다는 것을 엄밀하게 정의하는 것도 쉽지 않다. 유일한 선택은 그런 위험이 발생할 가능성(레이스 컨디션)을 원천적으로 차단하는 것뿐이다. 그래서 위의 예에서는 life 구조체에서 여러 스레드에 의해 동시에 접근될 수 있는 멤버 필드들은 원자적(_Atomic)으로 선언하고 있다.

Takeaway 3.18.1.2. 다른 스레드간 실행의 관점에서, 원자적 객체에 대한 표준 연산들은 선형화 가능하며 쪼개질 수 없다.

여기서 선형화는 서로 다른 스레드간 연산의 순서까지 논할 수 있다는 것을 의미한다. 그러므로 원자적 객체에 대한 연산들은 스레드들의 어떤 부분이 동시에 수행되지 않는지까지도 판단할 수 있게 해 준다.

원자적 객체들은 일반적인 객체들과는 다르므로 원자적 특정자 _Atomic을 붙여 줘야 한다. 이 때 A, B를 선언할 때 다음의 문법들은 동일하다. A는 double 45개짜리 배열에 대한 원자적 포인터이고 B는 원자적 double 45개짜리 배열에 대한 포인터이다.

extern _Atomic (double (*)[45]) A;
extern double (* _Atomic A)[45];
extern _Atomic (double) (*B)[45];
extern double _Atomic (*B)[45];

다음과 같은 규칙도 있다.

double var ;
// Valid : adding const qualification to the pointed -to type
extern double const * c = & var ;
// Valid : adding volatile qualification to the pointed -to type
extern double volatile * v = & var ;
// Invalid : pointers to incompatible types
extern double _Atomic * a = & var ;

Takeaway 3.18.1.3. _Atomic(T) 특정자는 원자적 선언으로 써라.

다른 규칙은 배열에는 원자성이 적용될 수 없다는 것이다.

_Atomic ( double [45]) C; // Invalid : atomic cannot be applied to arrays .
_Atomic ( double ) D [45]; // Valid : atomic can be applied to array base

typedef double darray [45];
// Invalid : atomic cannot be applied to arrays .
darray _Atomic E;
// Valid : const can be applied to arrays .
darray const F = { 0 }; // Applies to base type
double const F [45]; // Compatible declaration

Takeaway 3.18.1.4. 원자적 배열 타입은 없다.

Takeaway 3.18.1.5. 원자적 객체들은 레이스 컨디션이 일어나지 않음을 강제하는 강력한 도구이다.

18.2. Race-free initialization and destruction.

스레드간 공유되는 데이터는 동시적인 접근이 이루어지기 전에 잘 정의된 상태로 세팅됨이 보장되어야 하고, 소멸된 뒤에 절대로 접근이 이루어지지 않음을 보장해야 한다. 이를 위해 초기화 단계에서는 다음의 방법들이 존재한다.

  • 정적 저장소 주기를 갖는 공유 객체는 임의의 실행 이전에 초기화된다.
  • 자동 또는 할당 저장소 주기를 갖는 공유 객체는 그를 생성하는 스레드에 의해 공유된 접근이 일어나기 전 초기화를 보장할 수 있다.
  • 정적 저장소 주기를 갖는 공유 객체가 동적 초기화에 의해 정보가 세팅될 때에는, 그것이 시작 시에 가능할 때에는 다른 스레드가 만들어지기 전에 main에 의해 초기화되어야 하고, 그렇지 않을 때에는 반드시 call_once에 의해 초기화되어야 한다.
void call_once(once_flag* flag, void cb(void));

atexit과 비슷하게, call_once는 콜백 함수 cb 함수를 호출한다. 이 함수는 실행에 있어 단 한 번만 호출되어야 한다. 이 콜백 함수는 once_flag이라는 플래그 오브젝트와 연관되어 있는데 이를 통해서 call_once가 모든 스레드간에 최초에 불리는지, 한 번만 불리는지를 보장하고 이 콜백이 실행될 동안 다른 스레드들을 중지한다. 그래서 모든 스레드들은 레이스 컨디션 없이 해당 오브젝트의 초기화가 보장된 채로 이를 사용할 수 있다.

Takeaway 3.18.2.1. 잘 초기화된 FILE* 은 여러 스레드간 레이스 컨디션 없이 사용 가능하다.

주의할 점은 이는 단지 정의되지 않은 행동이 일어나지 않는다는 것을 보장할 뿐이란 것이다. 다른 스레드간 입력이 엉켜서 파일의 내용물이 당신이 원하지 않는 대로 나올 수도 있다.

Takeaway 3.18.2.2. 동시적인 쓰기 연산은 반드시 전체 라인을 한 번에 출력해야 한다.

레이스 컨디션 없는 오브젝트의 소멸은 훨씬 강제하기 힘들다. 오브젝트의 생성과 소멸은 비대칭적이기 때문이다.

Takeaway 3.18.2.3. 공유되는 동적 오브젝트의 소멸과 할당 해제는 특별히 신경써야 한다.

위의 예에서는 변수 L이 모든 스레드가 조인된 이후에만 소멸되도록 함으로써 안전성을 보장한다. once_flag에 대해서는 스트림을 언제 닫아야 하는지 판단하기 힘들다. 가장 쉬운 방법은 모든 스레드가 종료되고 그 후에 전체 프로그램이 종료되기 전에만 스트림을 닫는 것이다.

18.3. Thread-local data.

레이스 컨디션을 피하는 가장 쉬운 방법은 스레드들이 접근하는 데이터를 분리하는 것이다. 다른 원자적 객체나 뮤텍스, 조건 변수 등은 매우 복잡하고 훨씬 연산량이 많이 든다. 스레드에 대해 지역적인 데이터를 접근하는 최선의 방법은 지역 변수를 쓰는 것이다.

Takeaway 3.18.3.1. 스레드 종속적 데이터는 함수 인자로 전달하라.

Takeaway 3.18.3.2. 스레드 종속적 상태는 지역 변수로 쓰라.

이것이 가능하지 않은 경우에는 _Thread_local (thread_local) 특정자로 스레드 지역 저장소 주기, 즉 스레드별로 따로 선언되는 생애 주기를 갖는 오브젝트를 선언한다.

Takeaway 3.18.3.3. thread_local 변수는 스레드별로 각각 하나의 인스턴스를 갖는다.

그러므로 thread_local 변수는 정적 주기 변수와 비슷하게 다뤄져야 한다. 파일 스코프에 선언하거나, static으로 선언해야 한다. 그러므로, 이들은 동적으로 초기화될 수 없다.

Takeaway 3.18.3.4. thread_local은 초기화가 컴파일 타임에 결정될 수 있을 때만 쓰라.

동적 초기화를 해야 할 때는 스레드-특정 저장소, tss_t를 사용해야 한다.

void * tss_get ( tss_t key ); // Returns a pointer to an object
int tss_set ( tss_t key , void * val ); // Returns an error indication
typedef void (* tss_dtor_t )( void *); // Pointer to a destructor
int tss_create ( tss_t * key , tss_dtor_t dtor ); // Returns an error
 indication
void tss_delete ( tss_t key );

18.4. Critical data and critical sections.

위의 예에서 life 구조체의 다른 부분들은 쉽게 보호될 수 없다. 이들은 게임의 보드 위치와 같은 더 큰 데이터에 대응되기 때문이다. 일단 배열은 _Atomic으로 선언할 수 없고, 트릭을 써 비슷하게 구현하더라도 비효율적이다. 그러므로 특별한 멤버 필드 뮤텍스 (mtx_t)를 정의한다.

#include <stdbool.h>
#include <ctype.h>
#ifndef __STDC_NO_THREADS__
# include <threads.h>
#else
# error This needs C11 threads, aborting.
#endif
#include <stdatomic.h>


enum { life_maxit = 1ull << 23, };

typedef struct life life;
struct life {
  mtx_t mtx;    //< Mutex that protects Mv
  cnd_t draw;   //< cnd that controls drawing
  cnd_t acco;   //< cnd that controls accounting
  cnd_t upda;   //< cnd that controls updating

  void*restrict Mv;            //< bool M[n0][n1];
  bool (*visited)[life_maxit]; //< Hashing constellations

  // A bunch of parameters that are not subject to change
  size_t n0;    //< Number of rows
  size_t n1;    //< Number of columns
  size_t off0;  //< Start row
  size_t len0;  //< Number of rows to be handled
  size_t off1;  //< Start column
  size_t len1;  //< Number of columns to be handled
  void* restrict Bv;  //< Unsigned char B[len0][len1];


  // Parameters that are updated but that are protected by the mutex
  size_t iteration;   //< Current iteration
  size_t accounted;   //< Last accounted iteration
  size_t drawn;       //< Last drawn iteration
  size_t last;        //< Last iteration with new state
  size_t birth9;      //< Number of birth9 calls

  // Parameters that will dynamically be changed by
  // different threads
  _Atomic(size_t) constellations; //< Constellations visited
  _Atomic(size_t) x0;             //< Cursor position, row
  _Atomic(size_t) x1;             //< Cursor position, column
  _Atomic(size_t) frames;         //< FPS for display
  _Atomic(bool)   finished;       //< This game is finished.
};

inline
void life_advance(life* L, signed t0, signed t1) {
  if (t0) {
    size_t n = L->n0-2;
    L->x0 = ((L->x0-1)+n+t0)%n + 1;
  }
  if (t1) {
    size_t n = L->n1-2;
    L->x1 = ((L->x1-1)+n+t1)%n + 1;
  }
}

int life_wait(cnd_t*, mtx_t*);
void life_sleep(double s);

void life_birth9(life*);
size_t life_update(life*restrict L);
void life_count(life*restrict L);
void life_draw(life*);
void life_draw4(life*);
void life_account(life*);
life* life_init(life*, size_t _n, size_t _m, bool[_n][_m]);
void life_destroy(life* L);
void life_torus(life*);

#define LIFE_INITIALIZER                        \
{                                               \
    .visited = 0,                               \
    .birth9 = 1,                                \
    .x0 = 1,                                    \
    .x1 = 1,                                    \
    .frames = 20,                               \
    .finished = false,                          \
    .constellations = 1,                        \
    .birth9 = 1,                                \
}

이것은 한계 섹션에서 한계 데이터를 보호하기 위해 쓰인다. mtx_lock으로 락을 걸어서 다른 스레드의 호출을 막고 mtx_unlock으로 락을 푼다.

Takeaway 3.18.4.1. 뮤텍스 연산은 선형성을 제공한다.

C에서의 뮤텍스 락 인터페이스는 다음과 같다.

int mtx_lock ( mtx_t *);
int mtx_unlock ( mtx_t *);
int mtx_trylock ( mtx_t *);
int mtx_timedlock ( mtx_t * restrict , const struct timespec * restrict );
int mtx_init ( mtx_t *, int );
void mtx_destroy ( mtx_t *);

mtx_trylock은 다른 스레드가 이미 락을 획득했는지를 테스트한다. mtx_timedlock은 일정 시간 동안만 락을 기다려서 무한정 기다리게 되는 사태를 막는다. 이는 뮤텍스가 mtx_timed 타입에 의해 초기화되었을 때만 쓸 수 있다. 초기화는 mtx_init으로만 해야 한다. 뮤텍스는 정적 초기화가 불가능하다.

Takeaway 3.18.4.2. 모든 뮤텍스는 mtx_init으로만 초기화해야 한다.

mtx_init의 두번째 인자는 뮤텍스의 타입을 특정한다. mtx_plain, mtx_timed, mtx_plain | mtx_recursive, mtx_timed | mtx_recursive의 4가지이다. mtx_recursive는 같은 스레드에서 해당 뮤텍스를 여러 번 락할 수 있게 해 준다.

Takeaway 3.18.4.3. 비재귀적 뮤텍스를 획득한 스레드는 그 뮤텍스에 대한 락을 절대 호출해서는 안 된다.

Takeaway 3.18.4.4. 재귀적 뮤텍스는 그 뮤텍스를 획득한 스레드에서 mtx_unlock이 락을 건 횟수만큼 호출된 경우에만 반환된다.

Takeaway 3.18.4.5. 락된 뮤텍스는 스레드의 종료 이전 반드시 반환되어야 한다.

Takeaway 3.18.4.6. 스레드는 그 스레드가 획득한 뮤텍스에 대해서만 mtx_unlock을 호출해야 한다.

Takeaway 3.18.4.7. 각각의 성공적인 뮤텍스 락은 정확히 한개의 mtx_unlock에 대응된다.

mtx_init이 호출될 때 플랫폼에 따라 시스템 자원에 바인딩될 수 있다. 따라서 해당 뮤텍스의 생애 주기가 끝날 때 그 자원을 반드시 반환해줘야 한다.

Takeaway 3.18.4.8. 뮤텍스는 반드시 생애 주기가 끝날 때 소멸되어야 한다.

즉, mtx_destroy는 다음 경우 반드시 호출되어야 한다.

  • 자동 주기를 가진 뮤텍스의 스코프가 끝나기 전
  • 동적 할당된 뮤텍스의 메모리가 반환되기 전

18.5. Communicating through conditional variables.

스레드의 실행 내부 한계 섹션에 앞서, 새 동작이 실행되기 전 스레드가 중단되는 과정이 필요하다. 아래의 예에서는 조건 변수를 통해 어떤 이벤트가 발생할 때까지 스레드를 1초간 중지시킨다.

#include "life.h"
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include "termin.h"


void life_sleep(double s) {
  struct timespec duration = {
    .tv_sec = s,
    .tv_nsec = (s-(size_t)s)*1E9,
  };
  while (thrd_sleep(&duration, &duration)) {
    // Empty
  }
}

int life_wait(cnd_t* cnd, mtx_t* mtx) {
  struct timespec now;
  timespec_get(&now, TIME_UTC);
  now.tv_sec += 1;
  return cnd_timedwait(cnd, mtx, &now);
}

void life_advance(life* L, signed t0, signed t1);

static
bool life_alive(bool l, unsigned x) {
  return x == 3 || (l && x ==2);
}

// Computes the number of neighbors and stores them in B
void life_count(life* L) {
  size_t const n1 = L->n1;
  bool (*const restrict M)[n1] = (void*)L->Mv;
  size_t const off0 = L->off0;
  size_t const off1 = L->off1;
  size_t const len0 = L->len0;
  size_t const len1 = L->len1;
  if (!L->Bv) L->Bv =  malloc(sizeof(unsigned char[len0][len1]));
  unsigned char (*restrict B)[len1] = L->Bv;
  for (size_t j0 = 0; j0 < len0; ++j0) {
    size_t i0 = off0 + j0;
    for (size_t j1 = 0; j1 < len1; ++j1) {
      size_t i1 = off1 + j1;
      B[j0][j1] =
        + M[i0-1][i1-1] + M[i0-1][i1] + M[i0-1][i1+1]
        + M[i0  ][i1-1] + 0           + M[i0  ][i1+1]
        + M[i0+1][i1-1] + M[i0+1][i1] + M[i0+1][i1+1];
    }
  }
}

size_t life_update(life* L) {
  size_t const n1 = L->n1;
  bool (*const restrict M)[n1] = (void*)L->Mv;
  size_t const off0 = L->off0;
  size_t const off1 = L->off1;
  size_t const len0 = L->len0;
  size_t const len1 = L->len1;
  unsigned char (*restrict B)[len1] = L->Bv;
  size_t changed = 0;
  if (B) {
    for (size_t j0 = 0; j0 < len0; ++j0) {
      size_t i0 = off0 + j0;
      for (size_t j1 = 0; j1 < len1; ++j1) {
        size_t i1 = off1 + j1;
        bool newval = life_alive(M[i0][i1], B[j0][j1]);
        changed += (M[i0][i1] != newval);
        M[i0][i1] = newval;
      }
    }
  }
  L->Bv = 0;
  free(B);
  return changed;
}

void life_torus(life* L) {
  size_t const n0 = L->n0;
  size_t const n1 = L->n1;
  bool (*const restrict M)[n1] = L->Mv;
  for (size_t i0 = 1; i0 < n0; ++i0) {
    M[i0][0]    = M[i0][n1-2];
    M[i0][n1-1] = M[i0][1];
  }
  for (size_t i1 = 1; i1 < n1; ++i1) {
    M[0]   [i1] = M[n0-2][i1];
    M[n0-1][i1] = M[1]   [i1];
  }
  M[0]   [0]    = M[n0-2][n1-2];
  M[n0-1][0]    = M[1]   [n1-2];
  M[0]   [n1-1] = M[n0-2][1];
  M[n0-1][n1-1] = M[1]   [1];
}



#define DOT "⦿"
#define SPACE " "
#define WROOK ESC_TBLUE "♖" ESC_NORMAL
#define BROOK ESC_TBLUE "♜" ESC_NORMAL

static
void border(size_t m, int b) {
  fputs(ESC_TRED, stdout);
  fputs(ESC_BORDER[b & ~esc_right], stdout);
  for (size_t j = 0; j < m; ++j)
    fputs("━", stdout);
  fputs(ESC_BORDER[b & ~esc_left], stdout);
  fputs(ESC_NORMAL, stdout);
  esc_move(stdout, 1, -(m+2));
}

void life_draw(life* L) {
  size_t const n1 = L->n1;
  bool (*const restrict M)[n1] = L->Mv;
  size_t const x0 = L->x0;
  size_t const x1 = L->x1;
  size_t const off0 = L->off0;
  size_t const off1 = L->off1;
  size_t const len0 = L->len0;
  size_t const len1 = L->len1;

  fputs(ESC_SAVE ESC_HOME ESC_HIDE, stdout);
  border(len1, esc_top|esc_right|esc_left);
  for (size_t i0 = off0; i0 < off0+len0; ++i0) {
    fputs(ESC_TRED, stdout);
    fputs(ESC_BORDER[esc_left], stdout);
    fputs(ESC_NORMAL, stdout);
    for (size_t i1 = off1; i1 < off1+len1; ++i1) {
      if (i1 == x1 && x0 == i0) fputs(M[i0][i1] ? WROOK : BROOK, stdout);
      else fputs(M[i0][i1] ? DOT : SPACE, stdout);
    }
    fputs(ESC_TRED, stdout);
    fputs(ESC_BORDER[esc_right], stdout);
    fputs(ESC_NORMAL, stdout);
    esc_move(stdout, 1, -(len1+2));
  }
  border(len1, esc_bottom|esc_right|esc_left);
  esc_goto(stdout, len0+3, 1);
  fprintf(stdout, ESC_CLEAR "%3zu FPS, %5zu iterations, %5zu birth9, %5zu constellations, " ESC_BOLD "%6.2f" ESC_NORMAL " quotient",
          L->frames, L->iteration, L->birth9, L->constellations, L->constellations*1.0/L->birth9);
  fputs(ESC_RESTORE ESC_SHOW, stdout);
  esc_goto(stdout, len0+4, 1);
}

void life_draw4(life* L) {
  size_t const n1 = L->n1;
  bool (*const restrict M)[n1] = L->Mv;
  size_t const x0 = L->x0;
  size_t const x1 = L->x1;
  size_t const off0 = L->off0;
  size_t const off1 = L->off1;
  size_t const len0 = L->len0;
  size_t const len1 = L->len1;

  fputs(ESC_SAVE ESC_HOME ESC_HIDE, stdout);
  border(len1/2, esc_top|esc_right|esc_left);
  for (size_t i0 = off0; i0 < off0+len0; i0 += 2) {
    fputs(ESC_TRED, stdout);
    fputs(ESC_BORDER[esc_left], stdout);
    fputs(ESC_NORMAL, stdout);
    for (size_t i1 = off1; i1 < off1+len1; i1 += 2) {
      char const* str = 0;
      if ((i1 == x1 || i1+1 == x1) && (x0 == i0 || x0 == i0+1)) {
        // The cursor is a blue dot in the correct position, but the
        // three cell positions are also hidden. We grey them out.
        static char const*const corner[4] = {
          [0] = ESC_TBLUE ESC_BGREY "▗" ESC_NORMAL,
          [1] = ESC_TBLUE ESC_BGREY "▖" ESC_NORMAL,
          [2] = ESC_TBLUE ESC_BGREY "▝" ESC_NORMAL,
          [3] = ESC_TBLUE ESC_BGREY "▘" ESC_NORMAL,
        };
        unsigned ty = (x0%2)<<1 | (x1%2);
        str = corner[ty];
      } else {
        unsigned val = M[i0][i1]
          | (M[i0][i1+1] << 1)
          | (M[i0+1][i1] << 2)
          | (M[i0+1][i1+1] << 3);
        str = ESC_BLOCK[val];
      }
      fputs(str, stdout);
    }
    fputs(ESC_TRED, stdout);
    fputs(ESC_BORDER[esc_right], stdout);
    fputs(ESC_NORMAL, stdout);
    esc_move(stdout, 1, -(len1/2+2));
  }
  border(len1/2, esc_bottom|esc_right|esc_left);
  esc_goto(stdout, len0/2+3, 1);
  fprintf(stdout, ESC_CLEAR "%3zu FPS, %5zu iterations, %5zu birth9, %5zu constellations, " ESC_BOLD "%6.2f" ESC_NORMAL " quotient",
          L->frames, L->iteration, L->birth9, L->constellations, L->constellations*1.0/L->birth9);
  fputs(ESC_RESTORE ESC_SHOW, stdout);
  esc_goto(stdout, len0+4, 1);
}

static
bool hashin(bool (*visited)[life_maxit], size_t hash) {
  bool ret = false;
  hash %= life_maxit;
  if (!(*visited)[hash]) {
    (*visited)[hash] = true;
    ret = true;
  }
  return ret;
}

void life_account(life* L) {
  size_t const n = L->n0;
  size_t const m = L->n1;
  bool (*const restrict M)[m] = L->Mv;
  // Computes a hash of the new state
  size_t hash = 0;
  for (size_t i = 1; i < n-2; ++i) {
    for (size_t j = 1; j < m-2; ++j) {
      hash = 37*hash + M[i][j] + 7;
    }
  }
  bool is_new = hashin(L->visited, hash);
  if (is_new) {
    L->constellations++;
    L->last = L->accounted;
  }
  L->accounted++;
}

void life_birth9(life* L) {
  size_t const n0 = L->n0;
  size_t const n1 = L->n1;
  size_t const x = L->x0;
  size_t const y = L->x1;
  bool (*const restrict M)[n1] = L->Mv;
  for (size_t i = n0-4; i < n0-1; ++i) {
    size_t xi = ((x+i)%(n0-2)) + 1;
    for (int j = n1-4; j < n1-1; ++j) {
      size_t yj = ((y+j)%(n1-2)) + 1;
      M[xi][yj] = true;
    }
  }
  L->birth9++;
}

life* life_init(life* L, size_t n, size_t m, bool mat[static n][m]) {
  if (L) {
    L->n0 = n;
    L->n1 = m;
    L->off0 = 1;
    L->len0 = n-2;
    L->off1 = 1;
    L->len1 = m-2;
    L->visited = calloc(sizeof(bool), life_maxit);
    L->Mv = mat;
    mtx_init(&L->mtx, mtx_plain);
    cnd_init(&L->draw);
    cnd_init(&L->acco);
    cnd_init(&L->upda);
  }
  return L;
}

void life_destroy(life* L) {
  if (L) free(L->visited);
}

이는 cnd_timedwait으로 조건 변수 cnd_t와 뮤텍스와 시간 제한을 받아 대기를 수행한다.

Takeaway 3.18.5.1. cnd_t 대기에서 리턴이 발생할 때 해당 표현식은 반드시 다시 체크되어야 한다.

cnd_t는 다음의 4가지 인터페이스로 쓸 수 있다.

int cnd_wait ( cnd_t *, mtx_t *);
int cnd_timedwait ( cnd_t * restrict , mtx_t * restrict , const struct timespec *
restrict );
int cnd_signal ( cnd_t *);
int cnd_broadcast ( cnd_t *);

cnd_wait은 시간 제한을 걸지 않으며 cnd_t 변수가 동작되지 않으면 영원히 대기만 할 수 있다. cnd_signal과 cnd_broadcast는 조건 변수를 대기하는 하나나 모든 스레드를 깨운다. cnd_timedwait에서 뮤텍스가 하는 역할은 중요한데, 이 뮤텍스는 대기 함수를 호출하는 스레드에서 반드시 획득한 상태여야 한다. 이는 대기 동안 잠시 반환되어 다른 스레드가 조건 변수를 검증할 수 있게 허용하고, 대기 호출이 반환되기 전 다시 획득되어 한계 데이터가 레이스 컨디션 없이 접근될 수 있도록 한다.

Takeaway 3.18.5.2. 조건 변수는 오직 하나의 뮤텍스와 동시에 사용될 수 있다.

그러나 조건 변수에 쓰이는 뮤텍스는 한번 설정한 뒤 바꾸지 않는 것이 좋다. 그러나 한 뮤텍스에 여러 조건 변수를 할당하는 것은 가능하다. 여러 조건 변수가 같은 자원을 공유해 다룰 수도 있기 때문이다. 여러 스레드가 같은 조건 변수를 대기하는 과정에서 cnd_broadcast로 깨워지더라도, 이 스레드들이 동시에 깨워지는 것은 아니며 뮤텍스를 재획득하는 순서로 순차적으로 깨워진다. 뮤텍스와 마찬가지로 조건 변수도 시스템 자원에 바인딩될 수 있으므로, 객체 생애 주기가 끝나기 전에 반드시 그 자원을 반환해야 한다.

Takeaway 3.18.5.3. cnd_t는 반드시 동적으로 초기화되어야 한다.

Takeaway 3.18.5.4. cnd_t는 생애 주기가 끝나기 전 반드시 소멸되어야 한다.

인터페이스는 간단하다.

int cnd_init ( cnd_t * cond );
void cnd_destroy ( cnd_t * cond );

18.6. More sophiscated thread management.

스레드 간 서열 관계는 없다. 하나를 제외하고.

Takeaway 3.18.6.1. main으로부터의 리턴이나 exit의 호출은 모든 스레드를 종료시킨다.

위의 예에서 L을 전역 변수로 만들어 main이 종료될 때 소멸되지 않게 하고 싶으면 main이 종료될 때 콜백 함수를 atexit 핸들러에 등록해서 해당 함수가 자원 정리를 하게 하면 된다.

#include "life.h"
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdatomic.h>
#include <limits.h>
#include "termin.h"

// The keys that are used for cursor movement.
// Other keys that are use are
// b, B, space for birth9
// -, + to slow down and accelerate
// q, Q to quit
#define GO_UP 'k'
#define GO_DOWN 'l'
#define GO_RIGHT ';'
#define GO_LEFT 'j'
#define GO_HOME 'h'

// We translate escape sequences that we may receive to these standard
// characters for further processing.

#define ESCAPE '\e'

char const*const termin_trans[UCHAR_MAX+1] = {
  [GO_UP] = ESC_UP,
  [GO_DOWN] = ESC_DOWN,
  [GO_RIGHT] = ESC_FRWD,
  [GO_LEFT] = ESC_BKWD,
  [GO_HOME] = ESC_HOME,
};

static int account_thread(void*);

static
int update_thread(void* Lv) {
  /* Nobody should ever wait for this thread. */
  thrd_detach(thrd_current());
  /* Delegates part of our job to an auxiliary thread */
  thrd_create(&(thrd_t){0}, account_thread, Lv);
  life*restrict L = Lv;
  size_t changed = 1;
  size_t birth9 = 0;
  while (!L->finished && changed) {
    // block until there is work
    mtx_lock(&L->mtx);
    while (!L->finished && (L->accounted < L->iteration))
      life_wait(&L->upda, &L->mtx);

    // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
    if (birth9 != L->birth9) life_torus(L);
    life_count(L);
    changed = life_update(L);
    life_torus(L);
    birth9 = L->birth9;
    L->iteration++;
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    cnd_signal(&L->acco);
    cnd_signal(&L->draw);
    mtx_unlock(&L->mtx);

    life_sleep(1.0/L->frames);
  }
  /* Tells everybody that the game is over */
  L->finished = true;
  ungetc('q', stdin);
  return 0;
}

static
int draw_thread(void* Lv) {
  /* Nobody should ever wait for this thread. */
  thrd_detach(thrd_current());
  life*restrict L = Lv;
  size_t x0 = 0;
  size_t x1 = 0;
  fputs(ESC_CLEAR ESC_CLRSCR, stdout);
  while (!L->finished) {
    // Blocks until there is work
    mtx_lock(&L->mtx);
    while (!L->finished
           && (L->iteration <= L->drawn)
           && (x0 == L->x0)
           && (x1 == L->x1))
      life_wait(&L->draw, &L->mtx);

    // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
    if (L->n0 <= 30) life_draw(L);
    else life_draw4(L);
    L->drawn++;
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    mtx_unlock(&L->mtx);

    x0 = L->x0;
    x1 = L->x1;
    // no need to draw too quickly
    life_sleep(1.0/40);
  }
  return 0;
}

// This number of consecutive states that are already known (that is,
// hashed in) decides that this sequence of states is cyclic.
enum { repetition = 10, };

static
int account_thread(void* Lv) {
  /* Nobody should ever wait for this thread. */
  thrd_detach(thrd_current());
  life*restrict L = Lv;
  while (!L->finished) {
    // Blocks until there is work
    mtx_lock(&L->mtx);
    while (!L->finished && (L->accounted == L->iteration))
      life_wait(&L->acco, &L->mtx);

    // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
    life_account(L);
    if ((L->last + repetition) < L->accounted) {
      L->finished = true;
    }
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    cnd_signal(&L->upda);
    mtx_unlock(&L->mtx);
  }
  return 0;
}

static
int input_thread(void* Lv) {
  /* Nobody should ever wait for this thread. */
  thrd_detach(thrd_current());
  termin_unbuffered();
  life*restrict L = Lv;
  enum { len = 32, };
  char command[len];
  do {
    int c = getchar();
    command[0] = c;
    switch(c) {
    case GO_LEFT : life_advance(L,  0, -1); break;
    case GO_RIGHT: life_advance(L,  0, +1); break;
    case GO_UP   : life_advance(L, -1,  0); break;
    case GO_DOWN : life_advance(L, +1,  0); break;
    case GO_HOME : L->x0 = 1; L->x1 = 1;    break;
    case ESCAPE  :
      ungetc(termin_translate(termin_read_esc(len, command)), stdin);
      continue;
    case '+':      if (L->frames < 128) L->frames++; continue;
    case '-':      if (L->frames > 1)   L->frames--; continue;
    case ' ':
    case 'b':
    case 'B':
      mtx_lock(&L->mtx);
      // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
      life_birth9(L);
      // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      mtx_unlock(&L->mtx);
      break;
    case 'q':
    case 'Q':
    case EOF:      goto FINISH;
    }
    cnd_signal(&L->draw);
  } while (!(L->finished || feof(stdin)));
 FINISH:
  L->finished = true;
  return 0;
}

enum { n = 60, m = 160 };

bool M[n][m] = {
 { 0 },
 { 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0 },
 { 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, },
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, },
 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
 { 0 },
 { 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
 { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 },
 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
 { 0 },
};

life L = LIFE_INITIALIZER;

void B9_atexit(void) {
  /* Puts the board in a nice final picture */
  L.iteration = L.last;
  life_draw(&L);
  life_destroy(&L);
}

int main(int argc, char* argv[argc+1]) {
  /* Uses command-line arguments for the size of the board */
  size_t n0 = 30;
  size_t n1 = 80;
  if (argc > 1) n0 = strtoull(argv[1], 0, 0);
  if (argc > 2) n1 = strtoull(argv[2], 0, 0);
  /* Create an object that holds the game's data. */
  life_init(&L, n0, n1, M);
  atexit(B9_atexit);
  /* Creates four threads that operate on the same object and
     discards their IDs */
  thrd_create(&(thrd_t){0}, update_thread,  &L);
  thrd_create(&(thrd_t){0}, draw_thread,    &L);
  thrd_create(&(thrd_t){0}, input_thread,   &L);
  /* Ends this thread nicely and lets the threads go on nicely */
  thrd_exit(0);
}

조인되지 않은 스레드는 프로그램의 종료 시점까지 획득한 자원을 들고 있다. 그러므로 스레드를 떼어내서 시스템에 해당 스레드는 조인되지 않는다는 것을 알릴 수 있다. 인터페이스들은 다음과 같다.

thrd_t thrd_current ( void );
int thrd_equal ( thrd_t , thrd_t );
_Noreturn void thrd_exit ( int );
int thrd_detach ( thrd_t );
int thrd_sleep ( const struct timespec *, struct timespec *);
void thrd_yield ( void );

Takeaway 3.18.6.2. mtx_t와 cnd_t를 대기하고 있을 때, 스레드는 프로세싱 자원을 반환한다.

이것이 충분치 않다면 실행을 연기할 수 있는 두 가지 함수가 있다.

  • thrd_sleep은 특정 시간 동안 스레드를 잠재운다. 이 동안 하드웨어 자원은 다른 스레드에 의해 사용될 수 있다.
  • thrd_yield는 현재 시간 슬라이스를 종료하고 다른 프로세싱 기회를 기다린다.

요점 정리

  • 공유 데이터는 동시적으로 접근되기 전에 적절히 초기화되었음을 보장해야 한다. 이는 컴파일 타임에 하거나 main에서 하는 것이 최선이다.
  • 스레드는 자동 주기 변수나 함수 인자를 통해 지역 변수에만 연산을 하는 것이 바람직하다. 피할 수 없다면 thread_local 객체를 만들거나 tss_create로 스레드별로 해당 데이터를 만든다. 후자는 변수의 동적 할당/해제가 필요할 때만 쓴다.
  • 스레드간 공유되는 작은 한계 데이터는 _Atomic을 사용한다.
  • 한계 섹션은 반드시 보호되어야 한다. 보통 mtx_t 뮤텍스를 사용한다.
  • 스레드간 조건부 수행으로부터 오는 의존성은 cnd_t 조건 변수를 사용한다.
  • main 소멸 시 수행되는 자원 청소에 의존할 수 없는 스레드는 thrd_detach를 사용해야 하고 해당 스레드가 획득한 자원에 대한 청소는 atexit이나 at_quick_exit 핸들러에 등록해 수행해야 한다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중