Go 동시성을 시각적으로 설명 - 채널

동시 프로그래밍에서는 많은 프로그래밍 언어가 공유 메모리/상태 모델을 채택합니다. 그러나 Go는 CSP(Communicating Sequential Processes)를 구현하여 많은 언어와 차별화됩니다. CSP에서 프로그램은 상태를 공유하지 않지만 채널을 사용하여 작업을 통신하고 동기화하는 병렬 프로세스로 구성됩니다. 따라서 Go 도입에 관심이 있는 개발자는 채널 작동 방식을 이해하는 것이 중요합니다. 이 기사에서는 인간이 시각적으로 더 많은 것을 배운다고 굳게 믿기 때문에 가상의 카페를 운영하는 Gophers의 귀여운 비유를 사용하여 채널을 설명하겠습니다.

장면

Partier, Candier 및 Stringer는 카페를 운영합니다. 커피를 만드는 데는 주문을 받는 것보다 시간이 더 걸리기 때문에 Partier는 고객 주문을 받은 다음 해당 주문을 Candier와 Stringer가 커피를 만드는 주방으로 전달하는 데 도움을 줄 것입니다.

407a7ee78158179ec43e576ab86bfa5d.png
 

고퍼스 카페

버퍼링되지 않은 채널

처음에 카페는 가장 간단한 방식으로 운영되었습니다. 새로운 주문이 들어올 때마다 Partier는 채널에 주문을 넣고 Candier나 Stringer가 주문을 받기를 기다렸다가 새로운 주문을 수락했습니다. Partier와 주방 간의 이러한 통신은  ch := make(chan Order) Create를 사용하여 버퍼링되지 않은 채널을 통해 이루어집니다. 채널에 보류 중인 주문이 없으면 Stringer와 Candier가 모두 새 주문을 수락할 준비가 되어 있어도 유휴 상태로 새 주문이 도착할 때까지 기다립니다.

a7a89150e73f79a5f2a009a8aaf39f60.png
 

버퍼링되지 않은 채널

새로운 주문이 수신되면 당사자는 Candier 또는 Stringer 중 하나가 주문을 수락할 수 있도록 해당 주문을 채널에 넣습니다. 그러나 당사자는 새 주문을 계속 수락하기 전에 그 중 하나가 채널에서 주문을 받을 때까지 기다려야 합니다.

e07aa24dc42c12858855596399702a50.png
 

Stringer와 Candier 모두 새로운 주문을 수락할 수 있으므로 둘 중 하나가 즉시 주문을 수락합니다. 그러나 특정 수령인이 주문을 받게 될 것이라는 보장이나 예측은 없습니다. Stringer와 Candier 사이의 선택은 비결정적이며 일정 및 Go 런타임 내부와 같은 요소에 따라 달라집니다. Candier가 첫 번째 주문을 받았다고 가정합니다.

2934935331472e841430139ae0e66763.png
 

Candier는 첫 번째 주문 처리를 마친 후 다시 대기 상태로 돌아갑니다. 새로운 주문이 도착하지 않으면 Candier와 Stringer 두 작업자는 Partier가 처리할 채널에 다른 주문을 넣을 때까지 유휴 상태를 유지합니다.

c00dd14bc53dfd243aa9f50f62734c6f.png
 

새로운 주문이 도착하고 Stringer와 Candier가 모두 이를 처리할 수 있게 되면 Candier가 이전 주문을 방금 처리했더라도 새 주문을 받는 특정 작업자는 여전히 정의되지 않습니다. 이 경우 Candier가 다시 두 번째 주문의 수신자로 지정되었다고 가정합니다.

4ebd334a682903ef86f189679957060b.png
 

Candier는 새로운 주문이  도착했을 때 이를 order3 처리하고 있었고  order2그녀는 줄을 서지 않았  order := <-ch 으므로 Stringer가 해당 주문을 받을 수 있는 유일한  order3 직원이 되었습니다. 그러므로 그는 그것을 받을 것이다.

74c7465da2d4a72141ca1eabc9876f5b.png
 

스트링거로 보내진  order3 직후 도착합니다 order4 . 이 시점에서 Stringer와 Candier는 이미 각자의 주문을 처리하느라 바빠서 주문을 받을 수 있는 사람이 아무도 없습니다  order4. 채널은 버퍼링되지 않기 때문에  order4 채널에 넣으면 Stringer 또는 Candier가 이를 수신할 수 있을 때까지 Partier가 차단됩니다  order4 . 사람들이 버퍼링되지 않은 채널(사용  make(chan order) 또는  make(chan order, 0) 생성)과 버퍼 크기가 1인 채널(  생성 사용)을 make(chan order, 1) 혼동하는 경우가 많기 때문에 이 사례는 특별한 주의를 기울일 가치가 있습니다. 따라서 그들은 실수로  ch <- order4 즉각적인 완료를 기대하여 당사자가  ch <- order5 차단되기 전에 수락하도록  허용합니다 order5. 그렇게 생각하신다면, 여러분의 오해를 바로잡을 수 있도록 바둑놀이터에 코드 조각을 만들었습니다 https://go.dev/play/p/shRNiDDJYB4.

2c4426f6a9faafc4aaa93b93427e0c7c.png
 

버퍼링된 채널

버퍼링되지 않은 채널은 효율적이지만 전체 처리량을 제한합니다. 픽업만 하면

백엔드(주방)에서 순차적으로 처리될 수 있도록 일부 주문을 받는 것이 좋습니다. 이는 버퍼링된 채널을 사용하여 달성할 수 있습니다 . 이제 Stringer와 Candier가 주문을 처리하느라 바쁘더라도 Partier는 채널에 새 주문을 입력하고 채널이 가득 찬 한(예: 최대 3개의 보류 주문이 있는) 다른 주문을 계속 수락할 수 있습니다.

4fc6afe236daf3e0f1f953a43aacaa28.png
1*TCrrdUaa7XPcmRSDIO7xEQ.png

완충 차선이 도입되면서 카페는 더 많은 주문을 처리할 수 있는 능력이 향상되었습니다. 그러나 클라이언트 대기 시간을 합리적으로 유지하려면 적절한 버퍼 크기를 선택하는 것이 중요합니다. 결국, 오랜 대기 시간을 견디고 싶어하는 고객은 없습니다. 때로는 새로운 주문을 수락하지만 적시에 완료할 수 없는 것보다 새로운 주문을 거부하는 것이 더 수용 가능합니다. 또한 버퍼링된 채널이 있는 임시 컨테이너형(Docker) 애플리케이션을 사용할 때는 주의하세요. 무작위로 다시 시작될 수 있으므로 채널에서 메시지를 복구하는 것이 어렵거나 심지어 불가능한 작업일 수도 있습니다.

채널 대 차단 대기열

근본적으로 다르지만 Java에서는 블로킹 큐가 스레드 간 통신에 사용되는 반면, Go에서는 채널이 고루틴 간 통신에 사용되는 점에서 블로킹 큐와 채널은 어느 정도 유사성을 보입니다. 블로킹 큐에 익숙하다면 채널을 이해하는 것이 확실히 더 쉬울 것입니다.

일반적인 용도

채널은 Go 애플리케이션에서 기본적이고 널리 사용되는 기능이며 다양한 목적으로 사용될 수 있습니다. 채널의 일반적인 사용 사례는 다음과 같습니다.

고루틴 통신 : 채널을 통해 서로 다른 고루틴 간의 메시지 교환이 가능하므로 상태를 직접 공유하지 않고도 협업할 수 있습니다. 작업 풀 : 위의 예에서 볼 수 있듯이 채널은 여러 명의 동일한 작업자가 공유 채널에서 들어오는 작업을 처리하는 작업자 풀을 관리하는 데 자주 사용됩니다. 배포 및 집계 : 채널은 여러 고루틴(배포)이 작업을 수행하고 결과를 단일 채널로 보내는 반면 다른 고루틴(집계)은 이러한 결과를 소비하는 배포 및 집계 패턴을 용이하게 합니다. 시간 초과 및 기한select 명령문과 함께 사용되는 채널을 사용하여 시간 초과 및 기한을 처리할 수 있으므로 프로그램이 지연을 적절하게 처리하고 무한 대기를 피할 수 있습니다.

채널의 다양한 용도에 대해서는 다른 기사에서 더 자세히 살펴보겠습니다. 하지만 지금은 위의 카페 시나리오를 구현하고 채널이 어떤 역할을 하는지 살펴보는 것으로 이 소개 블로그를 마무리하겠습니다. Partier, Candier 및 Stringer 간의 상호 작용을 살펴보고 채널이 어떻게 이들 간의 원활한 통신 및 조정을 촉진하여 카페에서 효율적인 주문 처리 및 동기화를 가능하게 하는지 관찰합니다.

데모 코드

package main


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


func main() {
    ch := make(chan order, 3)
    wg := &sync.WaitGroup{}
    wg.Add(2)


    go func() {
        defer wg.Done()
        worker("Candier", ch)
    }()


    go func() {
        defer wg.Done()
        worker("Stringer", ch)
    }()


    for i := 0; i < 10; i++ {
        waitForOrders()
        o := order(i)
        log.Printf("Partier: I %v, I will pass it to the channel\n", o)
        ch <- o
    }


    log.Println("No more orders, closing the channel to signify workers to stop")
    close(ch)


    log.Println("Wait for workers to gracefully stop")
    wg.Wait()


    log.Println("All done")
}


func waitForOrders() {
    processingTime := time.Duration(rand.Intn(2)) * time.Second
    time.Sleep(processingTime)
}


func worker(name string, ch <-chan order) {
    for o := range ch {
        log.Printf("%s: I got %v, I will process it\n", name, o)
        processOrder(o)
        log.Printf("%s: I completed %v, I'm ready to take a new order\n", name, o)
    }


    log.Printf("%s: I'm done\n", name)
}


func processOrder(_ order) {
    processingTime := time.Duration(2+rand.Intn(2)) * time.Second
    time.Sleep(processingTime)
}


type order int


func (o order) String() string {
    return fmt.Sprintf("order-%02d", o)
}

이 코드를 복사하고 조정한 후 IDE에서 실행하면 채널 작동 방식을 더 잘 이해할 수 있습니다.

관련 기사 시리즈:

CSP(Communicating Sequential Process) 모델을 사용하여 언어 채널 이동

Go 동시성 시각적 설명 - Select 문

Supongo que te gusta

Origin blog.csdn.net/weixin_37604985/article/details/132769764
Recomendado
Clasificación