custom mutex - all goroutines are asleep - deadlock

68 Views Asked by At

Im trying to make a simple mutext with a specific behavior. But the specific behavior is imposible because golang doesnt work out of the blue

simple test code returns an error: all goroutines are asleep - deadlock!

A first block with for works perfectly without an error Second block failures on m.Unlock()

What's wrong? What's happened? I am only human. I write code like human. And there is human logic:

  1. I opened the channel with buffer
  2. I sent the value to the channel What go-routines should I think about?
Result:
=== RUN   TestNiceMutex_Lock
=== RUN   TestNiceMutex_Lock/test
   nicemutex_test.go:36: main: lock
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
testing.(*T).Run(0xc00007b380, {0xcee048?, 0xc1f6cd?}, 0xcf8388)
C:/Program Files/Go/src/testing/testing.go:1649 +0x3c8
testing.runTests.func1(0xdecd00?)
C:/Program Files/Go/src/testing/testing.go:2054 +0x3e
testing.tRunner(0xc00007b380, 0xc0000b7c48)
C:/Program Files/Go/src/testing/testing.go:1595 +0xff
testing.runTests(0xc00009a140?, {0xde0d90, 0x1, 0x1}, {0xbc3345?, 0xc0000b7d08?, 0x0?})
C:/Program Files/Go/src/testing/testing.go:2052 +0x445
testing.(*M).Run(0xc00009a140)
C:/Program Files/Go/src/testing/testing.go:1925 +0x636
main.main()
_testmain.go:47 +0x19c

goroutine 6 [chan receive]:
testing.(*T).Run(0xc00007b520, {0xceae57?, 0xc78420?}, 0xc00023c000)
C:/Program Files/Go/src/testing/testing.go:1649 +0x3c8
simply/nicemutex.TestNiceMutex_Lock(0x0?)
C:/Users/Aleksey/go/src/simply/nicemutex/nicemutex_test.go:26 +0xba
testing.tRunner(0xc00007b520, 0xcf8388)
C:/Program Files/Go/src/testing/testing.go:1595 +0xff
created by testing.(*T).Run in goroutine 1
C:/Program Files/Go/src/testing/testing.go:1648 +0x3ad

(content is repeated)

goroutine 97 [chan send]:
simply/nicemutex.lock()
C:/Users/Aleksey/go/src/simply/nicemutex/nicemutex.go:6
simply/nicemutex.TestNiceMutex_Lock.func1()
C:/Users/Aleksey/go/src/simply/nicemutex/nicemutex_test.go:12 +0x25
created by simply/nicemutex.TestNiceMutex_Lock in goroutine 6
C:/Users/Aleksey/go/src/simply/nicemutex/nicemutex_test.go:11 +0x2c


goroutine 117 [chan send]:
simply/nicemutex.(*NiceMutex).Lock(0xc000250008)
C:/Users/Aleksey/go/src/simply/nicemutex/nicemutex.go:25 +0x99
simply/nicemutex.TestNiceMutex_Lock.func2.1()
C:/Users/Aleksey/go/src/simply/nicemutex/nicemutex_test.go:32 +0x5f
created by simply/nicemutex.TestNiceMutex_Lock.func2 in goroutine 116
C:/Users/Aleksey/go/src/simply/nicemutex/nicemutex_test.go:30 +0xa5


Process finished with the exit code 1



package nicemutex

import (
    "sync"
    "testing"
    "time"
)

func TestNiceMutex_Lock(t *testing.T) {
    for i := 0; i < 100; i++ {
        go func() {
            lock()
            unlock()
        }()
        lock()
        unlock()
    }

    tests := []struct {
        name string
    }{
        {"test"},
    }
    wg := sync.WaitGroup{}
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            var m NiceMutex
            m.Lock()
            wg.Add(1)
            go func() {
                defer m.Unlock()
                m.Lock()
                t.Log("go: lock")
                wg.Done()
            }()
            t.Log("main: lock")
            time.Sleep(time.Second * 1)
            m.Unlock() // <- error
            wg.Wait()
        })
    }
}

package nicemutex

var internalLock = make(chan struct{}, 1)

func lock() {
    internalLock <- struct{}{} // <- error
}

func unlock() {
    <-internalLock
}

type NiceMutex struct {
    ch chan struct{}
}

func (m *NiceMutex) Lock() {
    defer unlock()
    lock()

    if m.ch == nil {
        m.ch = make(chan struct{}, 1)
    }
    m.ch <- struct{}{}
}

func (m *NiceMutex) Unlock() {
    defer unlock()
    lock() // <- error

    if m.ch == nil {
        return
    }
    <-m.ch
}

Updated: New version - same result

package nicemutex

import (
    "log"
    "time"
)

var (
    internalLock2 = make(chan struct{}, 1)
    TraceEnabled2 = false
)

type NiceMutex2 struct {
    ch       chan struct{}
    lockedOn int64
}

func (m *NiceMutex2) Lock() {
    internalLock2 <- struct{}{}
    defer func() { <-internalLock2 }()

    m.lockedOn = time.Now().UnixNano()

    if m.ch == nil {
        m.ch = make(chan struct{}, 1)
    }
    m.ch <- struct{}{}

    trace2("locked")
}

func (m *NiceMutex2) LockForSec() {
    internalLock2 <- struct{}{}
    defer func() { <-internalLock2 }()

    on := time.Now().UnixNano()
    m.lockedOn = on

    if m.ch == nil {
        m.ch = make(chan struct{}, 1)
    }
    m.ch <- struct{}{}

    go func(lockedOn int64) {
        <-time.After(time.Second)

        internalLock2 <- struct{}{}
        defer func() { <-internalLock2 }()

        if m.lockedOn == lockedOn {
            select {
            case <-m.ch:
                trace2("timeout used", true)
            default:
                trace2("timeout ignore")
            }
        }
    }(on)

    trace2("locked")
}

func (m *NiceMutex2) Unlock() {
    internalLock2 <- struct{}{}
    defer func() { <-internalLock2 }()

    m.lockedOn = time.Now().UnixNano()

    if m.ch == nil {
        return
    }
    select {
    case <-m.ch:
    default:
    }

    trace2("unlocked")
}

func trace2(msg string, ignoreEnabling ...bool) {
    if TraceEnabled2 || (len(ignoreEnabling) > 0 && ignoreEnabling[0]) {
        log.Println(msg)
    }
}
2

There are 2 best solutions below

4
coxley On BEST ANSWER

Welcome to SO!

Firstly, using channels like this as a mutex isn't exactly "simple". Reasoning about concurrency state is complex.

The reason for the deadlock is because the channel buffers are full.

  • main m.Lock() fills internalLock
  • main m.Lock() fills m.ch
  • main m.Lock() drains internalLock
  • goroutine m.Lock() fills internalLock
  • goroutine m.Lock() blocks as m.ch buffer is full
  • goroutine m.Lock() never drains internalLock
  • main m.Unlock() blocks as internalLock buffer is full
  • main m.Unlock() never drains m.ch

Both the goroutine and main paths are blocked forever. The goroutine will never drain internalLock until it can write to m.ch. And the opposite is true for main.

0
Aleksey Fomin On

My mistake! :) coxley, you are right I wrote locking only on start (if chan is nil) and it is working perfect

package nicemutex

import (
    "log"
    "time"
)

var (
    internalLock2 = make(chan struct{}, 1)
    TraceEnabled2 = false
)

type NiceMutex2 struct {
    ch       chan struct{}
    lockedOn int64
}

func (m *NiceMutex2) Lock() {
    if m.ch == nil {
        func() {
            internalLock2 <- struct{}{}
            defer func() { <-internalLock2 }()
            m.ch = make(chan struct{}, 1)
        }()
    }
    m.ch <- struct{}{}

    trace2("locked")
}

func (m *NiceMutex2) LockForSec() {
    if m.ch == nil {
        func() {
            internalLock2 <- struct{}{}
            defer func() { <-internalLock2 }()
            m.ch = make(chan struct{}, 1)
        }()
    }
    m.ch <- struct{}{}

    go func() {
        <-time.After(time.Second)

        select {
        case <-m.ch:
            trace2("timeout used", true)
        default:
            trace2("timeout ignore")
        }
    }()

    trace2("locked")
}

func (m *NiceMutex2) Unlock() {
    if m.ch == nil {
        return
    }
    select {
    case <-m.ch:
    default:
    }

    trace2("unlocked")
}

func trace2(msg string, ignoreEnabling ...bool) {
    if TraceEnabled2 || (len(ignoreEnabling) > 0 && ignoreEnabling[0]) {
        log.Println(msg)
    }
}