优雅地处理由传递通道和共享内存引起的数据竞争的方法

huangapple go评论76阅读模式
英文:

Elegant way to handle data race caused by passing channel and shared memory

问题

这是一篇关于Go语言中数据竞争模式的案例。

> 混合使用消息传递(通道)和共享内存会使代码复杂且容易发生数据竞争。

以下是一个关于f.err的数据竞争案例,它可以在StartWait中被访问到。

另一个问题是当ctx.Done()f.ch <- struct{}{}之前发生时,f.ch <- struct{}{}可能会永远阻塞。

在Golang中,有什么优雅的方法来处理这个问题?


我首先想到的方法是在Start函数中添加上下文(context)。

func (f *Future) Wait(ctx context.Context) error {
	select {
	case <-f.ch:
		return nil
	case <-ctx.Done():
		f.err = ctx.Err()
		return ctx.Err()
	}
}

func (f *Future) StartWithContext(ctx context.Context) {
	go func() {
		resp, err := f.f()

		select {
		case <-ctx.Done():
			return
		default:
			f.resp = resp
			f.err = err
			f.ch <- struct{}{}
		}
	}()
}

我们想知道我的解决方案是否有遗漏的地方?或者是否有更好的解决方案来解决这个问题?

英文:

Here is one case of Data Race Patterns in Go

> Mixed use of message passing (channels) and shared memory makes code complex and susceptible to data races

func (f *Future) Start() {
	go func() {
		resp, err := f.f()
		f.resp = resp
		f.err = err        // data race
		f.ch &lt;- struct{}{} // may block forever
	}()
}

func (f *Future) Wait(ctx context.Context) error {
	select {
	case &lt;-f.ch:
		return nil
	case &lt;-ctx.Done():
		f.err = ctx.Err() // data race
		return ctx.Err()
	}
}

Here is one data race for f.err, which could be accessed both in Start and Wait.

Another issue is that f.ch &lt;- struct{}{} may block forever when ctx.Done() happened before f.ch &lt;- struct{}{}.

What is the elegant way to handle it in Golang?


The idea comes to my mind first as below, add context to Start function

func (f *Future) Wait(ctx context.Context) error {
	select {
	case &lt;-f.ch:
		return nil
	case &lt;-ctx.Done():
		f.err = ctx.Err()
		return ctx.Err()
	}
}

func (f *Future) StartWithContext(ctx context.Context) {
	go func() {
		resp, err := f.f()

		select {
		case &lt;-ctx.Done():
			return
		default:
			f.resp = resp
			f.err = err
			f.ch &lt;- struct{}{}
		}
	}()
}

We want to know if something missing in my solution? or is there a better solution to this issue?

答案1

得分: 1

第二段代码片段仍然存在数据竞争问题。StartWithContext 可能会先进入 default 分支,然后 Wait 进入 ctx.Done() 分支。

你遇到了共享内存的问题,最明显的解决方法是使用互斥锁(mutex)。此外,请注意你正在将 f.ch 用作完成通道,所以代码如下:

type Future struct {
   sync.Mutex
   ...
}

func (f *Future) Start() {
    go func() {
        resp, err := f.f()
        f.Lock()
        f.resp = resp
        f.err = err 
        f.Unlock()
        close(f.ch)
    }()
}

func (f *Future) Wait(ctx context.Context) error {
    select {
    case <-f.ch:
        return nil
    case <-ctx.Done():
        f.Lock()
        f.err = ctx.Err()
        f.Unlock()
        return ctx.Err()
    }
}
英文:

There is still data race in the second code snippet. It is possible for StartWithContext to fall to default case first, and then Wait falling into the ctx.Done() case.

You have a shared memory problem, and the most obvious way to deal with it is with a mutex. Also note that you are using f.ch as a done-channel, so:

type Future struct {
   sync.Mutex
   ...
}


func (f *Future) Start() {
    go func() {
        resp, err := f.f()
        f.Lock()
        f.resp = resp
        f.err = err 
        f.Unlock()
        close(f.ch)
    }()
}

func (f *Future) Wait(ctx context.Context) error {
    select {
    case &lt;-f.ch:
        return nil
    case &lt;-ctx.Done():
        f.Lock()
        f.err = ctx.Err()
        f.Unlock()
        return ctx.Err()
    }
}

</details>



# 答案2
**得分**: 1

使用通道将错误发送给服务员。

将Future更改为:

     type Future struct {
           ch chan error
           // 其他字段与之前相同
     }

在Start中发送错误:

    func (f *Future) Start() {
    	go func() {
    		resp, err := f.f()
    		f.resp = resp
    		f.ch <- err
    	}()
    }
    
在Wait中接收错误:

    func (f *Future) Wait(ctx context.Context) error {
    	select {
    	case err := <-f.ch:
    		return err
    	case <-ctx.Done():
    		f.err = ctx.Err()
    		return ctx.Err()
    	}
    }

创建一个带缓冲的通道,以防止在上下文被取消时Start中的goroutine永远阻塞。

      f.ch = make(chan error, 1)

<details>
<summary>英文:</summary>

Use the channel to send the error to the waiter. 

Change Future to:

     type Future struct {
           ch chan error
           // other fields as before
     }


Send the error in Start:

    func (f *Future) Start() {
    	go func() {
    		resp, err := f.f()
    		f.resp = resp
    		f.ch &lt;- err
    	}()
    }
    
Receive the error in Wait:

    func (f *Future) Wait(ctx context.Context) error {
    	select {
    	case err := &lt;-f.ch:
    		return err
    	case &lt;-ctx.Done():
    		f.err = ctx.Err()
    		return ctx.Err()
    	}
    }

Create a buffered channel to prevent the goroutine in Start from blocking forever when the context is canceled.

      f.ch = make(chan error, 1)


</details>



huangapple
  • 本文由 发表于 2022年7月20日 12:53:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/73046156.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定