>백엔드 개발 >Golang >Go 언어의 고루틴 사용법에 대한 자세한 설명

Go 언어의 고루틴 사용법에 대한 자세한 설명

尚
앞으로
2019-11-25 14:34:113215검색

Go 언어의 고루틴 사용법에 대한 자세한 설명

go의 goroutine은 언어 수준에서 동시성을 지원하는 go 언어의 기능입니다. 처음 Go를 접했을 때 Go의 고루틴은 정말 우스꽝스러울 정도로 동시성을 구현하는 방식에 매우 만족했습니다.

하지만 프로젝트를 진행하면서 고루틴은 누구에게나 쉽게 남용될 수 있다는 사실을 점점 깨닫게 되었습니다. 고루틴은 양날의 검입니다. 다음은 고루틴 사용의 몇 가지 죄악입니다:

1. 고루틴에 전달되는 포인터는 안전하지 않습니다.

fun main() {
    request := request.NewRequest() //这里的NewRequest()是传递回一个type Request的指针
    go saveRequestToRedis1(request)
    go saveReuqestToRedis2(request)
     
    select{}
 
}

매우 논리적인 코드:

메인 루틴은 루틴을 열고 요청을 saveRequestToRedis1에 전달하여 요청을 저장하게 합니다. Redis 노드 1

동시에 다른 루틴을 열고 saveReuqestToRedis2에 요청을 전달하여 Redis 노드 2

에 요청을 저장할 수 있도록 합니다. 그런 다음 메인 루틴은 프로세스를 종료하지 않고 루프에 들어갑니다

The 이제 문제가 발생합니다. saveRequestToRedis1 및 saveReuqestToRedis2 두 함수는 실제로 제가 작성한 것이 아니라 팀의 다른 사람이 작성한 것입니다. 저는 구현에 대해 아무것도 모르고 내부 구현을 자세히 살펴보고 싶지 않습니다. 하지만 함수 이름에 따르면 나는 그것을 당연하게 여기고 요청 포인터를 전달했습니다.

실제로 saveRequestToRedis1과 saveRequestToRedis2는 다음과 같이 구현됩니다.

func saveRequestToRedis1(request *Request){
     …
     request.ToUsers = []int{1,2,3} //这里是一个赋值操作,修改了request指向的数据结构
     …
    redis.Save(request)
    return
}

이게 무슨 문제인가요? 두 개의 고루틴 saveRequestToRedis1 및 saveReuqestToRedis2는 동일한 공유 데이터 구조를 수정하지만 루틴 실행이 순서가 없기 때문에 request.ToUsers 설정 및 redis.Save()가 원자적 작업임을 보장할 수 없으므로 실제 redis 저장소는 데이터 오류 버그가 발생합니다.

글쎄요, 이 saveRequestToRedis 함수 구현에 문제가 있다고 할 수 있고, go 루틴을 사용하여 호출될 것이라는 점은 고려하지 않았습니다. 다시 생각해 보시기 바랍니다. saveRequestToRedis의 구체적인 구현에는 문제가 없습니다. 상위 계층에서 이를 어떻게 사용하는지 고려하면 안 됩니다.

고루틴 사용에 문제가 있기 때문입니다. 메인 루틴이 루틴을 열 때 루틴의 코드가 메인 루틴의 데이터를 수정했는지 여부를 확인하지 않습니다. 예, 메인 루틴에서는 이 상황을 고려해야 합니다.

go 루틴을 활성화하면 기본 goroutine은 공유 데이터가 수정되었는지 확인하기 위해 하위 루틴의 각 코드 줄을 읽어야 합니까? ? 실제 프로젝트 개발 과정에서 이로 인해 개발 속도가 얼마나 느려지나요!

Go 언어는 동시 개발의 부담을 줄이기 위해 고루틴을 사용하지만, 반면에 개발 부담이 커질 것이라고는 전혀 생각하지 못했습니다.

위에서 너무 많은 말을 했지만 결론을 내리자면:

gorotine 포인터 전달은 안전하지 않습니다! !

이전 예제가 충분히 숨겨지지 않았다면 여기에 또 다른 예제가 있습니다.

fun (this *Request)SaveRedis() {
    redis1 := redis.NewRedisAddr("xxxxxx")
    redis2 := redis.NewRedisAddr("xxxxxx")
    go this.saveRequestToRedis(redis1)
    go this.saveRequestToRedis(redis2)
     
    select{}
}

이 포인터가 가리키는 개체에 문제가 있는지 고려하는 사람은 거의 없습니다. 여기서 이 포인터를 루틴으로 전달하는 것은 다음과 같습니다. 매우 숨겨져 있습니다.

2. 고루틴은 함수의 위험 요소를 증가시킵니다

이 점은 실제로 위의 점에서 파생되었습니다. 위에서 언급했듯이 Go 함수에 포인터를 전달하는 것은 안전하지 않습니다. 그렇다면 다른 각도에서 생각해 보세요. 호출하려는 함수가 함수 구현 내부에서 사용되지 않을 것이라고 어떻게 보장할 수 있습니까? 함수 본문 내부의 구체적인 구현을 보지 않으면 이를 확인할 방법이 없습니다.

예를 들어 위의 대표적인 예를 살짝만 바꾸면

func main() {
    request := request.NewRequest()
    saveRequestToRedis1(request)
    saveRequestToRedis2(request)
    select{}
}

이제 동시성을 사용하지 않으니 이런 문제는 절대 발생하지 않겠죠? 나는 함수를 쫓아다녔고 어이가 없었습니다.

func saveReqeustToRedis1(request *Request) {
           …
            go func() {
          …
          request.ToUsers = []{1,2,3}
         ….
         redis.Save(request)
    }
}

고루틴이 내부에서 시작되어 요청 포인터가 가리키는 객체를 수정했습니다. 여기서 오류가 발생했습니다. 글쎄요, 함수를 호출할 때 함수 내부의 구체적인 구현을 살펴보지 않으면 이 문제를 피할 수 없습니다.

그래서 최악의 관점에서 보면 모든 함수 호출은 이론적으로 안전하지 않습니다! 이 호출 함수가 우리 개발팀의 누군가가 작성한 것이 아니라 인터넷에 있는 제3자 오픈 소스 코드를 사용했다고 상상해보세요... 이 버그를 찾는 데 얼마나 많은 시간이 걸릴지 정말 상상할 수 없습니다.

3. 고루틴의 남용 함정

이 예를 보세요:

func main() {
    go saveRequestToRedises(request)
}
 
func saveRequestToRedieses(request *Request) {
    for _, redis := range Redises {
        go redis.saveRequestToRedis(request)
    }
}
 
func saveRequestToRedis(request *Request) {
            ….
            go func() {
                     request.ToUsers = []{1,2,3}
                        …
                        redis.Save(request)
            }
 
}

놀랍습니다. go는 어디에나 있고, 눈 깜짝할 사이에 어딘가에 나타나는 것 같습니다. 이것은 go를 남용하는 것입니다. 어디에서나 go를 사용해야 하는지는 명확하지 않습니다. 왜 go를 사용하나요? 고루틴이 정말로 효율성을 향상시킬 수 있을까요?

C 언어의 동시성은 Go 언어의 동시성보다 훨씬 복잡하고 번거롭기 때문에 사용하기 전에 깊이 생각해보고 동시성을 사용하는 경우의 장점과 단점을 고려하겠습니다.

처리 방법

다음은 이러한 문제를 처리하는 몇 가지 방법입니다.

1. goroutine을 시작할 때 함수가 포인터를 전달해야 하지만 함수 수준이 매우 깊은 경우, 안전을 보장하려면 포인터를 직접 전달하는 대신 이 포인터를 개체의 복제본에 전달하세요

fun main() {
    request := request.NewRequest()
    go saveRequestToRedis1(request.Clone())
    go saveReuqestToRedis2(request.Clone())
     
    select{}
 
}

Clone 함수는 별도로 작성해야 합니다. 구조가 정의된 후에는 이 방법을 따르면 됩니다. 예:

func (this *Request)Clone(){
    newRequest := NewRequst()
    newRequest.ToUsers = make([]int, len(this.ToUsers))
    copy(newRequest.ToUsers, this.ToUsers)
 
}

其实从效率角度考虑这样确实会产生不必要的Clone的操作,耗费一定内存和CPU。但是在我看来,首先,为了安全性,这个尝试是值得的。

其次,如果项目对效率确实有很高的要求,那么你不妨在开发阶段遵照这个原则使用clone,然后在项目优化阶段,作为一种优化手段,将不必要的Clone操作去掉。这样就能在保证安全的前提下做到最好的优化。

2、什么时候使用go的问题

有两种思维逻辑会想到使用goroutine:

1 业务逻辑需要并发

比如一个服务器,接收请求,阻塞式的方法是一个请求处理完成后,才开始第二个请求的处理。其实在设计的时候我们一定不会这么做,我们会在一开始就已经想到使用并发来处理这个场景,每个请求启动一个goroutine为它服务,这样就达到了并行的效果。这种goroutine直接按照思维的逻辑来使用goroutine

2 性能优化需要并发

一个场景是这样:需要给一批用户发送消息,正常逻辑会使用

for _, user := range users {
    sendMessage(user)
 
}

但是在考虑到性能问题的时候,我们就不会这样做,如果users的个数很大,比如有1000万个用户?我们就没必要将1000万个用户放在一个routine中运行处理,考虑将1000万用户分成1000份,每份开一个goroutine,一个goroutine分发1万个用户,这样在效率上会提升很多。这种是性能优化上对goroutine的需求

按照项目开发的流程角度来看。在项目开发阶段,第一种思路的代码实现会直接影响到后续的开发实现,因此在项目开发阶段应该马上实现。

但是第二种,项目中是由很多小角落是可以使用goroutine进行优化的,但是如果在开发阶段对每个优化策略都考虑到,那一定会直接打乱你的开发思路,会让你的开发周期延长,而且很容易埋下潜在的不安全代码。

因此第二种情况在开发阶段绝不应该直接使用goroutine,而该在项目优化阶段以优化的思路对项目进行重构。

推荐:golang开发栏目

위 내용은 Go 언어의 고루틴 사용법에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

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