우선 지난달에 이어 투표/설문조사 관련해서 이제 성능개선을 하고 있는 와중 데이터 삽입의 로직이 약간 변화되어 벌크업설트를 해야 할 일이 생겼다. 그래서 뭐가 어떻게 다른지 확인해 보기 간단한 예제를 준비했다.
type Workout struct {
Id uint `gorm:"column:id;type:int;primaryKey"`
Title string `gorm:"column:title;type:varchar(20)"`
Raps uint `gorm:"column:raps;type:int"`
}
요런 테이블을 생성해주고
이렇게 데이터를 미리 생성해서 넣어주었다.
Gorm에서 Upsert를 해서 일반적으로는 insert를, pk 의 중복이 생길시 특정 저 raps 만 업데이트하고자 한다면 아래와 같이 작성하면 된다.
즉 리스트 별로 각자 다른 값이 있고 업데이트를 하고 싶지만 모두 동일한 값이 아니면 넣어줄수 없다... 제공 안 해주면 어떻게 하나, 직접 작성해야지...
이를 해결하기위해 아래와 같은 방식으로 작성하였다.
BatchUpsert[변경한 부분]
func (w *Workout) BatchUpsert(db *gorm.DB, data []Workout) error {
var (
value []string
valueArgs []interface{}
)
for _, v := range data {
value = append(value, ("(?,?,?)"))
valueArgs = append(valueArgs, v.Id)
valueArgs = append(valueArgs, v.Title)
valueArgs = append(valueArgs, v.Raps)
}
prep := "insert into workout(id,title,raps) values %s on duplicate key update raps = raps+values(raps)"
sql := fmt.Sprintf(prep, strings.Join(value, ","))
if err := db.Exec(sql, valueArgs...).Error; err != nil {
db.Rollback()
return err
}
return nil
}
단순 sql 문을 실제로 작성해 주는 부분이다. gorm value와 같은 도움을 받아 value 같을 매칭 시켜준다. sql을 실제로 찍어보면 이런 식으로 들어간다. insert into workout(id,title,raps) values (?,?,?), (?,?,?), (?,?,?) on duplicate key update raps = raps+values(raps)
func TestWorkoutBatchUpsert(t *testing.T) {
db := table.GetDB().Db
list := make([]table.Workout, 0, 3)
for i := 1; i <= 3; i++ {
w := table.Workout{
Id: i,
Raps: i * i * 3,
}
list = append(list, w)
}
var workout table.Workout
if err := workout.BatchUpsert(db, list); err != nil {
t.Error(err)
}
}
insert into workout(id,title,raps) values (1,'',3),(2,'',12),(3,'',27) on duplicate key update raps = raps+values(raps)
이 결과 sql 의 ? 부분들은 생성된 value들이 매칭되어 들어가 sql 쿼리가 만들어진다.
BatchUpdate[대안]
업설트가 아닌 위와 같이 업데이트 만 필요한 상황이라면? 업데이트 만하는 게 더 좋다고 본다. 왜냐하면 on duplicate update는 먼저 insert 이후 업데이트를 시도하기 때문이다.
func (w *Workout) BatchUpdate(db *gorm.DB, data []Workout) error {
var (
caseSql []string
whereSql []string
caseArgs []interface{}
whereArgs []interface{}
)
for _, v := range data {
caseSql = append(caseSql, "when ? then raps + ?")
caseArgs = append(caseArgs, v.Id, v.Raps)
whereArgs = append(whereArgs, v.Id)
whereSql = append(whereSql, "?")
}
prep := "update workout set raps = case id %s end where id in (%s)"
sql := fmt.Sprintf(prep, strings.Join(caseSql, " "), strings.Join(whereSql, ","))
caseArgs = append(caseArgs, whereArgs...)
if err := db.Exec(sql, caseArgs...).Error; err != nil {
return err
}
return nil
}
when case 문을 활용해서 작성했다. 확실히 Upsert 보다 가독성이 많이 떨어진다.
func TestWorkoutBatchUpdate(t *testing.T) {
db := table.GetDB().Db
list := make([]table.Workout, 0, 3)
for i := 1; i <= 3; i++ {
w := table.Workout{
Id: i,
Raps: i * i * 3,
}
list = append(list, w)
}
var workout table.Workout
err := workout.BatchUpdate(db, list)
if err != nil {
t.Error(err)
}
}
update workout set raps = case id when 1 then raps + 3 when 2 then raps + 12 when 3 then raps + 27 end where id in (1,2,3)
원하는 방식대로 쿼리가 나간다.
1000 개 비교
그렇다면 업설트와 업데이트의 차이는 어는 정도 있는지 궁금해졌다.
[32.416ms] insert into workout(id,title,raps) values [6.349ms] [rows:3] update workout set raps =
뒤에 내용은 길어서 삭제했다. 대충 여러번 돌려본 결과 약 3배 정도 ms 차이가 발생한다. 다시 말해 update or insert의 기능이 아닌 단순 업데이트만 필요하다면? 배치 업데이트를 사용하자.
회사에서 성능개선으로 고친부분 으로는 이 쿼리가 실행되는 시점은 절대 pk 값이 없을수가 없다. 로직 자체를 변경했다. 따라서 위의 단순 비교로만 본다면 해당 부분에서 약 3배의 성능 이점을 얻은거로 판단된다.
SQL INJECTION [왜 ?]
작성하면서 ? 부분이 왜 필요한가에 대해 확인해 봤다. SQL Injection이라는 어택을 방어하기 위해서 필요한 부분이다. sql injection 이란 ? 사용자가 임의의 sql 문을 집어넣어서 프로그램 실행에 방해 혹은 버그를 주는 공격을 말한다.
사용자가 주는 값을 그대로 받지않고 ? 부분을 사용해 스트링 값이 아닌 공간을 할당하는 변수가 포함되어 있어 sql 문을 실행하기 전 원하는 값으로 대체해준다.
예를 들어
// 1번케이스
sql := fmt.Sprintf("select name from user where id = %s","사용자의 입력값")
// select name from user where id = (select id from item where id =123 )
// 2번 케이스
sql := fmt.Sprintf("select name from user where id = ?")
db.Exec(sql,사용자의입력값)
// select name from user where id = 'select id from item where id =123'
위와 아래는 엄청난 차이가 있다. 아래에서는 사용자의 입력값을 말그대로 숫자 혹은 스트링으로 만 넣어주는 반면 위에 처럼 한번 생성하게 되면 만약 사용자가 select id from table where id =123과 같은 값을 넣었을 때 해당 부분이 서브쿼리로 실행되고 2번째 sql 은 'select id from table where id =123' 이렇게 스트링 자체로 실행 된다.
type user struct {
name, email string
}
func (u user) notify() {
fmt.Printf("Sending user email to %s <%s>\n", u.name, u.email)
}
func (u *user) changeEmail(email string) {
u.email = email
fmt.Printf("Changed User Email To %s\n", email)
}
어떻게 선언해서 사용하던 GO에서는 알아서 캐스팅해서 처리를 해준다.
고 내부적으로 어떻게 호출이 되는 걸까? 메서드 즉 리시버 함수는 앞에 선언된 값이 바로 첫 번째 파라미터로 동작한다는 의미이다.
u := user{"guiwoo","park.guiwoo@hotmail.com"}
u.notify() // u.notify(u)
u.changedEmail("holy") // (*u).changedEmail(&u,"holy")
// 에 표시된 부분처럼 고 내부적으로 동작한다는 의미이다.
그래서 뭐 어떻게 사용하라는 건가?
라는 의문이 들 수 있다. 일반적으로 구조체의 값 즉 필드 내부가 변경되어야 한다면 포인터 리시버를 , 그게 아닌 경우에는 값 리시버를 사용하는 게 맞다고 생각할 수 있다.
그러나 고 랜드라는 IDE를 사용하다 보면 구조체의 리시버 함수에 대해 일관성 있게 사용하라고 나온다.
다시 말해 뭐가 됐든 하나의 구조체 에는 일관된 리시버 함수를 사용하라는 말이다.
가장 큰 이유 중 하나는 바로 인터페이스에 대해 값 또는 리시버 타입에 따라 인터페이스가 충족될 수도 있고 불충족될 수도 있다.
이로 인해 인터페이스로 추상화된 코드들이 종종 깨지는 경우가 발생한다.
(실제로 프로젝트 수행 중 일관된 메서드로 변경하던 중 인터페이스가 깨지는 경우가 발생했다.)
그 외에도 사용자 입장에서 해당 함수가 값복사를 하는지, 아니면 메모리의 값을 변경하는지 혼란이 올 수 있다.
이런 고로 나는 프로젝트에서 는 대부분 * 리시버를 많이 사용하게 된다. 물론 기존에 있던 리시버 가 있다면 해당 리시버 방법을 따라가지만 만약 새로운 타입을 생성하게 된다면 포인터 리시버를 주로 사용한다.
특정 상태변경, 큰 구조체의 경우 값복사의 리소스 낭비를 이유로 포인터 리시버 가 보다 값을 관리하기 쉽다고 생각하기 때문이다.
프로젝트에서 중간중간 긴급하게 수정하고, 내가 부족한 부분이 많아 상당한 부분을 리팩터링 하는데 시간을 보내 개인적인 공부할 시간조차 할애하기 어려웠다. 변명은 그만하고 바로 가보자.
이번 챕터는 데이터 구조이다. 바로 가보자.
배열 Array
1-1. CPU 캐시
코어들은 메인 메모리에 바로 접근하지 않고, 로컬 캐시로 접근한다. 캐시의 속도는 L1, L2, L3 메모리 순으로 빠르고 "퍼포먼스"가 중요하다면 모두 캐시메모리에 접근해야 한다.
1-2. Cache miss
캐시 미스란 코어에서 처리하고자 하는 데이터가 캐시에 없는 상태를 말한다. 위의 CPU캐시에 없다면 메인 메모리까지 접근해야 하고 이는 성능상 캐시보다 많이 느린 성능을 제공하게 된다.
프로세스는 프래패쳐를 가지고 있는데 이는 어떤 데이터가 필요할지 예상하는 것을 말한다. 다시 말해 프리패쳐를 이용해 예측가능한 데이터 접근 패턴을 생성하는 코드를 작성하는 것 이것이 캐시미스를 줄이는 방법이다.
그래서 이 캐시 미스, CPU 캐시랑 Array와 도대체 무슨 관련이 있는가?라고 생각이 들 수 있다. 배열은 메모리의 연속할 등을 하게 된다. 다
시말해 배열로 할당하고 이를 순회한다면? 이는 캐시미스의 확률을 상당히 줄여주게 된다.
n*n의 큰 행렬이 있다고 할 때 이를 순회하기 위해
1. LinkedList 순회 : TLB 변환색인버퍼 페이지와 오프셋을 이용해 중간 성능을 가지게 된다.
2. 열 순회 : 열순회는 캐시 라인을 순회하지 않는다. 메모리 임의접근패턴을 가진다.
3. 행 순회 : 행순회는 캐시 라인을 순회하며 예측 가능한 접근패턴을 만든다.
성능의 우선순위를 구분하면? 3 > 1 > 2의 순서를 가진다.
1-3. TLB 변환 색인버퍼
캐싱 시스템은 하드웨어로 한 번에 64바이트(기계별로 다르다) 씩 데이터를 옮긴다. 운영 체제는 4k 바이트씩 페이징 함으로써 메모리를 관리한다.
관리되는 모든 페이지는 가상 메모리주소를 갖게 되는데, 올바른 페이지에 매핑되고 물리적 메모리로 오프셋 하기 위해 사용된다.
여기서 위에서 linkedList 가 중간 성능을 가지는 이유를 알 수 있다.
다수의 노드가 같은 페이지에 있기 때문이다.
그렇다면 왜 열순위가 마지막 순위에 도달하는가? 일반적으로 캐시미스, 변환색인버퍼 미스가 둘 다 발생할 수 있는 게 열 순회의 경우이다.
위의 1,2,3 모두 지향하는 바는 똑같다. 데이터 지향설계
효율적인 알고리즘에 그치지 않고, 어떻게 데이터에 접근하는 것이 알고리즘 보다 성능에 좋은 영향을 미칠지 고려하는 것
배열을 왜 써야 하는지 배열을 쓰면 어떻게 동작하는지에 대해 알아보았다. 실질적으로 코드에서 어떻게 작성하고 선언하는지에 대해 알아보자.
var string [5]string
위와 같이 선언하게 되면 각 배열은 위의 그림과 같은 형태의 제로값으로 설정된다. 문자열은? 포인터와 길이를 표현하는 2 단어로 표현되기 때문이다.
fmt.Printf("\n=> Iterate over array\n")
for i, fruit := range strings {
fmt.Println(i, fruit)
}
해당 코드블록과 같이
Println을 호출할 때 같은 배열을 공유하는 4개의 문자열을 가지게 되는 것이다. 문자열의 주소를 함수에 전달하지 않으면 이점이 있다. 문자열의 길이를 알고 있으니 스택에 둘 수 있고, 그 덕분에 힙에 할당하여 GC를 해야 하는 부담을 덜게 된다. 문자열은 값을 전달하여 스택에 둘 수 있게 디자인되어
이런 설명이 붙는데 한참 다시 읽어 봤다.
fruit는 string의 값을 하나씩 복사해서 선언된 메모리 주소에 계속 덮어 씌운다. Println 은 Go의 함수와 같이 파라미터로 전달된 값은? value 복사를 하게 된다. 그렇기 때문에 매번 함수가 호출될 때마다 매번 다른 fruit의 값을 프린트할 수 있게 되는 것이고,
힙에서는 공유되는 fruit에 대해서만 가비지컬렉팅이 발생되고, 프린트 함수에서는? 복사된 값을 가지기 때문에 해당 변수는 스택에 할당된다.
슬라이스
make([]string,5)
위의 코드를 실행하면 아래와 같은 이미지의 메모리 할당이 이뤄진다.
여기서 특이한 개념이 나오게 되는데 "길이와 용량이라는 개념이다."
make 함수를 이용해서 slice, map, channel을 생성할 수 있는데 여기서 3번째 키워드는 용량을 나타낸다.
길이는 포인터로부터 읽고 쓸 수 있는 수를 의미하지만, 용량은 포인터부터 배열에 존재할 수 있는 총량을 의미한다.
var data []string
data2 := []string{}
두 개는 다르다. data는 빈 슬라이스지만 nil 포인터를 갖고 있는 비어이 있는 슬라이스가 된다.
nil 슬라이스 와 비어있는 슬라이스는 각기 다른 의미를 가진다. 제로값으로 설정된 참조 타입은 nil로 여길수 있다는 점이다. marshal에 이 nil과 비어있는 슬라이스를 넘긴다면 json에서는 null과 [] 있는 각각의 슬라이스를 반환하게 된다.
append 함수의 특징상 capacity에 다다르면 새로운 메모리 주소를 할당한다.
func Test_SliceReference(t *testing.T) {
x := make([]int, 7)
for i := 0; i < len(x); i++ {
x[i] = i * 100
}
twoHundred := &x[1]
x = append(x, 800)
x[1]++
fmt.Println(x[1], *twoHundred)
}
twoHundred와, x [1]은 다른 메모리 주소를 가진다. append를 사용해 7 이 넘은 슬라이스의 상태가 되어 새로운 메모리 주소가 할당된다.
UTF8의 경우 지난번 string에 대해서 언급한 것 string을 range로 조회할 때 단어 하나단위로 조회된다고 작성했다.
특정 프로세스 내에서 진행 중인 투표와 관련되어 고 루틴 이 생성되어 투표의 결과를 특정 통계 자료 테이블로 변환시키는 로직을 작성했다.
지난주까지 계속 프로세스를 종료하고 올리고, 간단한 수정사항 등이 있어 매번 프로세스를 종료하고 올리고 하게 되어 알아차리지 못했다. 내가 싸놓은 커다란 응가를.....
알게 된 시점은 ps aux | grep "내가 개발한 프로세스 이름"을 찍고 나서였다. 이게 웬걸 20% 가 넘는 cpu 사용량과 10% 가 넘는 메모리 사용률이 발생되고 있었다.
이렇게 무거울 수가 없다. msa 가 적용된 프로젝트이기에 다른 프로세스의 평균 cpu 사용량은 0.1 ~ 1%, 메모리 사용량도 비슷하다...
회사의 코드를 공개할 수 없어 내가 작성한 비슷한 시나리오를 작성하고 어떻게 해결했는가에 대해 작성하고자 한다.
우선 고 루틴 누수가 발생되고 있는 코드이다.
type list struct {
signal chan interface{}
name int
}
type Handler struct {
list map[int]list
sync sync.Mutex
}
Handler와 list는 투표의 실제 핸들러 부분의 구조체가 되어 다양한 함수를 제공한다.
Handler의 sync는 list의 맵의 자료구조에서 키값을 지울 때 뮤텍스 락을 걸기 위해 제공되고 있고
list 타입은 signal 즉 투표에서 종료시점을 알려주는 신호가 되겠다. name 은 단순 내가 현재 인지하고 있는 고 루틴 즉 map에 의해 관리되고 있는 고 루틴에 대해 트래킹 하기 위해 작성했다.
func handlerStream(done <-chan interface{}) <-chan interface{} {
stream := make(chan interface{})
ticker := time.NewTicker(2 * time.Second)
go func() {
defer func() {
log.Println("handler stream closed")
close(stream)
ticker.Stop()
}()
// do something int stream handler
for {
select {
case <-done:
log.Println("got done signal")
return
case <-ticker.C:
time.Sleep(1 * time.Second)
stream <- "something on your mind"
}
}
}()
return stream
}
해당 함수는 실제 나의 개발에서는 데이터 이관하는 부분의 기능을 담당하고 있다. 2초 단위로 신호가 발생되고, 해당 신호에 대해 데이터 이관을 1초의 슬립으로 대체하여 작성하였다. 이관이 완료되면 stream에 데이터를 넘겨주고 있다.
func (h *Handler) Handle(a, b int) {
log.Printf("got %d and %d", a, b)
if b == 0 {
// 고루틴 삭제
if handler, ok := h.list[a]; ok {
h.sync.Lock()
close(handler.signal)
delete(h.list, a)
h.sync.Unlock()
}
} else if b == -1 {
// 관리되고 있는 고루틴 트래킹
for _, v := range h.list {
fmt.Printf("go routine runngin :%d\n", v.name)
}
} else {
//생성하는 로직
if _, ok := h.list[a]; ok {
return
} else {
log.Println("create go routine")
h.list[a] = list{make(chan interface{}), a}
}
go func() {
defer log.Println("go routine done")
for {
_, ok := <-handlerStream(h.list[a].signal)
if !ok {
return
}
}
}()
}
}
echo, http를 열어서 작성하기 싫어 해당 Handle 은 커맨드 사용자 인풋에 대해 처리하는 부분이다.
b 값에 따라 삭제, 생성 또는 현재 관리되는 고 루틴에 대해 트래킹 하는 결괏값을 반환하게 된다.
만약 생성을 하게 되면 고 루틴을 생성해 스트림 함수를 호출해 반환되는 값을 받고, 만약 채널이 닫힌다면 해당 고 루틴은 종료된다.
func main() {
go func() {
http.ListenAndServe("localhost:4000", nil)
}()
handler := NewHandler()
reader := bufio.NewReader(os.Stdin)
for {
var a, b int
set := make(chan interface{})
go func() {
defer close(set)
fmt.Fscanln(reader, &a, &b)
set <- "done"
}()
<-set
log.Println("got input")
handler.Handle(a, b)
log.Println("cycle done")
}
log.Println("go routine done")
}
localhost:4000 번은 고루틴 프로파일링을 위한 셋업이다.
_ "net/http/pprof"
이런 방식의 임포트를 거쳐 위와 같이 선언하게 되면 4000/debug/pprof 에서 확인 가능하다.
핸들러를 생성해 무한 반복문으로 사용자의 데이터 값을 가져오고 핸들러의 핸들을 입력받은 값으로 호출하게 된다.
여기서 사용자의 입력은 실제 개발 코드에서 들어오는 시그널, api 요청 등의 역할하게 된다.
코드실행
1,2의 인풋을 넣고 고 루틴이 생성되고 한 번의 for문 사이클이 종료된다. 이후 2,2 동일한 작업과 로그가 발생되고
현재 맵에서 관리되는 고 루틴의 확인을 위해 -1 -1을 집어넣어 확인결과 2개의 고 루틴을 트래킹 하고 있다.
프로파일링의 결과를 보면 오우..... 총 66개 가 돌아가고 Handler의 Handle 은 2개가 된다.
Handler의 Handle 은 내가 의도한 결과이다. 2번의 인풋과 생성이 발생되어 2개의 고 루틴이 생성되어야 한다.
그러나 stream 또한 2개의 값이 필요하지만 지금 이 순간도 고 루틴은 증가되고 있다.
문제의 코드를 보면 Handle 함수의 생성, stream 함수의 2초마다 보내는 라인을 확인해야 한다.
아... 정말 멍청했다. 우측 for 문 안쪽에 보면 handleStream(시그널)에서 채널링 값을 받아와 해당 채널이 닫힌여부에 따라 고 루틴을 종료한다.
해당 문제를 해결하기 위해 다양한 방법을 시도했다.
1. range로 스트림 데이터 뽑아오기
이 경우 매우 제대로 작동한다. 그러나 해당 스트림에서는 데이터 값을 뽑아서 무언가를 하고 싶지 않아 기존 코드를 위와 같이 변경한 것이다.
여기서 그렇다면 done의 신호가 올바르게 받지 못해서 생성되는 건가?라는 말도 안 되는 생각을 했다.
2. done의 신호 세분화 해서 작성
위와 같은 방식으로 변경하고 테스트했으나 실패했다. 기존 for 반복문을 활용한 스트림 데이터 받아오는데 계속 생성되고 있다...
여기서 멘털이 터진 건지 여러 개를 변경하고 테스트를 시도한다. 일반적으로 생각했더라면 done의 신호가 닫힌다면 ok의 여부에 상관없이 기존코드 와 동일한 동작을 한다.
3. 왜 스트림 패턴을 적용했는가?
- 원인을 드디어 찾게 되었다. 스트림 패턴을 적용하게 되면 해당 함수 호출부는 호출만 해서 쓰면 된다. for 연속으로 쓸 것이 아니라...
go func() {
defer log.Println("go routine done")
stream := handlerStream(h.list[a].signal)
for {
v, ok := <-stream
if !ok {
log.Println(v)
return
}
}
}()
단순하게 스트림의 변수 선언을 for 반복문 밖으로만 빼주면 된다. 코드를 실행해서 확인해 보자.
총 5개의 고 루틴을 트래킹 하고 있으며 핸들러, 스트림 모두 5개로 동일하다.
저 위의 방식이 된다면 당연히 range 방식도 동일하게 적용된다.
두 개의 경우 모두 4번을 지워보자.
완벽하게 지워진다. handler 4개, stream 4개
미숙한 고 루틴에 대해서 작성하고 관리하려다 보니 생각보다 많이 어려웠고, 내가 관리하는 고 루틴과 시스템에서 실행되는 고 루틴의 숫자는 이번 테스트 서버와 같이 다를 수 있다.
고퍼에게 정말 말도 안 되는 기본적인 실수라고 생각된다. 정말 운이 좋아 발견되어 다행이라고 생각한다.
추후 스트림의 파이프라인으로 고 루틴을 구성할 때 정말 조심하고 사용에 있어 수십 번을 고민하자. 고 루틴 사용함에 있어 항상 제공되는 프로파일링을 적용해서 테스트를 필수로 해야겠다고 생각된다.
생각보다 프로파일링에서 제공해 주는 트레이스 가 너무 완벽했다. 주말에 시간 내서 고에서 작성한 프로파일링에 대해 블로그 글을 읽고 작성하고자 한다.
프로파일링에 대해 단 한 번도 고려해 본 적 이 없었는데.. 만들어놓은 이유가 있다.
정말이지 프로파일링 이 없었더라면 오늘 퇴근을 못하지 않았을까 싶다.
식은땀이 여러 번 흐르는 하루였다.
코드전문 :
package main
import (
"bufio"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"os"
"sync"
"time"
)
type list struct {
signal chan interface{}
name int
}
type Handler struct {
list map[int]list
sync sync.Mutex
}
func handlerStream(done <-chan interface{}) <-chan interface{} {
stream := make(chan interface{})
ticker := time.NewTicker(2 * time.Second)
go func() {
defer func() {
log.Println("handler stream closed")
close(stream)
ticker.Stop()
}()
// do something int stream handler
for {
select {
case _, ok := <-done:
if !ok {
return
}
log.Println("got done signal close")
return
case <-ticker.C:
time.Sleep(1 * time.Second)
stream <- "something on your mind"
}
}
}()
return stream
}
func (h *Handler) Handle(a, b int) {
log.Printf("got %d and %d", a, b)
if b == 0 {
log.Println("got 0")
if handler, ok := h.list[a]; ok {
h.sync.Lock()
close(handler.signal)
delete(h.list, a)
h.sync.Unlock()
}
} else if b == -1 {
for _, v := range h.list {
fmt.Printf("go routine runngin :%d\n", v.name)
}
} else {
//생성하는 로직
if _, ok := h.list[a]; ok {
return
} else {
log.Println("create go routine")
h.list[a] = list{make(chan interface{}), a}
}
go func() {
defer log.Println("go routine done")
for _ = range handlerStream(h.list[a].signal) {
}
}()
}
}
func NewHandler() *Handler {
return &Handler{
list: make(map[int]list),
sync: sync.Mutex{},
}
}
func main() {
go func() {
http.ListenAndServe("localhost:4000", nil)
}()
handler := NewHandler()
reader := bufio.NewReader(os.Stdin)
for {
var a, b int
set := make(chan interface{})
go func() {
defer close(set)
fmt.Fscanln(reader, &a, &b)
set <- "done"
}()
<-set
log.Println("got input")
handler.Handle(a, b)
log.Println("cycle done")
}
log.Println("go routine done")
}