[기술 면접] Thread에 관하여

 안녕하십니까.

이 글에서는 기술면접에서 자주 물어보는 Thread에 관하여 다루려고 합니다.

제가 면접에서 경험하였던 내용 바탕으로 작성하였습니다. 기술면접을 보면 단골로 출제되는 면접 내용으로 컴퓨터 관련 전공이라면 꼭 숙지하여야 하는 내용 입니다.

Thread와 Process의 차이점은 무엇인가.

쓰레드 문제가 나오면 처음에 물어보는 개념 입니다.

Process

프로그램을 실행하는 단위. 특정 프로그램을 실행하면, 프로세스 하나가 뜬다. 기본적으로 프로세스 하나당 메인 쓰레드는 하나는 쓴다. 프로세스 끼리 독립적이기에, 여러 프로세스끼리 데이터를 동기화 하려면 통신을 해야하는 점이 있다. 

Thread

한 프로세스 내에서 프로그램이 수행되는 단위이며, 여러 쓰레드를 사용할 수 있다. 각 쓰레드는 CPU 단위를 사용한다. 

Multi Process vs Multi Thread

멀티 프로세스는 프로세스를 여러개 사용하는데, 이때 각각 메모리 등이 독립적이다. 멀티 쓰레드는 하나의 프로세스안에 여러개의 쓰레드를 사용하는 방식이며, 메모리를 공유한다. 

Thread 사용 경험

좀더 깊게 물어보고 싶을 경우 사용 경험을 물어보기도 합니다. 저는 디비 데이터를 실시간은 아니지만 30분 정도에 처리해야하며 데이터가 많은 경우 멀티 쓰레드를 사용하였다고 답을 하였습니다. 

Thread 간의 데이터 동기화 문제

쓰레드에 대하여 물어보면 왠만해서는 물어보는 문제 입니다.

Lock 사용하기

간단하게 Mutex, Semaphorer, 같은 Lock을 사용할 수 있습니다. 공유 자원을 사용해야할 경우 해당 객체에 대하여 Lock을 가져간 후 연산이 끝나면 Lock을 풉니다. 
이를 사용한 인터페이스로는 java에 BlockingQueue가 있습니다. LinkedBlockingDeque를 통해 구현체를 확인할 수 있습니다.

Atomic 연산 사용하기

면접관이 Lock을 쓰는 방법밖에 없냐고 물어볼 수 도 있습니다. Thread에 관한 면접중 절반정도는 물어본 경험이 있습니다. Lock을 쓰지 않고, 쓰레드 동기화를 할 수 있는 방법으로 CAS(Compare-and-swap)라는 연산을 통해 구현한 Atomic 연산을 사용하는 것 입니다. 
이를 사용한 구현체로는 java에 ConcurrentLinkedQueue가 있습니다.
특징으로는 Lock을 통해서 기다리는 방법보다야는 당연 더 빠를 수 있습니다. 단점으로는 모든 queue의 갯수를 리턴해야하는 size 연산이 어려워질 수 있습니다. 

CAS 란?

보통 CAS라는 것까지는 물어보지 않지만, 알아가는것도 좋다고 생각합니다. CAS는 비교, 변경을 1개의 Atomic 연산으로 구현한 명령어 이며, 이는 자바 코드에서가 아닌 architecture에서 구현해주는 명령어 입니다.
파라미터를 3개 받고 두 데이터를 비교하여, 같으면 다른 데이터로 변경. 다르면 끝나는 함수 입니다.

LinkedBlockingDeque

 Lock을 사용한 방식의 코드를 확인해 봅시다.

    @Override
    public boolean offerFirst(final E e) {
        if (e == null) {
            throw new NullPointerException();
        }
        lock.lock();
        try {
            return linkFirst(e);
        } finally {
            lock.unlock();
        }
    }

위와 같이 간단하게 lock을 사용합니다. 어려운 부분은 없습니다. 

Concurrent Linked Queue

ConcurrentLinkedQueue의 add 구현체를 봅시다.

    public boolean offer(E e) {
        ConcurrentLinkedQueue.Node<E> newNode = new ConcurrentLinkedQueue.Node(Objects.requireNonNull(e));
        ConcurrentLinkedQueue.Node<E> t = this.tail;
        ConcurrentLinkedQueue.Node p = t;
 
        do {
            while(true) {
                ConcurrentLinkedQueue.Node<E> q = p.next;
                if (q == null) {
                    break;
                }
 
                if (p == q) {
                    p = t != (t = this.tail) ? t : this.head;
                } else {
                    p = p != t && t != (t = this.tail) ? t : q;
                }
            }
        } while(!NEXT.compareAndSet(p, (Void)null, newNode));
 
        if (p != t) {
            TAIL.weakCompareAndSet(this, t, newNode);
        }
 
        return true;
    }
위의 코드에서 p는 내가 넣어야할 tail이고, newNode가 새로 넣어져야 할 데이터 입니다. do while문의 조건을 보면, NEXT.compareAndSet(p, (Void)null, newNode입니다. 이는 마지막 tail이 null이라는 것은 내가 연산하기 중간에 누가 변경하지 않았다는 것이고, 그러면 newNode로 변경하고 나갈 수 있습니다. 만약 다르다는 것은 중간에 누군가가 데이터를 넣고 갔다는 뜻이며, 이는 다시 연산을 해야하게 됩니다.

Immutable

immuatable이라는 불변으로 선언하는 방식도 있긴 합니다. 이는 객체를 생성할때 read-only로 생성합니다. 서로 다른 Thread에서 객체를 변경, 읽을 경우 중간에 변경되지 않음을 보장할 수 있는 방식이 immutable입니다. 이러한 연산이 많을 경우 유용하지만, 이 자체로써 위의 두 경우인 각자가 변경해야하는 경우는 커버하지 못해 보입니다.

마무리

깊게 들어갈 수 있는 여지가 충분히 있는 내용이지만, 굳이 여기서 더 물어보지는 않습니다. 간혹 처리 방법은 어떠냐고 한 경우도 있었는데, 이는 함정일 수 있습니다.




댓글

이 블로그의 인기 게시물

고려대학교 야간대학원 중간 후기

포켓몬 고 17셀 확인 포고맵 사용 방법

HTTP 오류 500.19 - Internal Server Error 에러 처리법