자바 쓰레드풀의 상태관리

쓰레드풀의 상태를 나타내는 멤버변수인 ctl 에 대해  (코딩 스킬적으로) 살펴보자.
*  코딩 스킬적으로 
 하나의 int 변수 안에  길이가 다른 다양한 특성을 담아둘때 이런 기법을 이용하시면 된다. 

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // RUNNING 상태와 쓰레드 개수1로 초기화
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 00011111 11111111 11111111 11111111

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS; // 11100000 00000000 00000000 00000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

private static int ctlOf(int rs, int wc) { return rs | wc; } // 2가지 상태를 하나의 Int 로 합쳐주는 함수

이 ctl 는 하나의 변수 안에 2가지 내용을 패킹시켜 두었다. 

첫째. workerCount   - 
현재  워커(쓰레드) 의 개수  (앞의 3비트 이용)
둘째. runState - 쓰레드풀의 현재 상태 (running, shutting down 등등)  (뒤의 29비트 이용) 

위의 코드를 보면 ctlOf 메소드를 통해서 초기에는 (RUNNING,0) 으로 초기화 시켜 놓은 부분이 보일 것이다. 즉 초기 상태는 
workerCount 는 0 이고 runState 는 RUNNING 이라는 것이다. 앞쪽비트는 runState 로 사용하고, 뒤쪽 비트는 workerCount 로 사용 한다는 것인데, 맞는지 확인 해보자.

- CAPACITY ( -1 을 해주는 이유는 하위 모든 비트를 1로 만들기 위해서이다. 대략 5억으로 workCount 의 용량제한이다.)
(1<< 29) - 1 = 2^29 -1  =  536870911 = 0x1FFFFFFF = 00011111 11111111 11111111 11111111
 

- RUNNING 은 다음과 같다.
-1 << 29  =  -536870912 =   0xE0000000 = 11100000 00000000 00000000 00000000

위 쪽의 3비트와 나머지 비트로 나누어 진 것을 알 수 있다. 

따라서 ctl 의 초기 상태는 다음과 같다.
11100000 00000000 00000000 00000000


workerCount
하나의 Int 타입으로 2가지 내용을 패킹하기 위해서 먼저 workerCount 를 대략 5억개로 제한시켜두었고 

private static final int COUNT_BITS = Integer.SIZE - 3; // 32-3 = 29

private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 00011111 11111111 11111111 11111111

private static int workerCountOf(int c) { return c & CAPACITY; } // 현재 워커 갯수는?

비트마스킹이 조금이라도 더 빠르니깐 이렇게 한것이며, workerCount 는 워커 쓰레드의 숫자로써, 살아있는 쓰레드들의 실질적인 숫자로써 계속 달라 질 것이다.(예를들어 ThreadFactory 가 쓰레드 생성에 실패 했을 때, 종료되는 와중에도 여전히 태스크 처리를 수행 될 때 등등), 내부적으로 사용되는 workerCount와는 다르게 사용자에게 알려지는 풀의 크기는 worker 를 모아둔 workers(HashSet) 의 크기가 된다.  

현재 ctl 의 초기 상태는 다음과 같다. 이것이 workerCountOf 의 매개변수인 c 로 들어가게 되는데 

11100000 00000000 00000000 00000000   (ctl)
 00011111 11111111 11111111 11111111 (capacity)

이 둘을 비트연산(&)하면 0 이다. 맞는지 확인 해보자.
int c = ctl.get();   
int ws = workerCountOf(c); // ws 는 0

runState
RUNNING: 새로운 태스크를 받고,  큐에 집어 넣는 일을 하라.
SHOUTDOWN: 새로운 태스크를 받지마라, 그러나 이미 큐에 있는 태스크는 처리하라
STOP: 새로운 태스크를 받지마라, 큐에 있는 태스크도 처리하지 않는다. 현재 진행중인 태스크에 인터럽트를 건다.
TIDYING: 모든 태스크는 소멸되었고, workerCount 는 0 이다. TIDYING 상태로 전이되는 쓰레드는 terminated() 훅 메소드를 실행 시킬 것이다. 
TERMINATED: terminated() 메소드가 완료 되었다. 

전이형태는 다음과 같다.
RUNNING -> SHUTDOWN    ( shoutdown() 메소드 호출시)
(RUNNING or SHUTDOWN) -> STOP ( shoutdownNow() 메소드 호출시)
SHUTDOWN -> TIDYING ( 풀과 큐가 비었을 때)
STOP -> TIDYING (풀이 비었을 때) 
TYDYING -> TERMINATED (terminated() 메소드가 완료 되었을 때)


소프트웨어 엔지니어링에서 풀의 종류는 다양한데요.

쓰레드풀,메모리풀,캐쉬풀,커넥션풀,객체풀 (자바에서 객체풀은 사용을 지양합니다. 메모리를 할당하는 작업이 C/C++보다 빠름) 등등이 있습니다. "풀"어서 말하면 미리 만들어두고 돌려막기로 사용하자 라고 볼 수 있는데요. 미리 만들어 두는 방식 / 쓰레드가 태스크를 처리하는 방식에 따라서 다양한 풀의 구현체들이 있을 수 있습니다.  이 글에서는 openJDK8 기준의 자바에서 구현된 newFixedThreadPool 를 해부해보도록 하겠습니다.  



쓰레드풀은 동일하고 서로 독립적인 다수의 작업을 실행 할 때 가장 효과적이다.실행 시간이 오래 걸리는 작업과 금방 끝나는 작업을 섞어서 실행하도록 하면 풀의 크기가 굉장히 크지 않은 한 작업 실행을 방해하는 것과 비슷한 상황이 발생한다. 또한 크기게 제한되어 있는 쓰레드 풀에 다른 작업의 내용에 의존성을 갖고 있는 작업을 등록하면 데드락이 발생할 가능성이 높다. 다행스컯게도 일반적인 네트웍 기반의 서버 어플리케이션 (웹서버,메일서버,파일서버등)은 작업이 서로 동일하면서 독립적이어야 한다는 조건을 대부분 만족한다.  -  Java concurrency in practice 책 발췌 


전설의 개발자들이 참여한 저 엄청난 책은 자신들이 참여한 java.util.concurrent 패키지를 기준으로 한다. 근데 이 책이 2006년에 JDK1.5 기준으로 쓰여졌기 때문에 "동일하고 서로 독립적인 다수의 작업" 이라고 한정지었는데, 후에 JDK1.7에서 FORK-JOIN 풀이 나오면서 기존 풀들에서는 할 수 없었던 분할 수행작업을 효율적으로 할 수 있는 보완재가 되었다.

                             


1.newFixedThreadPool 사용법 (자바8기준)


execute 메소드


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream; 
public class SimpleThreadPool {
  public static void main(String[] args) {   
  ExecutorService executor = Executors.newFixedThreadPool(10);   
  IntStream.range(0, 10).forEach( n -> executor.execute( () -> 
        {       
          try {         
                TimeUnit.MILLISECONDS.sleep(300);         
                String threadName = Thread.currentThread().getName();         
                System.out.println("Hello " + threadName);        
          } catch (InterruptedException e) {
             e.printStackTrace();        
          }     
      })   
  );  
}
}

- 쓰레드풀에 쓰레드를 10개를 뛰어놀게 한다. 
- 10번을 반복해서 쓰레드풀에 일을 시킨다. (쓰레드풀안의 쓰레드 하나가 선택되어 일처리를 할것이다)
- execute 메소드를 사용했다. 이 메소드는 void 를 리턴한다. 즉 일처리를 시키기만 하지 결과를 보고받지 않을것이다.
- 구현내용은 300밀리초를 기다렸다가 Hello 쓰레드이름을 출력하는 것이다. 

- 자바8부터는 for(int i = 0; i < 10; i++) 보다는 저렇게 사용하는게 좋다.


submit 메소드

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

public class SimpleThreadPool {

  public static void main(String[] args) {

    ExecutorService executor = Executors.newFixedThreadPool(2);
    final List<Integer> integers = Arrays.asList(1,2,3,4,5);
    Future<Integer> future = executor.submit(() -> {
      TimeUnit.MILLISECONDS.sleep(5000);
      int result = integers.stream().mapToInt(i -> i.intValue()).sum();
      return result;
    });

    try {
      Integer result = future.get();
      System.out.print("result: " + result);
      executor.shutdownNow();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (ExecutionException e) {
      e.printStackTrace();
    }
  }
}

- 쓰레드풀에 쓰레드를 10개를 뛰어놀게 한다. 
- 쓰레드풀에 하나의 일을 시킨다. (쓰레드풀안의 쓰레드 하나가 선택되어 일처리를 할것이다)
- submit 메소드를 사용했다. 이 메소드는 future 를 리턴한다. 즉 일처리를 시키기고, 그에 따른 결과를 보고받을 것이다.
- 구현내용은 5000밀리초를 기다렸다가 리스트안의 숫자들의 합을 리턴한다.

- 리턴 받은 future 로 부터 값을 얻는다. 여기서 get()메소드는 블럭된다. (타임아웃을 매개변수로 넣을 수도 있다) 아마 일처리를 하는 쪽의 쓰레드에서 일을 다 끝내고 set() 같은 것을 해 줄 때가지 블럭될거 같다.


이제 사용법은 알았으니 과연 쓰레드풀을 자바에서 어떻게 구현하고 있는지 확인 해보자.

@어떻게 10개의 쓰레드를 가진 풀을 만드는지? (newFixedThreadPool(10))
@어떻게 풀에서 쓰레드 하나를 할당해 주는지? (execute)

@쓰레드풀에서 할당 될 워커(쓰레드)가 없을 때 태스크가 어떻게 되는지?

@어떻게 future 를 리턴해주는지. (submit



2.newFixedThreadPool 소스 분석(자바8기준)


* 참고로 지면이 한정된 관계로 예외처리 포함해서 여러 로직들이 생략되었다.





(발췌: http://codepumpkin.com/threadpool-using-executor-framework/)




2-1. 어떻게 10개의 쓰레드를 가진 풀을 만드는지? (newFixedThreadPool(10))

public class Executors {

java/util/concurrent/Executors.java 라는 파일에는 Executors 라는 클래스가 있으며, 정적메소드로써 newFiexedThreadPool 이라는 것을 제공한다. 매개변수는 쓰레드풀에 들어갈 쓰레드들의 개수를 넣어준다.


public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}

퍼사드 역할 (Gof의 패턴중 하나로 내부 시스템의 복잡함을 감추고 사용자가 간편히 사용하도록 하는 의도)

을 하고있다. 그저 팩토리로써 내부의 ThreadPoolExecutor 객체를 대신 만들어주고 있는데, 매개변수로는 순서대로 살펴보자.


- corePoolSize : 풀 안에 유지되는 쓰레드 개수 (시작시) 

- maximumPoolSize : 풀에 유지되는 최대 쓰레드 개수

- keepAliveTime : corePoolSize 보다 쓰레드 개수가 많아 질 때, 새로운 테스크를 기다리기 위한 시간. 시간이 지나면 쓰레드를 없애서 corePoolSize 를 유지한다.

- unit :  keepAliveTime 시간단위

- workQueue : 실행 되기전에 홀드시켜 두는 태스크를 유지하는 큐. 쓰레드가 남지 않을 경우 여기 태스크를 넣는다.


시작개수가 1이고 최대개수가 1이면  newSingleThreadExecutor 이 되는것이고,

시작개수가 0이고 최대개수가 MAX_VALUE 가 되면, newCachedThreadPool 풀이라 한다.
우리는 최소,최대가 10개로 고정된 쓰레드풀을 만든 것이다.

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

java/util/concurrent/ThreadPoolExecutor.java 라는 파일에는 ThreadPoolExecutor라는 클래스가 있으며, 들어온 매개변수에 추가로 Executors.defaultThreadFactory()와 defaultHandler 를 추가 매개변수로 받아서 새 객체를 만든다.

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}


 Executors.defaultThreadFactory()
이게 먼가 쓰레드를 풀에 생성하는 것과 관련이 있는거 같아서 따라가 본다.
public static ThreadFactory defaultThreadFactory() {
    return new DefaultThreadFactory();
}
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
       
        return t;
    }
}

- 쓰레드 그룹이라는 것을 만들어 두고 있다. 자바는 하나의 객체안에 여러개의 쓰레드들을 그루핑하기 위한 편리한 방식을 제공하는데, 이 방식을 통해서suspend, resume, interrupt 같은 콜을 한방에 적용되게 할 수 있다.
- pool 의 첫번째 쓰레드라는 이름을 지정해주고 있다. "pool-1-thread-"
- newThread 라는 메소드를 제공하는거 봐서 역시 이 객체는 새로운 쓰레드를 생성하는 팩토리였던 것이다.
- 비데몬 쓰레드로 만든다. (메인쓰레드가 종료되도 종료되지 않고 살아있는)

결론적으로 위의 코드는 각종 세팅만 했지 실질적으로 쓰레드를 만들지는 않았다. 그럼 언제 만드는 것일까?

2-2. 어떻게 풀에서 쓰레드 하나를 할당해 주는지? (execute)

이번에는 executor.execute( 태스크 이 호출에 대해서 따라가 보자. 

public void execute(Runnable command) {

  int c = ctl.get(); 
  // 1 단계 
  if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
      return;

  }

  // 2 단계
    .... 생략 ....
}
위 소스에서 1단계만 살펴보자. 만약 corePoolSize 보다 작은 개수의 쓰레드가 풀 안에 생성되있다면 새로운 쓰레드를 만들어서 새로 들어온 태스크(위에 소스에서 command) 를 처리하고 리턴해준다. addWorker(command, true) 를 쫒아가보자.

private boolean addWorker(Runnable firstTask, boolean core) {
  .. 상태체크 부분 생략 .. 

  Worker w = null;

  w = new Worker(firstTask); // (1)
  final Thread t = w.thread;
  if (t != null) {

    workers.add(w); // (1)
    t.start();
    workerStarted = true;

  }

  return workerStarted;
}
쓰레드 생성 및 태스크 할당에 촛점을 맞추어 천천히 살펴보자.

1. Worker 객체를 만들고 태스크를 할당한다. 
  w = new Worker(firstTask); // (1)
Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}
Worker 생성자에서 반가운 손님을 보게되는데 newThread 를 통해서 태스크를 새로운 쓰레드에 할당하는 모습이다.
여기서 눈여겨 봐야 할 것이, 워커객체는 쓰레드를 참조하고 있으며, 역으로 쓰레드에도 this (워커객체)가 매개변수로 들어가는 것인데, 쓰레드는 워커객체 this 를 통해서 할당된 태스크에 대한 처리를 하리라는 것을 알 수 있다. 즉 쓰레드에 태스크가 직접적으로 할당되는 것이 아니라, 워커객체라는 대리자를 통해서 관리되는 것. 

2.쓰레드풀의  workers 컬렉션에 Worker 객체를 보관한다. 
workers.add(w);

즉 쓰레드풀은 쓰레드를 직접 관리하는게 아니라, 워커라는 대리자를 통해서 관리한다.

3. 쓰레드를 실행 시킨다. 
t.start();
워커를 실행시키는게 아니라, 쓰레드를 직접 실행시킨다. 이렇게 되면 쓰레드는 워커 참조를 내부적으로 가지고 있기 때문에, 워커 참조를 통해 워커가 가지고 있는 우리가 전달한 태스크를 실행 시킬 것이다. 참고로 좀 더 정밀하게 컨트롤하기 위한 상태체크 부분들은 모두 빠져있는데 나중에 여유가 생기면 각자 살펴보도록 하자.



2-3. 쓰레드풀에서 할당 될 워커(쓰레드)가 없을 때 태스크가 어떻게 되는지?

10개의 고정된 크기의 쓰레드풀을 만들었는데, 12개의 태스크를 넣어주면 어떻게 될까? 예상대로 10개의 워커(쓰레드) 만 생겨서 10개의 태스크만 처리하다가, 먼저 끝난 워커가 나머지 2개중 하나의 태스크를 처리하게 된다. 이 과정에 대해서 소스로 살펴보도록 하자.
public void execute(Runnable command) {
        // 1단계
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2단계 
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
1단계에서는 아직 워커(쓰레드)가 10개가 안되었을때, addWorker 를 통해 워커도 만들고 쓰레드도 만들어서 들어온 태스크를 처리하게 된다.
2단계에서는 이제 더 이상 처리할 워커(스레드)가 없을 경우에 대해서 처리하는데, workerQueue.offer(command) 즉  워커큐에 태스크를 넣어주고 리턴한다. 

여기서 예상 할 수 있는 것이 워커(쓰레드)가 자신이 담당한 태스크 처리를 끝내면 저 큐에서 기다리고 있는 태스크를 가져다가 실행 하리라는 것을 눈치 챌 수 있을 것이다. 맞는지 확인해보자.
 final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
  
        while (task != null || (task = getTask()) != null) {

            ... 생략 ...

            task.run();
                   
            ... 생략 ...
        }
           
    }
 예상대로 워커객체의  runWorker() 메소드를 보면 처음 워커가 시작하고 나서 while 문을 돌면서 들어오는 태스크를 꾸준히 처리해 주고 있다. 즉 쓰레드와 워커객체는 풀이 살아있는 동안 계속 살아 있는 것이다  (자동 소멸이 필요하면 그렇게 쓰레드풀을 구현 할 수도 있을 것이며, 이미 있다) . 가져온 태스크는 task.run()을 해주고 있다. 기억하라 우리의 task 는 Runnable 인터페이스를 상속받은 객체이고 run 을 구현했었다. getTask() 를 통해서 태스크를 가져오는 것이 보일 것이다. 그 안에서  workerQueue 에서 태스크를 가져올것이다. 확인해보자.

 private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
                   .. 생략 ..

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
              
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
 workerQueue 에서 (이름을 TaskQueue 라고 해야지 않나 싶다) 태스크를 가져오고 있다.


2-4. 어떻게 future 를 리턴해주는지. (submit
execute 메소드와 비슷할 거라 예상되는 submit 메소드를 살펴보자.
public <T> Future<T> submit(Callable<T> task) {
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
java/util/concurrent/AbstractExecutorService.java 는 ThreadPoolExecutor 의 부모클래스인데 submit는 여기에 존재한다.

1. 건네어진 태스크를 RunnableFuture 인터페이스를 상속받는 FutureTask 로 만든 후에
public FutureTask(Callable<V> callable) {
  this.callable = callable;
  this.state = NEW; // ensure visibility of callable
}
2. Runnable 인터페이스가 아닌 RunnableFuture 를 매개변수로 받는 execute 메소드로 보내진다.
   참고로 RunnableFuture 는 Runnable 인터페이스를 상속받았기 때문에 문제없다.


public void execute(Runnable command) {

  int c = ctl.get(); //1 단계
  if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
      return;
  c = ctl.get();
  }

  //2 단계
    .... 생략 ....
}


위의 addWorker 안에서는 쓰레드를 실행시키는데,(addWorker 코드는 위쪽에 이미소개했다.)
public void run() {
  runWorker(this);
}
Worker 객체의 run 메소드안에는 runWorker 메소드를 실행시킨다. 살펴보자.
final void runWorker(Worker w) {
  Thread wt = Thread.currentThread();
  Runnable task = w.firstTask;
  w.firstTask = null;

  try {

    task.run();

  } finally {
    processWorkerExit(w, completedAbruptly);
  }
}
이게 핵심 중 하나인데 Worker 객체에서 태스크를 실행 시키고 널로 세팅한다

새로운 태스크가 할당될때마다 쓰레드안에 넣어주면 되는 것이며, 쓰레드를 소멸 시킬 필요없이 새로운 태스크를 받아서 계속 활용 하는 것이 쓰레드풀의 아이디어 이며, 이 글에서 생략된 나머지 상태관리 구현은 매우 복잡하긴 하지만 핵심은 아니다. 

  task.run();
에 주목하자. runnable 이나 runnableFuture 는 각각 run을 구현한다.

public void run() {

  try {
    Callable<V> c = callable;
    if (c != null && state == NEW) {
    V result;
    boolean ran;
    try {
      result = c.call();
      ran = true;
    } catch (Throwable ex) {
      result = null;
      ran = false;
      setException(ex);
    }
    if (ran)
      set(result);
    }
  }

....
}
- run 내부에서 태스크를 실행시키고 있다. c.call() 리턴받은 result 를 set 메소드로 넘겨준다.

마지막으로 Future get() 과 set() 매칭에 대해서 생각해보자.
글 서두에 활용 예제 코드에서 Integer result = future.get();  기억 나는가?
future 객체의 get()메소드를 통해 무엇인가 해결이 됬으면 결과를 리턴받고 있는데, get()이 완료되려면 바로 위의 코드에서 set(result)가 호출되어야 한다.
if (ran)
    set(result);
이거 말이다. set 과 get 메소드의 코드는 아래에 있는데 이 부분은 쓰레드 풀링과 직접적인 관계가 없는 주제이므로 이 글을 읽는 분에게 맡겨 놓겠다.

set 메소드
protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

get 메소드


public V get() throws InterruptedException, ExecutionException {
  int s = state;
  if (s <= COMPLETING)
    s = awaitDone(false, 0L); // awaitDone 에서 블럭되고 있음을 알수 있다. 하염없이 기다림 
  return report(s);
}



부록: 쓰레드풀의 상태관리)

쓰레드풀의 상태를 나타내는 멤버변수로 ctl 에 대해 살펴보자.

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // RUNNING 상태와 쓰레드 개수1로 초기화
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 00011111 11111111 11111111 11111111

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS; // 11100000 00000000 00000000 00000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

private static int ctlOf(int rs, int wc) { return rs | wc; } // 2가지 상태를 하나의 Int 로 합쳐주는 함수

이 ctl 는 하나의 변수 안에 2가지 내용을 패킹시켜 두었다.

첫째. workerCount   -
현재  워커(쓰레드) 의 개수
둘째. runState - 쓰레드풀의 현재 상태 (running, shutting down 등등)

위의 코드를 보면 ctlOf 메소드를 통해서 초기에는 (RUNNING,0) 으로 초기화 시켜 놓은 부분이 보일 것이다. 즉 초기 상태는 
workerCount 는 0 이고 runState 는 RUNNING 이라는 것이다. 앞쪽비트는 runState 로 사용하고, 뒤쪽 비트는 workerCount 로 사용 한다는 것인데, 맞는지 확인 해보자.

- CAPACITY ( -1 을 해주는 이유는 하위 모든 비트를 1로 만들기 위해서이다.)
(1<< 29) - 1 = 2^29 -1  =  536870911 = 0x1FFFFFFF = 00011111 11111111 11111111 11111111
 

- RUNNING 은 다음과 같다.
-1 << 29  =  -536870912 =   0xE0000000 = 11100000 00000000 00000000 00000000

위 쪽의 3비트와 나머지 비트로 나누어 가진 것을 알 수 있다. 

따라서 ctl 의 초기 상태는 다음과 같다.
11100000 00000000 00000000 00000000

workerCount
하나의 Int 타입으로 2가지 내용을 패킹하기 위해서는 먼저 workerCount 를 대략 5억개로 제한시켜두었고 

private static final int COUNT_BITS = Integer.SIZE - 3; // 32-3 = 29

private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 00011111 11111111 11111111 11111111

private static int workerCountOf(int c) { return c & CAPACITY; } // 현재 워커 갯수는?

비트마스킹이 조금이라도 더 빠르니깐 이렇게 한것이며,위에서 workerCount 가 쓰레드 개수라고 했듯이, 그 숫자가 더 넘어서게도면 AtomicLong 으로 교체 해야 할 수도 있겠다.

workerCount 는 워커의 숫자로써, 살아있는 쓰레드들의 실질적인 숫자로써 계속 달라 질 것이다.(예를들어 ThreadFactory 가 쓰레드 생성에 실패 했을 때, 종료되는 와중에도 여전히 태스크 처리를 수행 될때 등등), 내부적으로 사용되는 workerCount와는 다르게 사용자에게 알려지는 풀의 크기는 worker 를 모아둔 workers(HashSet) 의 크기가 된다.  

현재 ctl 의 초기 상태는 다음과 같다. 이것이 workerCountOf 의 매개변수인 c 로 들어가게 되는데 

11100000 00000000 00000000 00000000   (ctl)
 00011111 11111111 11111111 11111111 (capacity)

이 둘을 비트연산(&)하면 0 이다. 맞는지 확인 해보자.
int c = ctl.get();   
int ws = workerCountOf(c); // ws 는 0

runState
RUNNING: 새로운 태스크를 받고,  큐에 집어 넣는 일을 하라.
SHOUTDOWN: 새로운 태스크를 받지마라, 그러나 이미 큐에 있는 태스크는 처리하라
STOP: 새로운 태스크를 받지마라, 큐에 있는 태스크도 처리하지 않는다. 현재 진행중인 태스크에 인터럽트를 건다.
TIDYING: 모든 태스크는 소멸되었고, workerCount 는 0 이다. TIDYING 상태로 전이되는 쓰레드는 terminated() 훅 메소드를 실행 시킬 것이다. 
TERMINATED: terminated() 메소드가 완료 되었다. 

전이형태는 다음과 같다.
RUNNING -> SHUTDOWN    ( shoutdown() 메소드 호출시)
(RUNNING or SHUTDOWN) -> STOP ( shoutdownNow() 메소드 호출시)
SHUTDOWN -> TIDYING ( 풀과 큐가 비었을 때)
STOP -> TIDYING (풀이 비었을 때) 
TYDYING -> TERMINATED (terminated() 메소드가 완료 되었을 때)



참고:

http://tutorials.jenkov.com/java-util-concurrent/executorservice.html 

http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/00cd9dc3c2b5/src/share/classes/java/util/concurrent/Executors.java 




잡설


개인적으로 리소스 해제와 관련해서 가장 먼저 떠오르는것은 C++의 포인터이다. 리소스를 해제하지 않아서 생기는 문제 또는 해제한 리소스를 사용하려고 해서 생기는 문제는 대규모 솔루션에서는 가장 골치 아픈 걱정거리가 되곤 하는데 스택트레이스에 잡히지 않는 머나먼 곳에서 해당 포인터를 조작한 것들이기에 전체 코드를 샅샅히 살피지 않으면 풀기 힘든 문제로 남기도 한다. (널포인터 문제와 레이스 컨디션 문제는 모든 솔루션에서 가장 대다수의 버그로 리포팅되며, 다른 폭탄을 야기하며, 해결하기 어려운 2가지 문제) C++은 이를 해결하고자 다음과 같은 스마트 포인터들을 제공하지만 스마트포인터 종류 분석  제대로 잘 활용하는 것도 문제이다.

자바(VM류)가 떠오르면서 리소스 관리로 부터 어느정도는 해방시켜 주었다. 하지만 외부리소스(파일,소켓,디비connection 등 의 리소스) 에 관해서는 자바도 사용자가 직접 리소스해제를 해야하는데, 리소스해제를 까먹는 경우가 생겨나서 말썽이 된다. 개발자 잘못이라고? 뭐 그렇게 볼수도 있지만 바야흐로 2018년. 개발자가 잘못하지 않게 만들어주는 덕목도 중요하리라~

지금부터 소개할 내용은 자바와 스칼라에서의 자동 리소스 해제 및 비슷한 상황으로 볼 수 있는 동기화객체 자동 해제 에 관련된 것들이며 소개차원의 범위를 가진다. 또한 정답도 없다.


* 참고로 다른 언어들도 관련된 것들이 있다. 예를들어 파이썬의 with 구문


- ARMAutomatic Resource Management 자동 리소스 관리를 지칭하는 일반적인 용어
- EAMEexcute Around Method Pattern 함수 실행시 규칙적으로 일어나는 일을 묶어 놓는 패턴 (리소스 할당/해제에만 국한된 개념이 아니다. 트랜잭션,락등에도 사용할 수 있다. AOP ,Proxy Pattern, Decorator Pattern 등도 비슷한 일을 의미/활용하긴 하는데 이것들의 의미는 좀 더 넓다)  


 리소스릭 막기



Java 


다음 코드를 보자.

private void incorrectWriting() throws IOException {
  DataOutputStream out = new DataOutputStream(new FileOutputStream("data"));
  out.writeInt(666);
  out.writeUTF("Hello");
  out.close();
}

파일 리소스를 얻어서 데이터를 쓰고(write), flush 로 밀어내고,리소스를 반납(close)를 하는 소스이다. 여기서의 문제는

1. close를 잊어버리는 개발자가 많이 있다. (개발자 수준이 낮아서 그렇다고? 쩌는 개발자들이 참여하는 훌륭한 오픈소스에서도 equals 와 == 실수 조차도 엄청 많이 발견된다. 실수는 누구나 할 수 있다.) 


2. close 를 하더라도 쓰는 과정에서 예외가 발생할 경우 리소스 누수가 생겨난다. 

따라서 리소스 누수 문제를 해결하기 위해서 아래와 같이 코딩한다.


private void correctWriting() throws IOException {
  DataOutputStream out = null;
  try {
    out = new DataOutputStream(new FileOutputStream("data"));
    out.writeInt(666);
    out.writeUTF("Hello");
  } finally {
    if (out != null) {
      out.close();
    }
  }
}

보시다사피 try ~ finally 블럭으로 감싸서, 무슨일이 있어도 finally 블록안의 내용을 실행하도록 하였다.
문제는 없다. 하지만 먼가 상투적인 코드가 남발되어 있다는 느낌적인 느낌? 강제가 아니라서 대충 안하고 넘겨 버릴거 같은 느낌? 

Java7에서는 이렇게 해결한다.


Java7 (try-with-resources 제공)


private void writingWithARM() throws IOException {
  try (DataOutputStream out = new DataOutputStream(new FileOutputStream("data"))) {
    out.writeInt(666);
    out.writeUTF("Hello");
  }
}

별거 없다. 그냥 코드를 좀 줄여주는 장치를 추가한거다. try 블럭의 모양새가 변했다. 
try 문안에 해제되야할 리소스 할당부분을 적어주고, 본문에 로직을 적어 주면 된다. (참고로 python 은 with 문으로 할당부분을 감싼다) 그러면 자동적으로 try 문을 벗어날때 자동적으로 close 를 호출해 준다.
이 특별한 형식을 try-with-resources 라고 부른다.

근데 저런 형식이 거저로 얻어지는 것은 아니다. 클래스 만드는 사람은 손이 더 가야한다.
try-with-resources 를 사용하려면 java.lang.AutoCloseable 인터페이스를 구현해야한다.


public class AutoClose implements AutoCloseable {

  @Override
  public void close() {
    System.out.println(">>> close()");  // DataOutputStream 경우는 내부에서 스트림에 대한 close()를 호출

  }

  public void work() throws MyException {
    System.out.println(">>> work()");
  }
}

이렇게 AutoCloseable 인터페이스를 통해 close 를 오버라이드 하면 된다.


public static void main(String[] args) {
  try (AutoClose autoClose = new AutoClose()) {
    autoClose.work();
  } 
}

이제 try-with-resources 를 사용할 수 있다.

* 참고로 JDK7 의 OuputStream 를 보면 public abstract class OutputStream implements Closeable, Flushable { 로 되있는데 Closeable 은 AutoCloseable 을 상속받은 인터페이스이며 해당i/O 부분에 특화되어있다. (IOException 을 발생시킴) 


자바7 ARM 실용예제 실습 

import java.io.FileWriter;
import java.io.IOException;
import java.lang.AutoCloseable;

public class FileWriterARM implements AutoCloseable {
    private final FileWriter writer;

    public FileWriterARM(final String fileName) throws IOException {
        writer = new FileWriter(fileName);
    }

    public void writeStuff(final String message) throws IOException {
        writer.write(message);
    }

    public void close() throws IOException {
        System.out.println("close called automatically...");
        writer.close();
    }
    //...
    public static void main(final String[] args) throws IOException {
        try(final FileWriterARM writerARM = new FileWriterARM("peekaboo.txt")) {
                writerARM.writeStuff("peek-a-boo");
                System.out.println("done with the resource...");
        }
    }
}

뭐 좋은거 같긴하다. 근데 문제가 있을까? 저런게 (try 구문및 AutoCloseable 인터페이스등) 있다는 것을 기억하고 사용을 하게 사용자에게 책임을 넘겨야 한다는 것이 문제라고 하면 문제이다. 저걸 할 사람들이 모여있는 곳이라면 그냥 try ~finally 감싸고 close 도 잘 해주겠지. (농담이다. 저런것을 팀 차원에서 강제한다는거 자체가 문제를 해결하는 방법일 수도 있다.) 암튼 더 일반화/강제화 할 수 있는 방법도 있을까? Java8에서의 람다를 이용한 방식을 살펴보자. 


Java8 (EAM) 

Java8에는 드디어 염원하던 람다식이 도입되므로써, 함수형 프로그래밍을 자바에서 할 수 있는 멋진 도구를 선물 해 주었다.이 람다식을 활용하면 리소스 할당/해제라든지, 트랜잭션,락 시작/종료와 같은 '틀'에 박힌 행동을 다른 방식으로 관리 할 수 있게 해주는데 어떤식으로 하는지 살펴보자.  (다른 방식이지 정답이거나 가장 좋은 방식이라고 말 할 순 없다.) 


import java.util.function.Consumer;
import static java.lang.System.out;

class JavaResource {
  private JavaResource() {out.println("created...");}
  public void operation1() {out.println("operation 1");}
  public void operation2() {out.println("operation 2");}
  private void close() { out.println("cleanup");}

  public static void use(Consumer<JavaResource> block) {
    JavaResource resource = new JavaResource();
    try {
      block.accept(resource);
    } finally {
      resource.close();
    }
  }
}

Interface Consumer<T>가 있다. 자바8에서는 자주 이용되는 함수형 인터페이스를 java.util.function 을 통해서 제공하는데, 함수형 인터페이스는 메서드가 하나만 정의 되있다고 약속하였다. Consumer 인터페이스는 accept 를 통해 하나의 인자를 매개변수로 받아서 어떤 행위(객체T를 받아서 소비,부수효과를 일으킴)를 하고 리턴은 안하는 것으로, 함수형 프로그래밍에서는 최대한 부수효과(side effect) 를 없애지만 어쩔 수 없이 필요할때는 이렇게 잘보여지도록 표현한다. 

위의 코드에서는 객체생성,정리 부분을 자기가 처리하는게 아니라 use 메소드를 통해서 위임하고 있다. (private를 보라)  use 는 어떻게 보면 팩토리패턴(생성) + 리소스정리(close)가 함께 존재하는 것으로써, 내부에서 객체생성을 하고 close()메소드도 호출해준다.

(역시 자바는 accept 라는것을 강제하는등 조금 불편한 구석이 있다. 신규 개발은 스칼라로 해보는것도 좋을 것이다) 


public class Main {
  public static void main(String... args) {

    JavaResource.use(resource -> {
      resource.operation1();
      resource.operation2();
    });

  }
}

이제 생성,해제를 함께 해주는 JavaResource.use 팩토리에게 맞기고, 관심사인 부수효과적인 행위에 대한 것만 Interface Consumer<T>를 타겟으로 하는 람다식을 구현하면 된다. 


(resource -> {
      resource.operation1();
      resource.operation2();
    }

그 람다식은 위와 같으며, 위의 resource 는 accept 의 매개변수로 들어가서 operation1() 과 operation2() 라는 부수효과를 발생시킬 것이다. 결국 이런 정형화된 패턴의 코드를 통해 위험한(?) 객체를 직접적으로 사용하는 것을 금지당하므로써, 보다 안정적인 솔루션이 만들어 질 것이다.

좀 더 실용적인 예를 살펴보자.


람다식 + 함수형 인터페이스 실습 

import java.sql.Connection;
import java.sql.SQLException;

public interface Transaction {
  public void execute(Connection connection) throws SQLException;
}

Transaction 이라는 인터페이스가 있다. 한번의 트랜잭션을 일으키기 원할 때 사용하는데, 이 인터페이스를 어떻게 사용하는지 아래 예제에서 확인하자.


import java.sql.Connection;
import java.sql.DriverManager;

public class TransactionHandler {

  public static void runInTransaction(Transaction transaction) throws Exception {

    Connection dbConnection = createDatabaseConnection();
    dbConnection.setAutoCommit(false);

    try {

      System.out.println("Starting transaction");
      transaction.execute(dbConnection);

      System.out.println("Committing transaction");
      dbConnection.commit();

    } catch (Exception e) {

      System.out.println(e.getMessage());
      System.out.println("Rolling back...");
      dbConnection.rollback();
    } finally {
      dbConnection.close();
    }
  }

  private static Connection createDatabaseConnection() throws Exception {
    Class.forName("com.mysql.jdbc.Driver");
    return DriverManager.getConnection("jdbc:mysql://localhost:3306/ticket_system", "user", "password");
  }
}

코드 자체로 모든 설명이 들어가 있기에 추가적으로 설명 할 필요는 없을 거 같다.


"트랜잭션 핸들러" 클래스가 runInTransaction 이라는 스태틱 메소드를 통해 생성/정리를 대행해주고 있다. 전형적인 데코레이터 패턴이라고 볼 수 있다. (*자바에서는 이런곳에 사용하라고 전문적으로 다이내믹 프록시라는 기술을 제공하며, 하둡코어에서는 일반 함수를 호출하는 듯하지만 내부적으론 RPC 를 하는데 그것이 사용되고 있다.)


  public static void runInTransaction(Transaction transaction) throws Exception {


이렇게 Transaction 인터페이스에 해당하는 로직을 넘겨 줘야하는데, 

TransactionHandler.runInTransaction(connection -> {

  int ticketId = findAvailableTicket(connection);

  reserveTicket(ticketId, connection);
  markAsBought(ticketId, connection);
});

람다식을 통해 Transaction 인터페이스를 타겟으로 갖는 비지니스 로직을 가시권에 들어오게 직접 만들었다. 함수형 인터페이스는 메소드가 하나만 정의되 있으므로, execute 라는 메소드는 자동으로 매칭해준다.


람다식 + Consumer 함수형 인터페이스 실습 (리소스 해제)

import java.io.FileWriter;
import java.io.IOException;
import java.lang.AutoCloseable;

public class FileWriterEAM  {
  private final FileWriter writer;

  private FileWriterEAM(final String fileName) throws IOException {
    writer = new FileWriter(fileName);
  }
  private void close() throws IOException {
    System.out.println("close called automatically...");
    writer.close();
  }
  public void writeStuff(final String message) throws IOException {
    writer.write(message);
  }
  //...
  public static void use(final String fileName, final UseInstance<FileWriterEAM, IOException> block) throws IOException {

    final FileWriterEAM writerEAM = new FileWriterEAM(fileName);
    try {
      block.accept(writerEAM);
    } finally {
      writerEAM.close();
    }
  }

  public static void main(final String[] args) throws IOException {
    FileWriterEAM.use("eam.txt", writerEAM -> {
      writerEAM.writeStuff("how");
      writerEAM.writeStuff("sweet");
    });
  }
}

 use 를 통해서 객체생성,실행,해제를 모두 처리 해 주고 있다. 딱 정해져 있기 때문에 클래스 사용자가 실수를 범할 가능성이 없어졌다.


람다식 + Runnable 함수형 인터페이스 실습 (동기화 객체 자동 해제)

import java.util.concurrent.locks.Lock;

public class Locker {
  public static void runLocked(Lock lock, Runnable block) {
    lock.lock();

    try {
      block.run();
    } finally {
      lock.unlock();
    }    
  }
}

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static fpij.Locker.runLocked;

public class Locking {
  Lock lock = new ReentrantLock(); //or mock
  protected void setLock(final Lock mock) {
    lock = mock;
  } 
  public void doOp1() {   // 행사코드 남발!!!!
    lock.lock();
    try {
      //...critical code...
    } finally {
      lock.unlock();
    }
  }
  
  // 깔끔해 졌다!!
  public void doOp2() {
    runLocked(lock, () -> {/*...critical code ... */});
  }
  public void doOp3() {
    runLocked(lock, () -> {/*...critical code ... */});
  }
  public void doOp4() {
    runLocked(lock, () -> {/*...critical code ... */});
  }
}


Scala (EAM) 

class ScalaResource private {
  println("created...")

  def operation1() = println("operation 1")
  def operation2() = println("operation 2")
  private def close() = println("cleaning up")
}
object ScalaResource {
  def use(closure : ScalaResource => Unit) = {
    val resource = new ScalaResource
    try {
      closure(resource)
    } finally {
      resource.close()
    }
  }
}
object MainEAM extends App {
  ScalaResource.use { resource =>
    resource.operation1()
    resource.operation2()
  }
}

-> 가 => 로 바뀌고 val, object같은 키워드가 있다는 것 정도의 기본 문법 차이를 제외하고 중요 포인트는 
- 스칼라는 굳이 Consumer 인터페이스라는것이 있을 필요가 없다는것~ 
- 자바에서 아주 군더더기 처럼 느껴졌던 accept 를 사용하지 않아도 된다.




참고:

http://www.oracle.com/technetwork/articles/java/trywithresources-401775.html 

https://www.geeksforgeeks.org/automatic-resource-management-java/ 

https://gist.github.com/dpsoft/9013481 

http://chrisoldwood.blogspot.kr/2011/07/execute-around-method-subsystem.html 

http://www.deadcoderising.com/transactions-using-execute-around-method-in-java-8/ 

https://pragprog.com/book/vsjava8/functional-programming-in-java 




쓰레스세이프하다? 란 과연 무엇인가? 
모든것이 불변이면? 동기화객체로 공유변수가 감싸져 있으면? TLS,STM,Actor,CSP 를 사용하면? 

땡~~~!

쓰레드로 경쟁으로 인해서 솔루션이 원하는대로 동작하지 않는다면 모두 쓰레드 세이프하지 않는 것이다.
좁은 의미로 단어를 사용하다 보면 그게 넓은 범위로 벌어진다고 착각하기 쉽다. 
객체 및 내부 변수를 불변으로 만들어 두었다고 쓰레드세이프하다고 착각하지말자.
그것을 어떻게 사용하냐에 따라서 쓰레드들은 당신이 원하는대로 동작하지 않은 결과를 내놓을 것이다.

'소프트웨어 사색' 카테고리의 다른 글

언어 선택의 기준  (0) 2018.03.20
인터페이스라는 사치  (0) 2018.03.09
블럭체인/비트코인  (1) 2018.01.31
소프트웨어는 유기물  (0) 2018.01.15
굿바이~ 옵저버 패턴 and FRP  (0) 2017.08.17



임백준님이 번역하신 "7가지 동시성 모델" 책에는 순차 프로세스 통신 (CSP) 이라는 내용이 있는데  Golang 에서 구현한 모델을 클로저언어로 래핑한 라이브러리를 이용해서 설명하고 있다. 역시 책에 나오는 내용 "미래는 불변이다", "미래는 분산이다" 라는 구절이 있다. 분산을 잘하기 위한 도우미로 "메세지 전달" 이 매우 중요한데, "액터" 나 "CSP" 처럼 메세지 전달을 기반으로 삼는 테크닉이 점점 더 중요한 역할을 하리라 예측하고 있다. 이번 번역 글 (중간 중간 동시성에 대한 개인적인 견해가 많이 들어가 있다) 에서는 Golang에서의 CSP 에 대해서 살펴본다. 학술적인 내용이 아니며 아주 기초적인 내용을 짧게 담고 있는데, 액터에 대해서 알고 있는 분이라면 통신하는 객체 자체(액터) 보다 "통신" 그 자체에 방점을 찍는게 CSP 라고 이해하고 읽어보면 편 할 것이다.



Concurrency in Golang

[번역] http://www.minaandrawos.com/2015/12/06/concurrency-in-golang/


어제, Quora 에서 고 언어에서의 동시성 모델에 대한 답변을 달아주었는데, 좀 더 말 할게 있겠다는 생각이 들어. 고 언어에서의 동시성은 고 언어를 빛나게 하는 가장 강력한 것 중 하나거든. 많은 사람들이 이것에 관해 간단한것 부터 시작해서 복잡한 내용에 이르기까지 말해 왔었는데, 이번에는 내 생각을 말 할 차례가 온 거 같아.

고 언어에서의 동시성은 단지 문법적인 요소라고 생각 하기에는 좀 더 깊이 있게 생각해 볼 만한 것들이 있어. 고 언어의 파워를 잘 활용하기 위해서는 일단 어떻게 고 언어가 동시성을 다루는지에 대해 잘 이해하고 있어야 하지. 고 언어는 CSP (Communicating Sequential Processes : 순차적 프로세스들의 통신) 라 불리는 동시성 모델을 사용하는데,이것은 컴공에서 동시성 시스템들 사이에서 일어나는 상호작용을 묘사하는 아주 기본적인 모델이야. 근데 이 글이 과학논문은 아니기 때문에 아주 구체적인 내용에 대해서는 일단 건너 뛰려해

고 언어에서의 동시성을 설명 할 때 주로 사용되는 문구는 다음과 같지.

Do not communicate by sharing memory; instead, share memory by communicating.

공유 메모리를 이용하여 커뮤니케이션 하지말고, 커뮤니케이션에 의해 메모리를 나누자.

뭐 좋은 소리 같긴 하다 ㅎㅎ  근데 이게 의미하는게 무엇일까? 커뮤니케이션에 의해 메모리를 나누자-라니..  내 머리속에서 개념들이 휘몰아 치다가 한순간에 정리 됬어. 알버트 아인슈타인은 이런 말을 했었거든 "만약 당신이 초딩도 알수 있게 끔 간단히 설명 할 수 없다면, 당신은 진정 그것을 이해한다고 볼 수 없습니다" 나는 이제 정말 간단히 설명할 수 있을 거 같아. 

(역주 :  쓰레드들이 공유 될 것들을 동시간에 접근하는게 아니라, 서로 전달하는 방식임. 여기서 함정(?)이 Go 언어에서 다른 언어처럼 공유변수를 서로 접근하는 방식을 사용 안하는건 아니며 서로 공유변수를 접근하는 방식을 사용하라고 sync 관련 키워드도 열라 많음. 자바나 스칼라도 STM,CSP,액터를 기본 동시성 객체와 함께 사용 할 수 있듯이~~차이점이라면 Go 언어는 언어 자체에서 CSP 를 지원한다는 점. 참고로 클로저언어는 STM 을 내장한다) 

진짜 내가 이해했는지 증명해보지. 자! 시작해 보자고~

공유 메모리에 의해 communicate 하지 말라!

주로 프로그래밍 언어에서는 코드를 동시적으로 실행시키는 것에 대해 생각 할 때,  먼저 여러개의 쓰레드들을 떠올릴 거야. 그것들이 복잡하게 구현되면서 병렬적 수행을 하는 것 말이지. 그리고 쓰레드들이 서로 나누어 가질 데이터 구조/변수/메모리/등등이 무엇인지 파악할 것이고, 이것들을 사이 좋게 나누어 갖게 하기 위해 동시성 객체(뮤텍스등)을 이용할 테지. 그 결과 2개의 쓰레드가 동시간에 한 군데에 쓰는 작업을 할 수 없을 테고 ~ 뭐 그냥 알아서 잘되겠지 하고 놔두거나, 아예 인지도 못할지도 모르지. 이런것이 전형적인 어떻게 다른 쓰레드들끼리 "communicate" 하는가에 관한 것일 꺼야. 아시다시피 이런 행위는 결국 레이스 컨디션, 메모리 매니지먼트, 알수 없는 예외, 데드락 등을 일으키는거지. 예외 없을꺼야. 대규모 동시성 프로그래밍을 C++ 나 Java 등으로 해본 사람들이라면 잘 알것이야.

대신해서, communicating 에 의해 메모리를 나누어라.

어떤 아이디어로 고 언어는 이걸 수행 할까? 공유 메모리 변수에 대해 락을 거는 대신해서 고 언어는 하나의 쓰레드에서 다른 하나(실제로는 우리가 알고있는 그 쓰레드는 아니지만 지금은 그렇게 생각하자고) 로 변수에 저장되어진 값을 communicate (or send) 하게 만들어줘. 기본 행동은 데이터를 보내는 쓰레드와 데이터를 받는(도착할때까지 기다리는)쓰레드야. 쓰레드들의 그 "기다림"은 쓰레드들 사이에 교환이 일어날 때 더 적합한 싱크를 하게 만들지.

좀 더 선명하게 말하자면, 안정적인 상태라는 것은 보내는 쓰레드와  받는 쓰레드가 전송이 완료 될 때까지 아무것도 안한다는데에 있어. 레이스 컨디션이라든지 비슷한 문제가 발생할 기회가 없어지게 한다는 거지. 즉 하나의 쓰레드가 어떤것을 완료하기 전에 다른 쓰레드가 행동 할 상황을 만들지 않는거야. 단순하지

고 언어는 이런 순차적 통신 행위를 할 수 있게 하는 여러 기능들을 지원하는데  중요한 것은 라이브러리 차원이 아니라 언어 차원  라는 거야. 엄청 간단하지. "buffered channel" 이라는 것을 지원하는데 이것에 의해 전송이 완료 될 때까지 쓰데드들 간에 어떤 락이나 sync 를 맞출 필요가 없어. 

대신 두개의 쓰레드들 사이의 미리 정해진 멈버를 조작 할 때는 싱크로니제이션/락킹을 할 수 는 있겠다. 간련된 뮤텍스 예제는 여기를 참고해: sync.Mutex  (역주1: 다시 언급하지만 이러한 문제가 있기 때문에 golang 도 동시성에 여전히 위험한 언어라고 생각된다. 아예 불변이 디폴트인 클로저나 얼랭이 이런 면에서는 괜찮긴 한데.. 여러모로..선택을 주저하게 만들고,..따라서 동시적 행위가 비교적 단순한 것은 golang ,좀 복잡하고 유기적이면 스칼라/아카를 선택하게 만들고 있다. 참고로 파이썬은 이상하게 멀티쓰레드,멀티프로세스를 사용하는데 문제가 많더라고.몇일후 혹은 한달후에 갑자기 작동을 안한다던지 하는..2.7 에서 3.6으로 바꾸니깐 그런 현상이 사라지기도 하고..따라서 파이썬에서 Golang으로 갈아탔다.)


(역주2: 불변은 쓰레드세이프하다? CPU 여러개를 활용하는 동시성이 뜨면서  불변이 강조되며, 불변=동시성=함수형프로그래밍이 라는 단어가 항상 함께 나열되다보니, 그럼 불변이면 쓰레드 세이프 한거야? 라고 생각 할 수 있는데 광의로 보면 그렇지 않습니다. 쓰레드세이프한 솔루션이 되지는 않습니다. 주의해야합니다.) 

고 언어를 이용한 동시성 코딩 

도대체 어떻게 한다는 것일까? 이제 코드를 보면 좀 더 명쾌해 질거야.

Go에서 "goroutine"은 위의 설명 한 스레드 개념을 제공하는데, 실제로는 스레드가 아니며 기본적으로 동일한 주소 공간에서 다른 goroutine과 동시에 실행할 수있는 기능이라고 볼 수 있지. 경량화된 쓰레드라고도 해. 그들은 O.S 쓰레드 사이에 다중화(multiplexed)되어 있는데 하나의 블록이 있다면 다른 것들은 계속 진행 될 수 있지. 모든 동기화 및 메모리 관리는 기본적으로 Go 언어에 의해 수행되며, 그들이 진짜 스레드가 아닌 이유는 항상 병행적으로 행동되지 않기 때문이야. 하지만 멀티플렉싱 및 동기화로 인해 동시적으로 동작이 발생 한다고 볼 수 있어. 

쓰레드와 비슷한  goroutine을 시작하려면 아무 함수에나 "go"라는 키워드를 사용하면 되. 간단하지~

go myfunction()

Go 채널은 Go에서 동시성을 실현하는 또 다른 핵심 개념이며 이 글의 주제이기도 하지. CSP 에서 강조하는 그 "통신" 역할을 담당하니깐. 즉 goroutine간에 메모리를 전달하는 데 사용되며 채널을 만들려면 "make" 를 사용하면되.

myChannel := make(chan int64)

goroutines이 대기하기 전에 더 많은 값을 대기열에 넣을 수 있도록 버퍼링 된 채널도 만들 수 있어.

myBufferedChannel := make(chan int64,4)

위의 두 예제에서는 채널 변수가 이 전에 생성되지 않았다고 가정했어. 그래서 ": ="을 사용하여 추론된 유형을 가진 변수를 만들었는데. ( "="는 값을 할당 할 뿐이며 변수가 이전에 선언되지 않은 경우 컴파일 오류가 발생한다.)

채널을 사용하려면 "<-"표기법을 사용하는데, 값을 (여기서 숫자 54) 보내는 goroutine은 다음과 같이 채널에 값을 할당하지.

mychannel <- 54

값을 받는 goroutine은 채널에서 그것을 추출하여 다음과 같은 새로운 변수에 할당해.

myVar := <- mychannel

이제 Golang에서 동시성을 보여주는 전체 예제를 살펴 보자고.

package main

import (
"fmt"
"time"
)
// 메인
func main()
{
ch := make(chan int) // 통신용 채널 생성
done := make(chan bool)

go sendingGoRoutine(ch) // GO 루틴 시작
go receivingGoRoutine(ch,done) // GO 루틴 시작
// 프로그램이 종료되는 것을 막음.
<- done
}
// 송신 코루틴
func sendingGoRoutine(ch chan int)
{
// 5초후에 메세지 달라는 코루틴 생성
t := time.NewTimer(time.Second*5)
// 5초후에 아무 메세지나 받음.
<- t.C

fmt.Println("Sending a value on a channel")
ch <- 45
}
// 수신 코루틴
func receivingGoRoutine(ch chan int, done chan bool)
{
// 채널로부터 값을 받으면 v 에 입력
v := <- ch
fmt.Println("Received value ", v)
done <- true
}

결과는 다음과 같어.

Sending a value on a channel
Received value 45

코루틴들끼리 채널을 이용해서 공통 관심 사항을 전달(커뮤니케이션)을 통해서 처리하는 것을 잘 기억하라고. 공통 관심 사항에 대해 메모리를 나누어 가지면서 서로 접근을 통해서 처리하는게 아니야


https://bl.ocks.org 싸이트를 보면 굉장히 다양한 d3.js 예제들이 있으니 참고 하십시요.
이 글은 글 마지막의 레퍼런스를 요약한 것이니, 구체적인 설명은 링크를 따라가서 읽어보십시요.
아래 내용은 윈도우8에서 테스팅하고 확인 하였습니다. (패키지 버전을 확인하세요. 버전이 다르면 본 문서의 명령어가 안먹힐 수도 있습니다.)

+  


개발환경 세팅 

자바스크립트 개발 환경 세팅

Node 설치  (npm 이용 및 브라우저 없이 실행하기 위함)

VS CODE 편집기 설치  (편집기는 아무거나~) 


1. 프로젝트 폴더 생성

mkdir d3v4-with-ts
cd d3v4-with-ts


2. 폴더 내에서 package.json 생성 (패키지 및 프로젝트 관리를 위함) 
npm init -y

3. 패키지 받아오기 (d3 와 타입스크립트를 설치합니다. -S 는 --save , -D 는 --save-dev) 
npm i -S d3@^4.13.0
npm i -D typescript@^2.4.2
npm i -D @types/d3@^4.10.0


타입스크립트 컴파일 환경 세팅
(타입스크립트를 Node 실행기가 이해하는 언어로 변경하기 위함)

 

4. 타입스크립트 컴파일러 설정 파일 만들기 (파일명: tsconfig.json)

{
"compilerOptions": {
"noImplicitAny": true,
"target": "es5",
"sourceMap": true,
"declaration": true,
"module": "es2015",
"moduleResolution": "node"
},
"exclude": [
"node_modules",
"dist"
]
}


  •  noimplicitAny : 모든것은 타입을 명시적으로 가져야 한다. 
  •  es5 버전의 자바스크립트로 컴파일하라. 
  •  소스캡을 만들어라. 브라우저에서 오리지날 파일에 대한 디버깅을 할 수 있다.
  •  es2015 (or es6) 모듈 타입을 사용하라. 
  •  노드가 하는 것처럼 모듈을 해석하라. 더 자세한 것은 여기 참고 here.




5. src 폴더를 만들고 그 안에 app.ts 파일을 만들고 일단 아래 내용 입력 

console.log('hello, world');


6. 커맨드라인에서 아래를 입력하여 타입스크립트를 js 로 변경 해보자

.\node_modules\.bin\tsc

build01

이렇게 js 파일들이 생겨 날 것이다. 

7. 위의 명령이 너무 길기 때문에 줄여 보자. package.json 의 script 섹션안에 아래 내용 입력

"build"
"tsc"

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc"
},


파일을 저장하고 이제 다음과 같이 명령하면 자동적으로 타입스크립트를 빌드 할 것이다.

npm run build


현재는 src 폴더 안에 ts 파일과 컴파일된 js 파일이 같이 저장될 것이나, 나중에는 따로 저장하게 만들어서 관리하기 편하게 할 것이다.

롤업을 이용한 번들링  
(브라우저가 이해하는 형태로 변경하기 위함)

8. 이제부터 번들링을 시작해보자. rollup 패키지를 설치한다. (참고로 라이브러리 번들링에는 Webpack 보다 더 낫다고 한다. Webpack and Rollup: the same but different)

npm i -D rollup@^0.45.2

9. 모드 모듈을 해결하기 위해서 플러그인도 설치 하자.
npm i -D rollup-plugin-node-resolve@^3.0.0

10. rollup 을 설정하기 위해서 rollup.config.js 파일을 만들고 아래 내용 입력

import resolve from 'rollup-plugin-node-resolve';

export default {
entry: 'src/app.js',
dest: 'src/bundle.js',
format: 'umd',
plugins: [
resolve({
jsnext: true,
main: true,
module: true
})
],
moduleName: 'app'
};



11. 아래 명령어를 실행해서 번들링하자. src/bundle.js 파일이 생겨 날 것이다.
.\node_modules\.bin\rollup -c
 (-c 는 설정파일을 이용하라는 의미)

12. 역시 너무 길기 때문에 다시 package.js 의 scripts 섹션에 build 를 수정하자.

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc && rollup -c"
},


파일을 저장하고 이제 다음과 같이 명령하면 자동적으로 타입스크립트를 컴파일해서 js 만들고 js 를 번들링 해서 최종적으로 브라우저가 이해 할 수 있는 js 파일로 만들 것이다. (파일 크기를 줄이고, 난독화하는 설정도 추가 가능하다) 


npm run build

비스 시작하기 

13.  간단하게 파일을 서비스 하기 위해 live-server 설치
npm i -D live-server@^1.2.0

14.  package.json 의 scripts 섹션에 아래 추가 (html,js,css 를 읽어서 서비스 제공 및 변화 감시)
"serve": "live-server src --watch=src/**/*.html,src/bundle.js,src/**/*.css"

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc && rollup -c",
"serve": "live-server src --watch=src/**/*.html,src/bundle.js,src/**/*.css"
},


15. src 폴더 아래에 index.html 파일 생성 (bundle.js 파일을 참조하여 브라우저에서 표현)

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>D3 with TypeScript</title>
</head>
<body>
<script src="/bundle.js"></script>
</body>
</html>


16. 드디어 서비스 시작!! 이제 브라우저에서 index.html 을 열어서 컨텐츠를 확인할 수 있다. 현재는 아무것도 안나올 것이며, F12 개발자도구의 콘솔창을 통해 hello, world 를 확인 할 수 있다.
npm run serve

17. 만약 app.ts 의 내용이 바꾸고 다시 빌드 하면 바뀐 내용을 볼 수 있을 것이다.
npm run build



TS 파일 변화에 대한 감시(Watching)
(지금까지는 ts파일을 변경해도 브라우저에서 내용이 변하지 않았다. 감시기는 JS,HTML 만 감시하지 ts를 감시하지는 않았기 때문인데, 이제 ts 포맷을 감시하도록 해보자)

18. 타입스크립트 내용을 변경 시킬 때마다 build 를 한다는것은 매우 귀찮은 일이다. rollup-watch 를 통해 해결하자.
npm i -D rollup-watch@^4.3.1

19. Pakcage.json 파일에 scripts 에 다음 내용을 추가하자.

"tsc:w": "tsc -w",
"rollup:w": "rollup -c -w",


20. 아래와 같은 명령어를 터미널 3개를 열어서 실행 시키면 app.ts가 바뀌었을때 브라우저도 자동으로 바뀔 것이다. 
npm run serve
npm run tsc:w
npm run rollup:w


Scripts 의 모든 명령을 한방에 해결하기 

21
.  이 모든것을 한방에 하기 위해서는 아래 패키지를 설치한다.

npm i -D npm-run-all@^4.0.2

22.  Package.json 에 아래 "start" 를  추가하고

"scripts": {
...
"start": "npm-run-all --parallel tsc:w rollup:w serve"
},


23
.  npm start 로 실행하면 모든게 한방에 된다. 이제 부터 ts,html 아무것이나 수정해도 브라우저에는 최신 내용이 업데이트 될 것이다.

npm start

D3 로 차트 만들기 

자 이제부터 본론인 데이터 가시화로 들어가 보자. (-.-;;) 

d3.js 는 차트라이브러리가 아니다. d3 는 데이터를 웹페이지 상에서 표현하는것을 도와주는 로우레벨 API 이다. 따라서 손쉽게 차트를 나의 서비스에 추가하고 싶은 분이라면 d3 기반의 좀 더 사용하기 쉬운 라이브러리나 엑셀차트를 사용하는 편이 더 낫다. 내가 d3.js 를 선택한 이유는 내 구미에 맞는 데이터 가시화를 하기 위해서이다. 따라서 d3.js 를 시작하기 위해서는 자바스크립트 및 DOM 이 무엇이며 SVG 가 무엇이지에 대한 기초 지식은 필수이다. 

d3.js 밑바닥 맛보기

아래와 같은 도형의 나열을 d3.js 를 이용하여 표현해보자.

1. HTML 의 body 요소 아래에 svg 요소를 추가하자. (DOM 추가) 

let svg = d3.select("body").append("svg");


2. 해당 svg 요소의 크기를 설정해준다. (attr 를 이용한다. style 을 이용하여 이쁘게 할 수 도 있다)

const w = 960;
const h = 480;

svg.attr("width",w);
svg.attr("height", h);


3. DOM에 바인딩할 데이터를 준비해두고,

let dataset = [5,10,15,20,25];


4. 데이터를 바인딩할 DOM 을 만들어서 HTML 문서에 추가한다. circle 이라는 DOM 들을 선택하고 , dataset 의 크기 만큼  바인딩하는데 ,이때 선택된 DOM 보다 data 가 더 많은 경우 , enter 를 이용해서 임의의 요소를 만든 후에 , circle 이라는 DOM 을 append 해준다라는 의미이다.

- enter에 대한 설명은 요기 참고 -> http://blog.nacyot.com/articles/2015-02-02-d3-selection/

let circle = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")


5. svg 에 circle 요소가 추가되었으니, 이제 circle 에 대한 속성을 추가해보자. circle 이 위치할 x,y 좌표와 반지름을 속성에 넣어준다. 반지름의 경우는 바인딩된 data 의 개별 요소가 익명함수를 이용하여 들어간다.

circle.attr("cx", function(d,i){
return (i * 50) + 25;
})
.attr("cy", h/2)
.attr("r", function(d){
return d;
})


좀 더 그럴싸한 차트를 그려보자.

이 단락에서는 위와 같은 챠트를 얻을 것이다. reddit API를 사용하여 마지막 25개의 포스트에 대한 스코어를 표현 할 것이다.

차트 구성 요약 

기본 차트 구성은 아래와 같을 것이다.

  • the svg (dark background)
  • the plot area (light background)
  • the axes (blue text)
  • the points (orange)

그러기 위해서 우리는 아래와 같은 절차를 밟을 것이다.

  • svg 만들기 
  • plot area 만들기 
  • plot area 에 x-축에 대한 그룹 추가 
  • plot area 에 y-축에 대한 그룹 추가 
  • plot area 에 데이터 포인트들에 대한 그룹 추가 

전체적인 외형 만들기 

import * as d3 from 'd3';
import { redditObject } from './redditFormat';
const width = 960;
const height = 480;
let svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
let plotMargins = {
top: 30,
bottom: 30,
left: 150,
right: 30
};
let plotGroup = svg.append('g')
.classed('plot', true)
.attr('transform', `translate(${plotMargins.left},${plotMargins.top})`);
let plotWidth = width - plotMargins.left - plotMargins.right;
let plotHeight = height - plotMargins.top - plotMargins.bottom;


- 아무것도 없는 HTML 파일에 동적으로 svg 엘리먼트를 추가하고 그것의 크기를 960 / 480으로 설정한다.
- 차트외형은 왼쪽에서 150, 위에서 30 만큼 이동해서 그려준다. plot 로 클래스명 설정

축 만들기 


let xScale = d3.scaleTime()
.range([0, plotWidth]);
let xAxis = d3.axisBottom(xScale);
let xAxisGroup = plotGroup.append('g')
.classed('x', true)
.classed('axis', true)
.attr('transform', `translate(${0},${plotHeight})`)
.call(xAxis);
let yScale = d3.scaleLinear()
.range([plotHeight, 0]);
let yAxis = d3.axisLeft(yScale);
let yAxisGroup = plotGroup.append('g')
.classed('y', true)
.classed('axis', true)
.call(yAxis);

- X,Y 축을 그려준다. 위치등을 자신이 알아서 설정 할 수 있다. 
- X 축은 시간축으로 그리기 위해서 scaleTime() 함수를 이용했고
- Y 축은 일반적인 데이터 크기에 따라서 그려지기 위해 scaleLinear() 를 이용했다. 
- 항상 순서는 scale 설정 ->  axis 에 
scale 을 추가 -> plot 에 axis 추가  방식이다.

데이터 가져와서 바인딩 하기 

d3.json<redditObject>('https://api.reddit.com', (error, data) => {
if (error) {
console.error(error);
} else {
console.log(data);
}
});
let prepared = data.data.children.map(d => {
return {
date: new Date(d.data.created * 1000),
score: d.data.score
}
});
console.log(prepared);

- reddit 데이터를 가져와서 설정해준다. score: d.data.score 
- 시간은 unix timestamp (세컨드 + 소수점) 에서 javascript 시간 타입(밀리세컨드)으로 변경.


스케일/축 업데이트 하기

xScale.domain(d3.extent(prepared, d => d.date))
.nice();
xAxisGroup.call(xAxis);
yScale.domain(d3.extent(prepared, d => d.score))
.nice();
yAxisGroup.call(yAxis);


- 위에서 만든 기존 x,y 축을 데이터에 적합하게 바꾼다. 즉 데이터의 시간과 데이터 값의 min,max 에 따라서 축의 도메인을 변하게 만든다. 

데이터 포인트 드로잉


var dataBound = pointsGroup.selectAll('.post')
.data(prepared);

// delete extra points
dataBound
.exit()
.remove();

// add new points
var enterSelection = dataBound
.enter()
.append('g')
.classed('post', true);

enterSelection.append('circle')
.attr('r', 2)
.style('fill', 'red');

// update all existing points
enterSelection.merge(dataBound)
.attr('transform', (d, i) => `translate(${xScale(d.date)},${yScale(d.score)})`);


- 데이터 별 원형의 반지름2이고 빨강색으로 채워진 도형을 추가해줍니다.


완전한 코드샘플은 여기에 있습니다
 GitHub.


레퍼런스:

https://hstefanski.wordpress.com/2017/07/23/d3-v4-and-typescript-getting-set-up/
https://hstefanski.wordpress.com/2017/08/15/creating-a-chart-with-d3-v4-and-typescript-or-es6/



[요약번역] https://fabianlee.org/2017/05/21/golang-running-a-go-binary-as-a-systemd-service-on-ubuntu-16-04/

1. SleepService  예제 만들기 

  1. package main
  2. import (
  3. "time"
  4. "log"
  5. "flag"
  6. "math/rand"
  7. "os"
  8. "os/signal"
  9. //"syscall"
  10. )
  11. func main() {
  12. // load command line arguments
  13. name := flag.String("name","world","name to print")
  14. flag.Parse()
  15. log.Printf("Starting sleepservice for %s",*name)
  16. // setup signal catching
  17. sigs := make(chan os.Signal, 1)
  18. // catch all signals since not explicitly listing
  19. signal.Notify(sigs)
  20. //signal.Notify(sigs,syscall.SIGQUIT)
  21. // method invoked upon seeing signal
  22. go func() {
  23. s := <-sigs
  24. log.Printf("RECEIVED SIGNAL: %s",s)
  25. AppCleanup()
  26. os.Exit(1)
  27. }()
  28. // infinite print loop
  29. for {
  30. log.Printf("hello %s",*name)
  31. // wait random number of milliseconds
  32. Nsecs := rand.Intn(3000)
  33. log.Printf("About to sleep %dms before looping again",Nsecs)
  34. time.Sleep(time.Millisecond * time.Duration(Nsecs))
  35. }
  36. }
  37. func AppCleanup() {
  38. log.Println("CLEANUP APP BEFORE EXIT!!!")
  39. }

$ ./sleepservice

이렇게 포그라운드에서 실행 시키면 SSH 터미널을 닫거나 하면 프로그램도 같이 죽어버린다.

2. systemd 서비스로 SleepService 만들기 

systemd 를 이용해서 서비스로 만들기 위해서는 “/lib/systemd/system/sleepservice.service” 아래에
다음과 같은 것을 만든다. 

[Unit]
Description=Sleep service
ConditionPathExists=/home/ubuntu/work/src/sleepservice/sleepservice
After=network.target
 
[Service]
Type=simple
User=sleepservice
Group=sleepservice
LimitNOFILE=1024

Restart=on-failure
RestartSec=10
startLimitIntervalSec=60

WorkingDirectory=/home/ubuntu/work/src/sleepservice
ExecStart=/home/ubuntu/work/src/sleepservice/sleepservice --name=foo

# make sure log directory exists and owned by syslog
PermissionsStartOnly=true
ExecStartPre=/bin/mkdir -p /var/log/sleepservice
ExecStartPre=/bin/chown syslog:adm /var/log/sleepservice
ExecStartPre=/bin/chmod 755 /var/log/sleepservice
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=sleepservice
 
[Install]
WantedBy=multi-user.target

위의 파일에서 사용된 패스들은 여러분의 경로에 맞게 수정 하면 될 것 이다.  
물론 sleepservice 라는 서비스 이름 자체도 말이다.

해당 프로그램 전용 사용자를 만들고 , 깃헙에서 만들어 둔것이 있으면 제대로 된 위치로 옮기고, 755 권한을 준다.

$ cd /tmp
$ sudo useradd sleepservice -s /sbin/nologin -M
$ wget https://raw.githubusercontent.com/fabianlee/blogcode/master/golang/sleepservice/systemd/sleepservice.service
$ sudo mv sleepservice.service /lib/systemd/system/.
$ sudo chmod 755 /lib/systemd/system/sleepservice.service

 systemctl 를 이용해서 sleepservice.service 를  enable 시키고 시작시키고, journalctl 을 통해 제대로 시작되는지 확인한다.

$ sudo systemctl enable sleepservice.service

$ sudo systemctl start sleepservice

$ sudo journalctl -f -u sleepservice

May 21 16:20:43 xenial1 sleepservice[4037]: 2017/05/21 16:20:43 hello foo
May 21 16:20:43 xenial1 sleepservice[4037]: 2017/05/21 16:20:43 About to sleep 1526ms before looping again
May 21 16:20:45 xenial1 sleepservice[4037]: 2017/05/21 16:20:45 hello foo
May 21 16:20:45 xenial1 sleepservice[4037]: 2017/05/21 16:20:45 About to sleep 196ms before looping again


번외) 

기존의 etc/init.d 를 이용해서 할 수도 있다. 

1) /etc/init.d 아래 스크립트 작성 (Golang 프로그램을 시작시키거나 멈추는 run 스크립트를 부팅시 호출 해준다.

#! /bin/sh
# /etc/init.d/myservice


USERNAME=who??
COMMAND_MYSERVICE_SCRIPT="/home/$USERNAME/myservice/run"


case "$1" in
  start)
    echo "Starting myservice .."
    sudo -u $USERNAME $COMMAND_MYSERVICE_SCRIPTstart
  
    echo "Done!!"
    ;;
  stop)
    echo "Stopping myservice .."
    sudo -u $USERNAME $COMMAND_MYSERVICE_SCRIPTstop
    echo "Done!!"
    ;;
  *)
    echo "Usage: /etc/init.d/myservice {start|stop}"
    exit 1
    ;;
esac

exit 0

권한설정

* sudo chmod 755 myservice

* update-rc.d 로 설정   ( sudo update-rc.d myservice defaults) 





Golang 에서의 Map, Filter 등


Python 으로 코딩하다가 Golang 으로 바꿔보면 가장 크게 불편한점은 List Comprehension 의 부재라고 느꼈다.
나도 Newbie라 Golang 에서는 어떻게 리스트 조작을 할까 궁금해서 자료들을 찾아서 정리 해보았다. 


기본 

가장 기본적인것은 Python 이나 Scala등에서 제공하는 synthetic sugar 마법을 사용하지 않고, Golang 답게 직접 해당 함수를 만들어서 호출하는 것이다. 아래 예를 보자.

func Map(vs []int, f func(int) int) []int {
vsm := make([]int, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}

func add5(n int) int {
return n + 5
}
func main(){
var strs = []int{1,2,3}

fmt.Println(Map(strs, add5))
}

Map 이라는 함수를 만들었다. 첫번째 인자로 배열이 들어가고, 두번째 인자로 그 배열을 조작할 함수가 매개변수로 들어갔다.사용법은 간단한 위의 코드가 설명해 줄 것이다.

 Filter 같은 경우는 아래와 같이 코딩 하면 될 것이다.

func Filter(vs []string, f func(string) bool) []string {
vsf := make([]string, 0)
for _, v := range vs {
if f(v) {
vsf = append(vsf, v)
}
}
return vsf
}


func main(){
var strs = []string{"peach", "apple", "pear", "plum"}

fmt.Println(Filter(strs, func(v string) bool {
return strings.Contains(v, "e")
}))
}

모든 것이 직관적이고 심플하다.!!  (아쉬운 점은 제네릭하지 않다는 것인데, 사실 공용 라이브러리 개발자가 아닌 이상 경험상 제네릭은 그닥 필요 없다.)

다른 예제들은 다음 링크를 참고하자. https://gobyexample.com/collection-functions


고급 (go-funk) 

자신의 필요성에 맞게 빠륵 개발 할 경우에는 제네릭이 거의 필요 없지만, 공용으로 사용될 라이브러리 차원이라면 좀 말이 달라진다. golnag 의 경우는 리플렉션,인터페이스를 통해서 제네릭을 흉내내는 것으로 보이는데, go-funk 라이브러리가 그 것이다. 자바스크립트이 유명한 유틸리티 라이브러리인 lodash와 비슷한 구석이 있지만 차별되는 로드맵을 가지고 있다고 한다. 

사용법을 간단히 살펴보면 (go get github.com/thoas/go-funk 을 통해 패키지 설치한다)

r := funk.Map([]int{1, 2, 3, 4}, func(x int) int {
return x * 2
}) // []int{2, 4, 6, 8}

r := funk.Map([]string{"1", "2"}, func(x string) string {
return "ID-" + x
})
// [ID1 ID2]

와 같이 다양한 타입으로 사용 할 수 있다.

참고로 Map 의 내부는 이렇다.


// Map manipulates an iteratee and transforms it to another type.
func Map(arr interface{}, mapFunc interface{}) interface{} {
if !IsIteratee(arr) {
panic("First parameter must be an iteratee")
}

if !IsFunction(mapFunc) {
panic("Second argument must be function")
}

var (
funcValue = reflect.ValueOf(mapFunc)
arrValue = reflect.ValueOf(arr)
arrType = arrValue.Type()
)

kind := arrType.Kind()

if kind == reflect.Slice || kind == reflect.Array {
return mapSlice(arrValue, funcValue)
}

if kind == reflect.Map {
return mapMap(arrValue, funcValue)
}

panic(fmt.Sprintf("Type %s is not supported by Map", arrType.String()))
}

먼저 순회가능한 타입과 함수가 매개변수로 각각 들어왔는지 확인 한후에, 순회가능 타입이 배열인지, 맵타입인지를 리플렉션으로 확인해서 mapSlice 혹은 mapMap 을 호출해 주고있다.



func mapSlice(arrValue reflect.Value, funcValue reflect.Value) interface{} {
funcType := funcValue.Type()

if funcType.NumIn() != 1 || funcType.NumOut() == 0 || funcType.NumOut() > 2 {
panic("Map function with an array must have one parameter and must return one or two parameters")
}

arrElemType := arrValue.Type().Elem()

// Checking whether element type is convertible to function's first argument's type.
if !arrElemType.ConvertibleTo(funcType.In(0)) {
panic("Map function's argument is not compatible with type of array.")
}

if funcType.NumOut() == 1 {
// Get slice type corresponding to function's return value's type.
resultSliceType := reflect.SliceOf(funcType.Out(0))

// MakeSlice takes a slice kind type, and makes a slice.
resultSlice := reflect.MakeSlice(resultSliceType, 0, 0)

for i := 0; i < arrValue.Len(); i++ {
result := funcValue.Call([]reflect.Value{arrValue.Index(i)})[0]

resultSlice = reflect.Append(resultSlice, result)
}

return resultSlice.Interface()
}

if funcType.NumOut() == 2 {
// value of the map will be the input type
collectionType := reflect.MapOf(funcType.Out(0), funcType.Out(1))

// create a map from scratch
collection := reflect.MakeMap(collectionType)

for i := 0; i < arrValue.Len(); i++ {
results := funcValue.Call([]reflect.Value{arrValue.Index(i)})

collection.SetMapIndex(results[0], results[1])
}

return collection.Interface()
}

return nil
}

배열의 타입과 배열을 조작할 맵함수의 파라미터 타입을 비교하여 같을 경우에 한해서,
배열을 순회하며 맵함수를 적용시켜서 

result := funcValue.Call([]reflect.Value{arrValue.Index(i)})[0]

새 배열에 추가해서 리턴한다.

resultSlice = reflect.Append(resultSlice, result)

레퍼런스:

https://gobyexample.com/collection-functions
go-funk


+ Recent posts