Thread

thread = program?
스레드는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다.
일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다.

멀티태스킹?
사실상 하나의 cpu가 동시에 여러가지 thread를 처리할 수 가 없다. cpu는 하나의 thread만 처리하지만 순간적으로 다른 thread로 스위치하면서 왔다갔다(context switching) 하기 때문에 동시에 일을 처리하는 것 처럼 느끼는 것이다.

이렇게 cpu가 여러 thread를 왔다 갔다 하면서 처리하기 위해 조율해 주는 역활을 하는 것이 OS이다.

cpu가 thread를 전환할때는 당연히 비용이 든다. 만약 context switching이 자주 일어나게 되면 전환하는 비용이 많이 들게 되서 효율이 떨어진다.

1. GO Thread

go에서는 context switching의 비용을 최소한으로 하기 위해 컴퓨터가 가지고있는 cpu의 갯수와 최대한 가깝게 thread를 만들고 잘게 짤라서 할당 한다.

ex) go thread

package main

import (
	"fmt"
	"time"
)

func main() {
	go fun1() // go를 붙이면 thread를 사용한다는 얘기
	for i := 0; i < 20; i++ {
		time.Sleep(100 * time.Millisecond)
		//시간을 지연시켜 천천히 실행 시킴
		fmt.Println("main", i)
	}
	fmt.Scanln()
}

func fun1() {
	for i := 0; i < 10; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println("fun1", i)
	}
}
-------------------------------------
main 0
fun1 0
fun1 1
main 1
main 2
fun1 2
fun1 3
main 3
main 4
fun1 4
fun1 5
main 5
main 6
fun1 6
fun1 7
main 7
main 8
fun1 8
fun1 9
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19

go thread에 의해서 각 각의 함수가 번갈아 가면서 실행 되는걸 볼 수 있다.


2. 용어 정리

Program > Proccess > Thread

program에는 실행파일과 데이터가 포함되어 있다. 하나의 기능을 묶어놓은 app 이라고도 생각할 수 있다. 게임으로 예를 들면 실행파일과 게임에 포함된 이미지들 배경음, 효과음 등의 데이터가 포함되어 있다.

program을 실행을 하게 되면 os는 실행 파일을 메모리에 올린다. 메모리의 시작 위치를 표시하고 cpu를 통해 실행한다.

이 때 program의 실행 파일이 메모리에 적재되어 실행 되는 상태를 proccess라고 한다.

그리고 proccess는 여러개의 thread를 가질 수 있다.


3. Thread의 문제점

tread의 장점은 노는 cpu가 없게 나누어서 처리된다는 장점이 있지만 동기화에 관한 단점도 있다.

여러 개의 tread가 하나의 메모리 영역을 사용하기 때문에 각 각의 tread가 마음대로 데이터를 지우고 덮어쓰고 하다보면 메모리가 엉망진창이 되어 버린다.

ex) 계좌 송금

package main

import (
	"fmt"
	"math/rand"
	"time"
)

type Account struct {
	balance int
}

func (a *Account) Widthdraw(val int) {
	a.balance -= val
}

func (a *Account) Deposit(val int) {
	a.balance += val
}

func (a *Account) Balance() int {
	return a.balance
}

var accounts []*Account

func Transfer(sender, receiver int, money int) {
	accounts[sender].Widthdraw(money)
	// 보내는 사람 계좌에서 돈을 뺀다음
	accounts[receiver].Deposit(money)
	// 받는 사람 계좌로 입금하는 원리
}

func GetTotalBalance() int {
	total := 0
	for i := 0; i < len(accounts); i++ {
		total += accounts[i].Balance()
		// total 값에 현재 계좌의 잔액을 모두 더함
	}
	return total
}

//랜덤한 계좌의 돈을 뽑아서 랜덤한 계좌에 돈을 보내는 함수
func RandomTransfer() {
	var sender, balance int
	for {
		sender = rand.Intn(len(accounts))
		balance = accounts[sender].Balance()
		if balance > 0 {
			// 잔액이 있는 계좌를 찾아서 탈출
			break
		}
	}
	var receiver int
	for {
		receiver = rand.Intn(len(accounts))
		if sender != receiver {
			// 보내는 계좌와 받는 계좌가 다르면 탈출
			break
		}

	}

	money := rand.Intn(balance)
	// 송금하는 돈도 랜덤 하게 잔액 보다는 작게
	Transfer(sender, receiver, money)
}

func GoTransfer() {
	for {
		RandomTransfer()
		//무한 랜덤  송금
	}
}

// total 잔고 출력
func PrintTotalBalance() {
	fmt.Printf("Total: %d\n", GetTotalBalance())
}

func main() {
	for i := 0; i < 20; i++ {
		accounts = append(accounts, &Account{balance: 1000})
		// 계좌 20개를 만들어 잔고를 1000으로 만듦
	}
	PrintTotalBalance()

	for i := 0; i < 20; i++ {
		go GoTransfer()
		// 랜덤으로 송금하는 go thread
	}

	for {
		PrintTotalBalance()
		time.Sleep(100 * time.Millisecond)
		// 프로그램 동작 지연
	}
}
---------------------------------
Total: 20000
Total: 20000
Total: 40643
Total: 50464
Total: 138628
Total: 97087
Total: 156839
Total: 60486
Total: 88391
Total: 70743
Total: 71566
Total: 107985
Total: 100401

계좌를 20개 만들어서 각 각의 계좌에 1000씩 넣었을때 total은 20000이 된다.
go thread를 10개를 만들어 랜덤 입출금을한 결과로 고정 되어 있어야 하는 total 값이 증가한 것을 볼 수 있다.


4. Mutex.Lock

동기화 문제를 해결하기 위해 Lock을 건다. 하나의 자원에 접근할 때 다른 cpu들은 접근하지 못하게 lock을 하여 작업을 한 후 작업이 끝나면 lock을 풀어 다른 cpu가 접근 할 수 있게 하는 원리 이다.

ex) Mutex.Lock

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

type Account struct {
	balance int
	mutex   *sync.Mutex
	// balance가 마구 잡이로 변형되지 않게 보호
}

func (a *Account) Widthdraw(val int) {
	a.mutex.Lock() // 작업할때 lock을 건다.
	a.balance -= val
	a.mutex.Unlock() // 작업이 끝나면 lock을 푼다.
}

func (a *Account) Deposit(val int) {
	a.mutex.Lock()
	a.balance += val
	a.mutex.Unlock()
}

func (a *Account) Balance() int {
	a.mutex.Lock()
	balance := a.balance
	a.mutex.Unlock()
	return balance
}

var accounts []*Account
var globallock *sync.Mutex

func Transfer(sender, receiver int, money int) {
	globallock.Lock()
	accounts[sender].Widthdraw(money)
	// 보내는 사람 계좌에서 돈을 뺀다음
	accounts[receiver].Deposit(money)
	// 받는 사람 계좌로 입금하는 원리
	globallock.Unlock()
}

func GetTotalBalance() int {
	globallock.Lock()
	total := 0
	for i := 0; i < len(accounts); i++ {
		total += accounts[i].Balance()
		// total 값에 현재 계좌의 잔액을 모두 더함
	}
	globallock.Unlock()
	return total
}

//랜덤한 계좌의 돈을 뽑아서 랜덤한 계좌에 돈을 보내는 함수
func RandomTransfer() {
	var sender, balance int
	for {
		sender = rand.Intn(len(accounts))
		balance = accounts[sender].Balance()
		if balance > 0 {
			// 잔액이 있는 계좌를 찾아서 탈출
			break
		}
	}
	var receiver int
	for {
		receiver = rand.Intn(len(accounts))
		if sender != receiver {
			// 보내는 계좌와 받는 계좌가 다르면 탈출
			break
		}

	}

	money := rand.Intn(balance)
	// 송금하는 돈도 랜덤 하게 잔액 보다는 작게
	Transfer(sender, receiver, money)
}

func GoTransfer() {
	for {
		RandomTransfer()
		//무한 랜덤  송금
	}
}

// total 잔고 출력
func PrintTotalBalance() {
	fmt.Printf("Total: %d\n", GetTotalBalance())
}

func main() {
	for i := 0; i < 20; i++ {
		accounts = append(accounts, &Account{balance: 1000, mutex: &sync.Mutex{}})
		// 계좌 20개를 만들어 잔고를 1000으로 만들고, mutex로 보호
	}
	globallock = &sync.Mutex{}

	PrintTotalBalance()

	for i := 0; i < 20; i++ {
		go GoTransfer()
		// 랜덤으로 송금하는 go thread
	}

	for {
		PrintTotalBalance()
		time.Sleep(100 * time.Millisecond)
		// 프로그램 동작 지연
	}
}
------------------------------------
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000
Total: 20000

total이 변하지 않는것을 볼 수 있다.


DeadLock

철학자의 식사 시간
5명의 철학자가 동시에 식사를 하는데 포크를 양손으로 들어야만 식사가 가능하다고 할 때, 5명이 동시에 자신의 왼쪽 포크를 집었을때 오른쪽의 포크를 집기위해 대기 상태가된다. 5명 모두가 대기 상태이기 때문에 누구 하나 포크를 놓지않고 식사도 아무도 못하는 상태가 바로 deadlock 이라고 한다.

deadlock 상황은 간헐적으로 발생하기 때문에 처음에 바로 deadlock에 눈치 채지 못 할 가능성이 있다.

deadlock을 피하기 위해선 lock을 작게 잡거나 크게 잡아야 한다.

작게 잡는다는 말은 한사람이 한입만 먹고 포크를 놓고 다른사람이 또 한입만 먹고 포크를 놓는 것과 같다.

크게 잡는다는 것은 한사람이 포크를 가지고 식사가 끝날 때까지 기다렸다가 포크를 놓으면 그때 식사를 시작하는 것과 같다.

Producer Consumer 패턴

deadlock을 피하기 위한 방법 중 하나로 공장의 컨베이어 벨트에서 일하는거 처럼 각자가 맡은 부분의 일만 하여 완성품을 만드는 패턴이다.

이때 가장 중요한게 바로 queue이다. thread간에 자원을 전달하기위한 컨베이어 벨트로 쓰인다.

go에서는 이러한 작업을 하기위해 가장 효율적이고 빠른 queue를 제공하는데 이 것을 channel이라고 한다.

profile
개린이

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN