Recursion in golang is giving deadlock or negative WaitGroup counter when using goroutines, channels and sync.Waitgroup

Issue

I am trying to find the list of all directories using a recursive function. The code to the function is

func FindDirs(dir string, nativePartitions []int64, wg *sync.WaitGroup, dirlistchan chan string) {
    // defer wg.Done here will give negative waitgroup panic, commenting it will give negative waitgroup counter panic
    fd, err := os.Open(dir)
    if err != nil {
        panic(err)
    }
    filenames, err := fd.Readdir(0)
    if err != nil {
        panic(err)
    }

    for _, i := range filenames {

        var buff bytes.Buffer
        buff.WriteString(dir)
        switch dir {
        case "/":
        default:
            buff.WriteString("/")
        }

        buff.WriteString(i.Name())
        /*err := os.Chdir(dir)
        if err != nil {
            return err
        }*/

        t := new(syscall.Statfs_t)
        err = syscall.Statfs(buff.String(), t)
        if err != nil {
            //fmt.Println("Error accessing", buff.String())
        }
        if checkDirIsNative(t.Type, nativePartitions) && i.IsDir(){
            dirlistchan <- buff.String()
            FindDirs(buff.String(), nativePartitions, wg, dirlistchan) //recursion happens here
        } else {
            //fmt.Println(i.Name(), "is not native")
        }


    }
}

and in the main function, I am calling it as

wg := new(sync.WaitGroup)
dirlistchan := make(chan string, 1000)
wg.Add(1)
go func() {
    filtermounts.FindDirs(parsedConfig.ScanFrom, []int64{filtermounts.EXT4_SUPER_MAGIC}, wg, dirlistchan)
}()


go func() {
    wg.Wait()
    close(dirlistchan)
}()
for i := range dirlistchan {
    fmt.Println(i)
}
wg.Wait()

and I am getting a

fatal error: all goroutines are asleep - deadlock!

I was able to get this working if I am printing the result instead of using channels, or append to a slice using mutex. (verified with the linux find command to see if the results are same.) Please find the function after omitting channels and using sync.Mutex and append.

func FindDirs(dir string, nativePartitions []int64, dirlist *[]string, mutex *sync.Mutex) []string{


    fd, err := os.Open(dir)
    defer fd.Close()
    if err != nil {
        panic(err)
    }
    filenames, err := fd.Readdir(0)
    if err != nil {
        panic(err)
    }

    for _, i := range filenames {
        var buff bytes.Buffer
        buff.WriteString(dir)
        switch dir {
        case "/":
        default:
            buff.WriteString("/")
        }

        buff.WriteString(i.Name())
        /*err := os.Chdir(dir)
        if err != nil {
            return err
        }*/

        t := new(syscall.Statfs_t)
        err = syscall.Statfs(buff.String(), t)
        if err != nil {
            //fmt.Println("Error accessing", buff.String())
        }
        if checkDirIsNative(t.Type, nativePartitions) && i.IsDir(){
            //dirlistchan <- buff.String()
            mutex.Lock()
            *dirlist = append(*dirlist, buff.String())
            mutex.Unlock()
            //fmt.Println(buff.String())

            FindDirs(buff.String(), nativePartitions, dirlist, mutex)
        } else {
            //fmt.Println(i.Name(), "is not native")
        }

    }
    return *dirlist
}

But I cannot think of a way to make this work with channels and goroutines. Any help is greatly appreciated.

Note: Here is a link to the golang playground with the code. I couldn’t find a workaround to get the syscall thing to work on the playground either. It works on my system though.

Thanks.

Solution

Short answer : You are not closing the channel.

Fix : add defer wg.Done() at beginning of the go routine that calls FindDirs

go func() {
    defer wg.Done()
    filtermounts.FindDirs(parsedConfig.ScanFrom, []int64{filtermounts.EXT4_SUPER_MAGIC}, wg, dirlistchan)
}()

Why did it happen

The go routine that is responsponsible for closing the channel waits for wg there is no wg.Done in the code above. So close never happens

Now the for loop blocks on the channel for close or a value for ever, this cause the error

fatal error: all goroutines are asleep - deadlock!

So here is your code ,this may be run as

go run filename.go /path/to/folder

Code

package main

import (
    "bytes"
    "fmt"
    "os"
    "sync"
    "syscall"
)

func main() {

    wg := new(sync.WaitGroup)
    dirlistchan := make(chan string, 1000)
    wg.Add(1)
    go func() {
        defer wg.Done()
        FindDirs(os.Args[1], []int64{61267}, wg, dirlistchan)
    }()

    go func() {
        wg.Wait()
        close(dirlistchan)
    }()
    for i := range dirlistchan {
        fmt.Println(i)
    }
    wg.Wait()

}

func FindDirs(dir string, nativePartitions []int64, wg *sync.WaitGroup, dirlistchan chan string) {
    fd, err := os.Open(dir)
    if err != nil {
        panic(err)
    }
    filenames, err := fd.Readdir(0)
    if err != nil {
        panic(err)
    }

    for _, i := range filenames {

        var buff bytes.Buffer
        buff.WriteString(dir)
        switch dir {
        case "/":
        default:
            buff.WriteString("/")
        }

        buff.WriteString(i.Name())
        /*err := os.Chdir(dir)
          if err != nil {
              return err
          }*/

        t := new(syscall.Statfs_t)
        err = syscall.Statfs(buff.String(), t)
        if err != nil {
            //fmt.Println("Error accessing", buff.String())
        }
        if checkDirIsNative(t.Type, nativePartitions) && i.IsDir() {
            dirlistchan <- buff.String()
            FindDirs(buff.String(), nativePartitions, wg, dirlistchan) //recursion happens here
        } else {
            //fmt.Println(i.Name(), "is not native")
        }

    }
}

func checkDirIsNative(dirtype int64, nativetypes []int64) bool {
    for _, i := range nativetypes {
        if dirtype == i {
            return true
        }
    }
    return false
}

Find the go.play link here

Answered By – Sarath Sadasivan Pillai

Answer Checked By – Robin (GoLangFix Admin)

Leave a Reply

Your email address will not be published.