쓰레드가 생성될 때 TCB가 생성된다.
쓰레드의 실행이 완료되면 제거된다.
응답성
자원 공유
경제성
확장성
프로세스 문맥 교환은 쓰레드 문맥 교환 시간보다 더 오래 걸리며, 더 많은 오버헤드가 발생한다.
문맥 교환 비용: 프로세스 문맥 교환은 프로세스의 상태를 저장하고 복원하는 작업으로 비용이 많이 든다. 반면에 쓰레드 문맥 교환은 프로세스의 주소 공간을 공유하기 때문에, 쓰레드 간의 문맥 교환이 더 빠르다. 쓰레드의 상태 저장 및 복원 작업은 프로세스 수준에서 이루어지는 것이 아니라, 스레드 라이브러리 수준에서 이루어지기 때문이다.
스케줄링 오버헤드: 프로세스 문맥 교환은 다른 프로세스에 대한 스케줄링을 수행해야 하므로 추가적인 오버헤드가 발생한다. 반면 쓰레드는 동일한 프로세스 내에서 스케줄링되므로, 스케줄링 오버헤드가 줄어든다. 이로 인해 쓰레드 문맥 교환은 더 빠르게 이루어집니다.
문맥 교환 시 CPU 스택 메모리를 사용하는 이유
- 빠른 접근 속도
암달의 법칙은 순차 실행(병렬 실행이 아닌) 구성 요소와 병렬 실행 구성 요소로 이루어진 시스템에서 코어를 추가했을때 얻을 수 있는 잠재적인 성능 이득을 나타내는 공식
전부 병렬 처리를 하는 것이 아니면 기대한 만큼의 속도증가는 나오지 않음을 볼 수 있다.
코어가 무조건 많은수록 더 빠르다고 할 수는 없다.
쓰레드는 크게 사용자 쓰레드, 커널 쓰레드로 나눌 수 있다.
멀티코어의 이점을 사용할 수 없기 때문에 이 모델을 사용 중인 시스템은 거의 존재하지 않음
Windows 계열의 운영체제들과 Linux 운영체제에선 일대일 모델을 구현하고 있다.
사용되는 쓰레드 라이브러리는 POSIX Pthreads, Windows, Java를 예시로 들 수 있다.
Pthreads는 리눅스 커널 수준 쓰레드를 제공하고, Windows는 윈도우 커널 수준 쓰레드, JAVA는 JVM에서 동작하는 쓰레드(사용자 수준 쓰레드)를 제공한다.
Java의 경우 JVM이 쓰레드 관리를 책임진다.
자바의 경우 아래 세가지 방법을 사용하여 명시적으로 쓰레드를 생성할 수 있다.
extends Thread
implements Runnable
Lambda
extends Thread
public class MyThead extends Thread {
public static void main(String[] args) {
MyThead myThead = new MyThead();
myThead.start();
System.out.println("Hello, my child!");
}
@Override
public void run() {
try {
while (true) {
System.out.println("Hello, Thread!");
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Interrupted!");
}
}
}
implements Runnable
class Sum {
private int sum;
public int getSum() {
return sum;
}
public void plus(int plus) {
this.sum += plus;
}
}
class Summutation implements Runnable {
private Sum sum;
public Summutation(Sum sum) {
this.sum = sum;
}
@Override
public void run() {
int sm = 0;
for (int i = 0; i < 10; i++) {
sm += i;
}
sum.plus(sm);
}
}
public class MyRunnable {
public static void main(String[] args) {
Sum sum = new Sum();
Thread thread1 = new Thread(new Summutation(sum));
Thread thread2 = new Thread(new Summutation(sum));
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println(sum.getSum());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Lambda
public class MyLambda {
public static void main(String[] args) {
Runnable task = () -> {
try {
while (true) {
System.out.println("Hello, Lambda!");
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Interrupted!");
}
};
Thread thread = new Thread(task);
thread.start();
System.out.println("Hello, my child!");
}
}
멀티 코어에서 멀티 쓰레딩을 프로그래머가 명시적으로 코드를 설계하는 것은 쉽지 않은 일이다.
이것을 해결할 수 있는 방법 중 하나는 스레딩의 생성과 관리 책임을 컴파일러와 런타임 라이브러리에게 넘기는 것이다.
스레딩의 생성과 관리 책임을 컴파일러와 런타임 라이브러리에 넘기는 것을 암시적인 쓰레딩이라고 한다.
암시적 쓰레딩으로 아래 3가지 방법을 사용할 수 있다.
프로세스를 시작할 때 아예 일정한 수의 스레드들을 미리 풀에 만들어두는 방법
OpenMP는 C,C++ 로 작성된 API와 컴파일러 디렉티브의 집합
GCD는 애플 운영체제를 위한 기술이고, C 언어, API 및 런타임 라이브러리 각각을 확장하여 조합한 기술
^{ printf("I'm block); }
fork()
를 호출하면 새로운 프로세스는 모든 스레드를 복제해야 하는가 아니면 한 개의 스레드만 가지는 프로세스여야만 하는가?몇몇 UNIX 계열 운영체제는 아래 2가지 fork()
를 모두 제공한다.
exec()
을 호출하면 exec()
매개변수로 지정된 프로그램이 모든 스레드를 포함한 전체 프로세스를 대체한다.
어느쪽을 선택할 것인지는 응용 프로그램에 달려있다.
fork()
호출하자마자 exec()
를 호출하면 모든 스레드를 다 복제해서 만들어 줄 필요가 없다.exec()
을 호출하지 않는다면 새로운 프로세스는 모든 스레드들을 복제해야 한다.시그널은 운영체제에서 프로세스에게 어떤 사건이 일어났음을 알려주기 위해 사용된다.
시그널은 동기식/비동기식으로 전달 될 수 있다. 동기식이건 비동기식이건 모든 시그널는 다음과 같은 형태로 전달된다
모든 시그널마다 커널이 실행시키는 디폴트 시그널 핸들러가 있다. 이 디폴트 시그널 핸들러는 신호를 처리하기 위하여 호출되는 사용자 정의 시그널 핸들러에 의해 대체될 수 있다.
단일 스레드 프로세스에서는 신호 처리는 간단하다. 신호는 항상 그 프로세스에게 전달되면 된다
아래와 같은 선택이 존재한다.
동기식 신호는 그 신호를 야기한 스레드에게만 전달되어야 하고, 다른 스레드에게 전달되면 안된다.
비동기식 신호는 명확하지 않다. 예를 들어 control + C 와 같은 경우 모든 스레드에게 전달되어야 한다.
운영체제에서는 스레드에게 받아들일 신호와 Block할 신호를 지정할 수 있는 선택권을 준다
스레드 취소는 비동기식 취소, 지연 취소로 나눌 수 있다
스레드 취소가 어려운 점은 스레드들에게 할당된 자원을 회수할 때이다.
스레드가 다른 스레드와 데이터를 공유하는 도중에 취소 요청이 오면 문제가 된다.
운영체제는 취소된 스레드로부터 시스템 자원을 회수할 수도 있지만, 모든 시스템 자원을 다 회수하지 못하는 경우도 있다.
따라서 비동기식으로 스레드를 취소하면 필요한 시스템 자원을 다 사용 가능한 상태로 만들지 못할 수도 있다.
지연 취소는 스레드 자신이 취소되어도 안전하다고 판단되는 시점에서 취소 여부를 검사할 수 있다.
디폴트 취소 유형은 지연 취소이다.
ETHREAD(executive thread block)
KTHREAD(kernel thread block)
TEB(thread environment block)