GoLang_channel struct{}

wldbs._.·2024년 11월 11일

GoLang

목록 보기
6/6
post-thumbnail

Go. 왜 빈 struct{}를 context.Value()의 키로 사용할까?
Go 빈 구조체의 의미와 활용 struct{}
What uses a type with empty struct has in Go?
What is the use of empty struct in GoLang
Empty struct
The Ingenious World of Empty Structs in Go: Zeroing in on Zero Memory
Decrypt Go: empty struct

ChatGPT

Go를 활용한 MQTT 프로젝트 진행 중, 종료 신호를 보내는 채널이 필요하여 찾아보던 도중
챗지피티의 도움을 받아 chan struct{}를 사용했더니, 코드가 잘 작동하였다.

struct 는 구조체로 알고 있는데, 구조체 타입의 채널? 이라고 생각하니 잘 감이 오지 않아
챗지피티의 도움을 받아 정리해보고자 한다.


0. chan struct{}란?

chan struct{}는 빈 구조체(struct{}) 타입의 채널이다.

  • 이 채널은 신호를 전달하는 용도로 사용된다.

채널: 고루틴 간에 데이터를 주고받는 데 사용되는 도구

  • 데이터를 전달하지 않고 단순히 신호만 전달하고 싶을 때 struct{} 타입의 채널을 사용할 수 있다.

chan struct{} 의 신호 전달 방식

  • struct{}는 빈 구조체로, 메모리를 전혀 사용하지 않는다.
  • chan struct{}를 통해 -> 고루틴 간에 종료 신호나 작업 완료 신호를 전달할 수 있다.
    • struct 값 자체는 중요하지 않고, 채널이 닫히거나 값이 전송되는 것이 신호 역할을 한다.

1. struct{}란?

struct는 Go에서 가장 작은 데이터 타입으로, 빈 구조체이다.

// 일반 구조체 선언
person := struct {
		Name string
		Age  int
	}{"peter", 10}
  • 메모리를 전혀 사용하지 않는다.
  • 주로 다음과 같은 경우에 사용한다.
    • 메모리 절약: struct는 메모리 사용량이 0인 데이터 타입, 메모리를 전혀 사용하지 않고도 값을 전달하거나 상태 표시 가능
    • 신호 전달: chan struct{}와 같은 형태로 채널 선언 시, 실제 데이터를 주고받지 않고 신호만 전달 가능. 이 경우, 채널에서 데이터를 꺼내는 작업은 struct{} 타입의 빈 값 하나만을 전달하여 신호 역할만 하도록 함
    • 구조체 필드에서 플래그처럼 사용: map[string]struct{} 형태로 선언 시, 특정 키가 있는지 여부만을 확인하고 싶을 때 사용. 빈 구조체는 키에 대한 추가 정보를 저장할 필요가 없으므로 메모리 절약 가능
  1. An empty struct is still a struct, just with a size of 0.
  2. All empty structs share the same address: the address of zerobase.
  3. We can leverage the empty struct's non-memory-occupying feature to optimize code, such as using maps to implement sets and channels.
// 빈 구조체 테스트
test1 := struct{}{}
test2 := struct{}{}

fmt.Printf("빈 구조체1의 메모리 사이즈: %d\n", unsafe.Sizeof(test1)) // 메모리 크기 = 0 : 구조체 필드 없어서 메모리 차지 X
fmt.Printf("빈 구조체2의 메모리 사이즈: %d\n", unsafe.Sizeof(test2))

fmt.Printf("빈 구조체1의 메모리 주소: %v\n", unsafe.Pointer(&test1)) // 모든 빈 구조체가 동일한 주소 공유하도록 최적화 : 메모리 공간 점유 XW
fmt.Printf("빈 구조체2의 메모리 주소: %v\n", unsafe.Pointer(&test2))

2. 예제

stopChan := make(chan struct{})

이 경우, stopChan <- struct{}{}로 값을 보내거나, close(stopChan)을 호출하여 채널을 닫아 수신 측에서 종료 신호를 전달할 수 있다.

  • 송신 측: stopChan <- stopChan{}{}로 송신 (빈 값)
  • 수신 측: <-stopChan로 신호 대기

chan struct{}는 데이터 없이 간단히 신호만 전달해야 할 때 사용되므로, 메모리 사용을 최소화하고 효율적으로 종료 신호를 주고받을 수 있다.


3. <- stopChan

<- stopChan은 Go에서 채널에서 값을 수신할 때 사용하는 구문이다.

  • 여기서는 stopChan 채널로부터 값을 기다리는 역할

<- stopChan의 역할
1. 수신 대기: <- stopChanstopChan 채널에서 값이 올 때까지 기다림
2. 동기화 및 신호: stopChan 채널은 종료 신호 전달에 사용됨

  • close(stopChan)이나 stopChan <- struct{}{}로 신호를 보내면,
  • 수신 측에서 <- stopChan을 통해 이를 감지하고 실행을 종료하거나 특정 작업을 멈출 수 있다.

사용 예시

stopChan := make(chan struct{})

// 다른 고루틴에서 종료 신호를 보내는 코드
go func() {
    fmt.Println("작업 중...")
    time.Sleep(2 * time.Second)
    close(stopChan) // stopChan을 닫아 종료 신호를 보냄
}()

// stopChan에서 신호를 기다림
<-stopChan
fmt.Println("종료 신호를 받아 작업 종료")

위 코드에서 <- stopChanstopChan이 닫힐 때까지 대기한다.
close(stopChan)이 호출되면 <- stopChan이 작동하여 "종료 신호를 받아 작업 종료"를 출력하게 된다.

  • 동기화 및 신호 전달
    : <-stopChan을 통해 프로그램의 특정 부분이 종료 조건을 기다리게 할 수 있다.
    채널을 통한 신호 전달이므로, 별도의 플래그 변수를 쓰는 대신 간결하게 동작을 제어할 수 있다.

4. 프로젝트 적용

발행자가 topic/exit이라는 주제로 메시지 "exit" 발행 시, 아래의 구독자 프로그램은 종료되도록 코드를 구성하였다.

func subscribeToMQTT(client mqtt.Client, topic string, id int, wg *sync.WaitGroup, results chan<- SubscriberResult, stopChan chan struct{}, once *sync.Once) {
...

// 발행자 종료 확인 주제 (topic/exit) 구독
	exitTopic := topic + "/exit"
	client.Subscribe(exitTopic, 2, func(_ mqtt.Client, msg mqtt.Message) {
		if string(msg.Payload()) == "exit" {
			fmt.Printf("-> Subscriber %d received exit message. Shutting down.\n", id)
			// chan struct{}는 데이터 없이 간단히 신호만 전달해야 할 때 사용
			once.Do(func() { close(stopChan) }) // 모든 구독자에게 종료 신호 전송 (한 번만)
		}
	})
...
}

: MQTT 메시지를 구독하면서, exit 메시지를 수신했을 때 stopChan 채널을 닫아 모든 구독자에게 프로그램 종료 신호를 보낸다.

1. sync.Once와 once.Do

sync.Once는 코드가 한 번만 실행되도록 보장하는 동기화 도구

  • 여러 고루틴에서 Do 메서드를 동시에 호출해도, 첫 번째 호출에서만 함수가 실행되고 그 이후에는 무시된다.
  • once.Do(func() { close(stopChan) })close(stopChan)을 여러 구독자가 동시에 호출하지 않도록 보장한다.
    • 여기서는 exit 메시지를 받는 구독자가 여러 명일 수 있지만, 그중 한 명만 stopChan을 닫도록 하는 것.

2. close(stopChan)
close(stopChan)은 stopChan 채널을 닫음

  • stopChan이 닫히면, <-stopChan을 기다리고 있는 모든 고루틴이 신호를 수신하고, 이를 통해 프로그램이 종료된다.
  • close는 채널을 닫아도 값을 보내지 않지만, 수신 대기 중인 <-stopChan 구문은 채널이 닫힌 것을 감지하여 다음 작업으로 넘어가거나 종료를 준비할 수 있다.

5. 프로젝트 세부 이해

// MQTT 주제 구독 및 메시지 수신 핸들러 설정 (QoS=2)
	client.Subscribe(topic, 2, func(_ mqtt.Client, msg mqtt.Message) {
		select {
		case <-stopChan: // stopChan이 닫히거나 값이 들어올 때
			return
		default:
			payload := string(msg.Payload())
			fmt.Printf("[Subscriber %d] Received from MQTT: %s\n", id, payload)
			receivedCount++
		}
	})
	<-stopChan // stopChan 채널에서 값을 수신할 때까지 대기
	results <- SubscriberResult{
		ID:          id,
		Receivedcnt: receivedCount,
		Expectedcnt: expectedCount,
	}

-> select 문은 각 메시지 수신마다 처음부터 반복되며, stopChan이 닫힐 때까지 계속해서 메시지를 처리

  1. <- stopChan : "종료 신호를 기다린다"
  • stopChan 채널에서 값을 수신할 때까지 대기
  • stopChan 채널에서 값이 수신되거나, 채널이 닫힐 때까지 해당 구문은 블록(멈춤) 상태
  • 보통 close(stopChan)이 호출되면, <- stopChan에서 대기 중이던 고루틴이 이 신호를 감지하고 다른 작업으로 넘어가거나 종료하게 됨
  1. case <- stopChan (select 구문 내)
  • select 구문은 여러 채널을 동시에 감시 가능
  • case <- stopChanselect 블록 안에서 stopChan에서 신호가 오면 실행할 코드 정의하는 방식
  • stopChan이 닫히거나 값이 들어올 때 해당 case 블록이 실행되도록 함
  • 여러 작업 중 하나가 종료 신호를 받으면 실행되는 동작을 정의하는 데 유용
  1. 채널이 닫히면 <-stopChanstruct{}{} (zero value)를 반환
  2. 이 반환을 통해 종료 신호로 인식하여, case <-stopChan이 즉시 실행
  3. 코드에서 <-stopChan이 있던 다른 위치들도 대기를 중단하고 다음 코드로 진행

즉, stopChan이 닫히면:
case <-stopChan이 즉시 실행되어 메시지 수신을 중단한다.
코드의 다른 <-stopChan도 대기를 중단하고 종료 작업을 진행하게 된다.

  1. stopChan이 닫히지 않으면:
  • 수신한 메시지를 payload에 저장하고, 메시지를 출력한 후 receivedCount를 증가시키면서 계속 메시지를 수신
  1. stopChan이 닫히면:
  • case <-stopChan이 실행되어 return을 호출해 핸들러가 종료
  • 이후 핸들러 밖의 <-stopChan을 통해 구독 종료를 인식하고, results 채널로 구독 결과를 전송

6. close(channel)과 신호 전달

Go에서 채널을 닫는 것은 수신 측에 종료 신호를 전달하는 효과를 가진다.
채널을 닫으면 그 채널을 수신하는 모든 고루틴이 채널이 닫혔다는 사실을 감지하고, 이를 이용해 작업을 종료할 수 있다.

<채널을 닫으면 생기는 일>

  1. 채널이 닫히면, 수신(<-) 동작이 즉시 완료된다.
  • 닫힌 채널에서 값을 수신하려고 하면, 채널이 닫혔음을 감지하고 즉시 zero value(해당 타입의 기본값, struct{} 타입의 경우 빈 값 {})를 반환한다.
    • 메모리를 사용하지 않는 0 크기의 구조체 값
    • chan int의 경우 기본값은 0
  • 이를 통해 채널이 더 이상 활성 상태가 아님을 알 수 있다.
  • 수신 동작이 즉시 완료됨을 통해 종료 신호를 전달할 수 있다.
  1. 채널을 닫는 동작(close(chan))은 고루틴 간에 더 이상 작업을 수행하지 말라는 신호로 사용된다.
  • 수신 측에서는 <-stopChan을 사용하여 이 신호를 기다리다가, 채널이 닫히면 작업을 종료하게 된다.

채널을 닫으면, 그 채널에서 값을 기다리던 모든 수신 측은 즉시 값을 수신(기본값 반환)하면서 채널이 닫힌 것을 감지한다.

  • 닫힌 채널이 기본값을 반환하기 때문에 수신 측은 그 채널이 닫혔다는 사실을 감지
profile
공부 기록용 24.08.05~ #LLM #RAG

0개의 댓글