为什么基于频道的锁定阻止?

时间:2016-09-05 14:19:15

标签: multithreading go locking channel

您好我正在编写Lock使用频道,该频道旨在锁定/解锁给定“应用”的操作。

一般的想法是,一个goroutine继续收听两个频道:lockChunlockCh。任何Lock()操作都会向lockCh发送一个自制频道,并等待从该自制频道进行阅读,通过阅读此频道表示Lock()成功。

类似的处理适用于Unlock()

对于听众来说,它会在接受Lock()时检查'app'是否已被锁定,如果是,它会将该自制频道放到服务员列表的尾部。如果有人Unlock(),它会唤醒(通过向频道发送消息)下一个服务员或删除服务员列表,如果没有其他人在等待锁定。

代码放在下面,我不知道哪里出错了,但测试用例没有通过(它在几个Lock()Unlock()之后阻塞!)

感谢您给我一些建议。

type receiver struct {
            app  string
            ch   chan struct{}
            next *receiver
}

type receiveList struct {
            head *receiver
            tail *receiver
}

type AppLock struct {
            lockCh   chan receiver
            unlockCh chan receiver

            // Consider lock x:
            // if map[x] doesn't exist, x is unlocked
            // if map[x] exist but head is nil, x is locked but no waiter
            // if map[x] exist and head isn't nil, x is locked and there're waiters
            m map[string]receiveList
}

func NewAppLock() *AppLock {
            l := new(AppLock)
            l.lockCh = make(chan receiver)
            l.unlockCh = make(chan receiver)
            l.m = make(map[string]receiveList)

            go l.lockRoutine()
            return l
}

func (l *AppLock) Lock(app string) {
            ch := make(chan struct{})
            l.lockCh <- receiver{
                app: app,
                ch:  ch,
            }
            <-ch
}

func (l *AppLock) Unlock(app string) {
            ch := make(chan struct{})
            l.unlockCh <- receiver{
                app: app,
                ch:  ch,
            }
            <-ch
}

func (l *AppLock) lockRoutine() {
            for {
                select {
                case r := <-l.lockCh:
                    rlist, ok := l.m[r.app]
                    if ok { // already locked
                        if rlist.head == nil { // no waiter
                            rlist.head = &r
                            rlist.tail = &r
                        } else { // there're waiters, wait in queue
                            rlist.tail.next = &r
                            rlist.tail = &r
                        }
                    } else { // unlocked
                        rlist = receiveList{}
                        l.m[r.app] = rlist
                        r.ch <- struct{}{}
                    }
                case r := <-l.unlockCh:
                    rlist, ok := l.m[r.app]
                    if ok {
                        if rlist.head == nil { // no waiter
                            delete(l.m, r.app)
                            r.ch <- struct{}{}
                        } else { // there're waiters
                            candidate := rlist.head
                            if rlist.head == rlist.tail {
                                rlist.head = nil
                                rlist.tail = nil
                            } else {
                                rlist.head = rlist.head.next
                            }
                            candidate.ch <- struct{}{}
                            r.ch <- struct{}{}
                        }
                    } else {
                        panic("AppLock: inconsistent lock state")
                    }
                }
            }
}

测试:

func main() {
        app := "APP"
        appLock := NewAppLock()
        c := make(chan bool)

        for i := 0; i < 10; i++ {
            go func(l *AppLock, loops int, cdone chan bool) {
                for i := 0; i < loops; i++ {
                    l.Lock(app)
                    l.Unlock(app)
                }
                cdone <- true
            }(appLock, 1, c)
        }

        for i := 0; i < 10; i++ {
            <-c
        }

    fmt.Println("DONE")
}

4 个答案:

答案 0 :(得分:0)

您是否需要实现包含链表的状态机?使用Go的正常通道争用提供队列可能会有一个更简单的解决方案。

因此,客户端排队等待访问无缓冲的通道。锁定服务只是等待客户端。当一个人到达时,锁定服务进入锁定状态。使用无缓冲通道发出解锁信号,状态返回解锁状态。

虽然锁定,但锁定服务会忽略后续获取锁定的请求。那些客户端必须等待(Go运行时为此提供了一个队列)。每次解锁时,服务都可以接受下一个客户端。

        for i := 0; i < loops; i++ {
            l.Lock()
            l.Unlock()
        }

调用循环包含

l.Lock()

频道是无缓冲的非常重要,因为客户必须等到已完成的集合,才能知道他们已经获得(l.Unlock())或释放(If arVerb.Contains(txtBoxVerb.Text) Then ... )锁定。

https://play.golang.org/p/2j4lkeOR_s

<强> PS

我不禁想知道你是否只需要RWMutex(https://golang.org/pkg/sync/#RWMutex

答案 1 :(得分:0)

我刚刚在代码中发现了错误。

type AppLock struct {
    lockCh   chan receiver
    unlockCh chan receiver

    // Consider lock x:
    // if map[x] doesn't exist, x is unlocked
    // if map[x] exist but head is nil, x is locked but no waiter
    // if map[x] exist and head isn't nil, x is locked and there're waiters
    m map[string]receiveList
}

最初,我认为head中的tailreceiveList都是指针,所以我们总是可以在相同的服务员列表上操作,即使receiveList不是&#39 ; ta指针类型。 (显然这是错的)

确实,如果我们只使用headtail而不使用receiveList的指针类型,那就没关系了。但是,我确实在lockRoutine中写入了它们,它正在写入它们的副本。这就是问题所在。

答案 2 :(得分:0)

对此有一个更简单的解决方案。单槽缓冲通道已经可以作为一个简单的锁定器。不需要管理器例程或链表列表:

// AppLock maintains a single exclusive lock for each app name string provided
type AppLock struct {
    // Consider lock x:
    // if map[x] doesn't exist, x is implicitly unlocked
    // if map[x] exist and can be read from, x is unlocked
    // if map[x] exist and blocks on read, x is locked
    m map[string]chan struct{}

    // RWMutex to protect the map writes, but allow free read access
    mut sync.RWMutex
}

func NewAppLock() *AppLock {
    l := new(AppLock)
    l.m = make(map[string]chan struct{})
    return l
}

func (l *AppLock) Lock(app string) {
    l.mut.RLock()
    ch, ok := l.m[app]
    l.mut.RUnlock()

    if ok {
        <-ch // blocks until lock acquired
    } else {
        l.mut.Lock()
        l.m[app] = make(chan struct{}, 1)
        // we don't want to put anything on it yet, because we still want the lock
        l.mut.Unlock()
    }
}

func (l *AppLock) Unlock(app string) {
    l.mut.RLock()
    ch, ok := l.m[app]
    l.mut.RUnlock()

    if ok {
        select {
        case ch <- struct{}{}:
            // do nothing, expected behavior
        default:
            // something already on channel, which means it wasn't locked
            panic("Unlock called on app not previously locked")
        }
    } else {
        panic("Unlock called on app not previously locked")
    }
}

https://play.golang.org/p/o02BHfRto3

这为每个应用维护一个缓冲为1的通道。要锁定,您尝试从频道中读取。如果你收到一些东西(它只是代表锁的通用标记),你就得到了锁,然后你将令牌放回到通道上以解锁它。如果接收阻塞,则其他东西具有锁定,并且当其他例程解锁时接收将解除阻塞(将令牌放回到通道上)。

顺便提一下,这仍然可以作为先进先出系统,因为Go频道是有序的,并且将填补最早开始接收的接收(在那些仍在尝试接收的接收中)。

答案 3 :(得分:-1)

我认为问题在于您的main for循环。您没有关闭导致死锁的任何地方的c频道。尝试将循环更改为:

for i := 0; i < 10; i++ {
    var wg sync.WaitGroup // use WaitGroup
    wg.Add(1)
    go func(l *AppLock, loops int, cdone chan bool) {
        defer wg.Done()
        for i := 0; i < loops; i++ {
            l.Lock(app)
            l.Unlock(app)
        }
        cdone <- true
    }(appLock, 1, c)
    go func() {
        wg.Wait()
        close(c)
    }()
}

工作代码:https://play.golang.org/p/cbXj0CPTGO

虽然这可以解决死锁问题,但这并不一定意味着程序的行为是正确的。你需要更好的测试来解决这个问题。