Subin Suthan
Subin Suthan

Reputation: 107

Deadlock while using Select in GO lang

I have a doubt regarding the below GO program which calculates the no of letters, digits, special characters and spaces using go routines and select statement

func main() {

    letters := make(chan int)
    digits := make(chan int)
    others := make(chan int)
    spaces := make(chan int)

    //c := make(chan int)
    //tot := 0

    text := `William Butler Yeats (1865-1939) is one of the greatest of all Irish poets.

    No Second Troy

    Why should I blame her that she filled my days
    With misery, or that she would of late
    Have taught to ignorant men most violent ways,
    Or hurled the little streets upon the great,
    Had they but courage equal to desire?
    What could have made her peaceful with a mind
    That nobleness made simple as a fire,
    With beauty like a tightened bow, a kind
    That is not natural in an age like this,
    Being high and solitary and most stern?
    Why, what could she have done, being what she is?
    Was there another Troy for her to burn?`

    go cntLetters(text, letters)
    go cntDigits(text, digits)
    go cntOthers(text, others)
    go cntSpaces(text, spaces)

    for i := 0; i < 5; i++ {
        select {
        case t1 := <-letters:
            fmt.Println("letter :", t1)

        case t2 := <-digits:
            fmt.Println("Digits :", t2)

        case t3 := <-others:
            fmt.Println("Others :", t3)

        case t4 := <-spaces:
            fmt.Println("Spaces :", t4)

        }
    }

    //fmt.Println(tot)

}

func cntLetters(txt string, letters chan int) {

    s := 0
    for _, v := range txt {
        if unicode.IsLetter(v) {
            s++
        }
    }
    letters <- s

}

func cntDigits(txt string, digits chan int) {
    s := 0
    for _, v := range txt {
        if unicode.IsDigit(v) {
            s++
        }
    }
    digits <- s

}

func cntOthers(txt string, others chan int) {
    s := 0
    for _, v := range txt {
        if !unicode.IsDigit(v) && !unicode.IsLetter(v) && !unicode.IsSpace(v) {
            s++
        }
    }
    others <- s

}

func cntSpaces(txt string, spaces chan int) {
    s := 0
    for _, v := range txt {
        if unicode.IsSpace(v) {
            s++
        }
    }
    spaces <- s
}

After running the above code , I am getting the below output,

Spaces : 129
Digits : 8
Others : 16
letter : 464
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()

According to my understanding Select statement waits till any of the channels are ready with data. So I have created separate channels to calculate the no of letters, digits, spaces and characters and whichever channel gets the data first is executed by the select statement.

The query is why the deadlock is happening and any possible ways to remove it?

Upvotes: 1

Views: 67

Answers (2)

fm2279
fm2279

Reputation: 89

I'm just going to provide an alternate approach that might be of use as you work through this. It uses a WaitGroup, a Mutex, and a single channel for counts from your functions. I've included comments to clarify things (hopefully).

package main

import (
    "fmt"
    "sync"
    "unicode"
)

type charKind int

type charCount struct {
    kind charKind
    count int
}

func newCharCount(kind charKind, count int) (*charCount) {
    return &charCount{kind: kind, count: count}
}

const (
    OTHER charKind = iota
    LETTER
    DIGIT
    SPACE
)

func main() {

    text := `William Butler Yeats (1865-1939) is one of the greatest of all Irish poets.

    No Second Troy

    Why should I blame her that she filled my days
    With misery, or that she would of late
    Have taught to ignorant men most violent ways,
    Or hurled the little streets upon the great,
    Had they but courage equal to desire?
    What could have made her peaceful with a mind
    That nobleness made simple as a fire,
    With beauty like a tightened bow, a kind
    That is not natural in an age like this,
    Being high and solitary and most stern?
    Why, what could she have done, being what she is?
    Was there another Troy for her to burn?`
    // make a "done" channel to signal to main
    // goroutine when the range loop is finished
    done := make(chan struct{})
    // make a channel for the output of
    // count functions, using the type
    // above that includes what kind and
    // how many
    countStream := make(chan *charCount)
    // make a waitgroup and add 4, the
    // number of functions you want to
    // wait for (4)
    countWait := &sync.WaitGroup{}
    countWait.Add(4)

    // initialize a variable for the
    // total, scoped outside the go func
    // below, and create a mutex to make
    // more concurrency-safe
    tot := 0
    totLock := sync.Mutex{}

    // spawn your count functions,
    // passing the single channel
    go cntLetters(text, countStream)
    go cntDigits(text, countStream)
    go cntOthers(text, countStream)
    go cntSpaces(text, countStream)

    // spawn a goroutine with a range
    // loop over the channel -- this
    // will exit when we close the count
    // channel
    go func() {
        for cc := range countStream {
            label := "?"
            switch cc.kind {
            case LETTER:
                label = "letters"
            case DIGIT:
                label = "digits"
            case OTHER:
                label = "others"
            case SPACE:
                label = "spaces"
            }
            fmt.Printf("%s: %d\n", label, cc.count)
            // lock, then increment the total, unlock
            totLock.Lock()
            tot += cc.count
            totLock.Unlock()
            // decrement the waitgroup
            countWait.Done()
        }
        done <- struct{}{}
    }()
    // wait for the waitgroup
    countWait.Wait()
    // close the channel, make sure
    // the range loop exits in the above
    // goroutine
    close(countStream)
    // print the total -- the lock/unlock
    // isn't strictly necessary, but a good
    // habit
    totLock.Lock()
    defer totLock.Unlock()
    // wait for range loop goroutine to exit
    <-done
    fmt.Printf("total: %d\n", tot)
}

func cntLetters(txt string, countStream chan<- *charCount) {

    s := 0
    for _, v := range txt {
        if unicode.IsLetter(v) {
            s++
        }
    }
    countStream <- &charCount{kind: LETTER, count: s}

}

func cntDigits(txt string, countStream chan<- *charCount) {
    s := 0
    for _, v := range txt {
        if unicode.IsDigit(v) {
            s++
        }
    }
    countStream <- &charCount{kind: DIGIT, count: s}
}

func cntOthers(txt string, countStream chan<- *charCount) {
    s := 0
    for _, v := range txt {
        if !unicode.IsDigit(v) && !unicode.IsLetter(v) && !unicode.IsSpace(v) {
            s++
        }
    }
    countStream <- &charCount{kind: OTHER, count: s}
}

func cntSpaces(txt string, countStream chan<- *charCount) {
    s := 0
    for _, v := range txt {
        if unicode.IsSpace(v) {
            s++
        }
    }
    countStream <- &charCount{kind: SPACE, count: s}
}

Upvotes: 0

user5712794
user5712794

Reputation:

I would say that the loop is not necessary in this example. Essentially what you are trying to do is to parallelize calculations and at the end produce a report, right?

The report output would depend on the channel communication and is not stable (if you have 2 channels ready to receive value, the one picked would be randomly selected).

In this particular example, what I would suggest is simply this:

fmt.Printf("Letters : %d\n", <-letters)
fmt.Printf("Digits : %d\n", <-digits)
fmt.Printf("Others : %d\n", <-others)
fmt.Printf("Spaces : %d", <-spaces)

The time that your loop will spend is slightly greater than the time you can receive from the slowest channel. Let's say letters are the slowest one, then others will proceed immediately, and you will have a stable output and the penalty is only that you will see the report once all of them are done.

However, there is a much stronger argument that your functions doing calculations should not receive channel only to produce a value. It is much simpler to create a synchronous interface of a function and make it asynchronous later. For example:

func cntDigits(txt string) int {
    s := 0
    for _, v := range txt {
        if unicode.IsDigit(v) {
            s++
        }
    }
    return s
}

// caller making it asynchronous

go func() {
  digits <- cntDigits(text)
}()

With this in mind, if you want results as quickly as you can, you can simply use a waitgroup:

var wg sync.WaitGroup
wg.Add(1) // has to be before you spin up goroutine
go func() { 
  fmt.Println("Digits: ", cntDigits(text))
  wg.Done()
}()

wg.Wait()

Having said that, if you want to use a loop and select statement, then you need to make sure that number of iterations matches the number of reads.

Upvotes: 1

Related Questions