>백엔드 개발 >Golang >Go 언어의 메모리 모델 소개

Go 언어의 메모리 모델 소개

尚
앞으로
2020-01-10 17:01:202287검색

Go 언어의 메모리 모델 소개

Go의 메모리 모델은 "한 고루틴의 변수에 대한 읽기 작업이 다른 고루틴의 변수에 대한 쓰기 작업을 감지할 수 있습니다"라는 조건을 자세히 설명합니다. 해당 변수의 읽기 및 쓰기 작업 성능은 작성된 코드에서 파생된 기대와 일치해야 합니다. 즉, 프로그램의 성능을 변경하지 않고도 컴파일러와 프로세서는 코드를 최적화하기 위해 변수의 연산 순서를 변경할 수 있습니다. 즉, 명령어가 순서 없이 재배열됩니다.

그러나 두 개의 서로 다른 고루틴이 동일한 변수에 대해 작동하는 경우 명령 재배치로 인해 서로 다른 고루틴이 변수의 작동 순서를 일관되게 이해하지 못할 수 있습니다. 예를 들어, 한 고루틴이 a = 1; b = 2;를 실행하면 다른 고루틴은 변수 b가 변수 a보다 먼저 변경되었음을 알 수 있습니다. 이 모호성 문제를 해결하기 위해 Go 언어에서는 메모리 작업 순서를 설명하는 데 사용되는 발생 이전 개념을 도입합니다. 이벤트 e1이 이벤트 e2 이전에 발생하면 이벤트 e2가 e1 이후에 발생한다고 말합니다. 이벤트 e1이 이벤트 e2 이전에 발생하지 않고 e2 이후에도 발생하지 않으면 이벤트 e1과 e2가 동시에 발생한다고 말합니다.

단일 고루틴의 경우 이전에 발생한 순서는 코드 순서와 일치합니다.

다음 조건이 충족되면 변수 v의 "읽기 이벤트 r"은 변수 v의 또 다른 "쓰기 이벤트 w"를 인식할 수 있습니다.

1 "읽기 이벤트 r" 전에 "쓰기 이벤트 w"가 발생합니다.

2. w 이후에 발생하고 동시에 r 이전에 발생하는 변수 v에 쓰기 이벤트 w가 없습니다.

읽기 이벤트 r이 변수 v에 대한 쓰기 이벤트를 감지할 수 있도록 하려면 먼저 w가 변수 v에 대한 유일한 쓰기 이벤트인지 확인해야 합니다. 동시에 다음 조건이 충족되어야 합니다.

1. "이벤트 w 쓰기"가 "읽기 이벤트 r"보다 먼저 발생합니다.

2. 변수 v에 대한 다른 액세스는 "이벤트 w 쓰기" 이전에 발생하거나 "이벤트 r 읽기" 이후에 발생해야 합니다.

두 번째 조건은 첫 번째 조건보다 더 엄격합니다. w와 r이 병렬로 실행되는 프로그램에는 다른 읽기 작업이 없어야 하기 때문입니다.

두 조건 세트가 단일 고루틴에서 동일하기 때문에 읽기 이벤트는 변수에 대한 쓰기 이벤트가 감지되도록 할 수 있습니다. 그러나 두 고루틴 사이의 공유 변수 v의 경우 동기화 이벤트를 통해 사전 발생 조건을 보장해야 합니다(이는 읽기 이벤트가 쓰기 이벤트를 인식하는 데 필요한 조건입니다).

변수 v를 0으로 자동 초기화하는 것도 이 메모리 작동 모델에 속합니다.

1개의 기계어 길이를 초과하는 데이터를 읽고 쓰는 경우 순서가 보장되지 않습니다.

Synchronization

Initialization

프로그램의 초기화는 독립적인 고루틴에서 실행됩니다. 초기화 중에 생성된 고루틴은 초기화에 사용된 첫 번째 고루틴이 실행을 완료한 후에 시작됩니다. 패키지 p가 패키지 q를 가져오는 경우 패키지 p의 초기화 전에 패키지 q의 init 초기화 기능이 실행됩니다.

프로그램의 main.main 입력 기능은 모든 init 기능이 실행된 후에 시작됩니다. 모든 init 함수에서 새로 생성된 고루틴은 모든 init 함수가 완료된 후에 실행됩니다.

고루틴 생성

고루틴을 시작하는 데 사용되는 go 문은 고루틴 이전에 실행됩니다.

예를 들어 다음 프로그램은

var a string;

func f() {
        print(a);
}

func hello() {
        a = "hello, world";
        go f();
}
hello 함수를 호출하고 특정 순간(아마도 hello 함수가 반환된 후)에 "hello, world"를 인쇄합니다.

채널 통신 파이프 통신

파이프 통신은 두 고루틴 간의 주요 동기화 방법입니다. 일반적인 사용법은 서로 다른 고루틴이 동일한 파이프에서 읽기 및 쓰기 작업을 수행하고, 한 고루틴은 파이프에 쓰고, 다른 고루틴은 파이프에서 데이터를 읽는 것입니다.

파이프의 전송 작업은 파이프의 수신이 완료되기 전에 발생합니다.

예를 들어,

var c = make(chan int, 10)
var a string

func f() {
        a = "hello, world";
        c <- 0;
}

func main() {
        go f();
        <-c;
        print(a);
}
프로그램은 "hello, world"가 출력되도록 보장할 수 있습니다. 왜냐하면 a의 할당은 파이프 c로 데이터를 보내기 전에 발생하고 파이프의 전송 작업은 파이프 수신이 완료되기 전에 발생하기 때문입니다. 따라서 인쇄할 때 a가 지정되었습니다.

버퍼되지 않은 파이프에서 데이터 수신은 파이프로의 데이터 전송이 완료되기 전에 전송됩니다.

다음은 샘플 프로그램입니다.

var c = make(chan int)
var a string

func f() {
        a = "hello, world";
        <-c;
}
func main() {
        go f();
        c <- 0;
        print(a);
}

는 "hello, world"의 출력도 보장할 수 있습니다. 왜냐하면 a의 할당은 파이프로부터 데이터를 수신하기 전에 발생하고, 파이프로부터 데이터를 수신하는 작업은 버퍼링되지 않은 파이프로의 전송이 완료되기 전에 발생하기 때문입니다. 따라서 인쇄할 때 a가 지정되었습니다.

버퍼 파이프(예: c = make(chan int, 1))를 사용하는 경우 "hello, world" 결과가 출력된다는 보장은 없습니다(빈 문자열일 수 있지만 반드시 그렇지는 않습니다). 알 수 없는 문자열이거나 프로그램 충돌을 일으킬 수 있습니다).

Locks

패키지 동기화는 sync.Mutex 및 sync.RWMutex라는 두 가지 유형의 잠금을 구현합니다.

对于任意 sync.Mutex 或 sync.RWMutex 变量l。 如果 n < m ,那么第n次 l.Unlock() 调用在第 m次 l.Lock()调用返回前发生。

例如程序:

var l sync.Mutex
var a string

func f() {
        a = "hello, world";
        l.Unlock();
}

func main() {
        l.Lock();
        go f();
        l.Lock();
        print(a);
}

可以确保输出“hello, world”结果。因为,第一次 l.Unlock() 调用(在f函数中)在第二次 l.Lock() 调用(在main 函数中)返回之前发生,也就是在 print 函数调用之前发生。

For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after the n'th call to l.Unlock and the matching l.RUnlock happens before the n+1'th call to l.Lock.

Once

包once提供了一个在多个goroutines中进行初始化的方法。多个goroutines可以 通过 once.Do(f) 方式调用f函数。但是,f函数 只会被执行一次,其他的调用将被阻塞直到唯一执行的f()返回。once.Do(f) 中唯一执行的f()发生在所有的 once.Do(f) 返回之前。

有代码:

var a string

func setup() {
        a = "hello, world";
}

func doprint() {
        once.Do(setup);
        print(a);
}

func twoprint() {
        go doprint();
        go doprint();
}

调用twoprint会输出“hello, world”两次。第一次twoprint 函数会运行setup唯一一次。

错误的同步方式

注意:变量读操作虽然可以侦测到变量的写操作,但是并不能保证对变量的读操作就一定发生在写操作之后。

例如:

var a, b int

func f() {
        a = 1;
        b = 2;
}

func g() {
        print(b);
        print(a);
}

func main() {
        go f();
        g();
}

函数g可能输出2,也可能输出0。

这种情形使得我们必须回避一些看似合理的用法。

这里用Double-checked locking的方法来代替同步。在例子中,twoprint函数可能得到错误的值:

var a string
var done bool

func setup() {
        a = "hello, world";
        done = true;
}

func doprint() {
        if !done {
                once.Do(setup);
        }
        print(a);
}

func twoprint() {
        go doprint();
        go doprint();
}

在doprint函数中,写done暗示已经给a赋值了,但是没有办法给出保证这一点,所以函数可能输出空的值。

另一个错误陷阱是忙等待:

var a string
var done bool

func setup() {
        a = "hello, world";
        done = true;
}

func main() {
        go setup();
        for !done {
        }
        print(a);
}

我们没有办法保证在main中看到了done值被修改的同时也 能看到a被修改,因此程序可能输出空字符串。更坏的结果是,main 函数可能永远不知道done被修改,因为在两个线程之间没有同步操作,这样main 函数永远不能返回。

下面的用法本质上也是同样的问题.

type T struct {
        msg string;
}

var g *T

func setup() {
        t := new(T);
        t.msg = "hello, world";
        g = t;
}

func main() {
        go setup();
        for g == nil {
        }
        print(g.msg);
}

即使main观察到了 g != nil 条件并且退出了循环,但是任何然 不能保证它看到了g.msg的初始化之后的结果。

更多go语言知识请关注PHP中文网go语言教程栏目。

위 내용은 Go 언어의 메모리 모델 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 cnblogs.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제