1

For some reason, when I remove the fmt.Printlns then the code is blocking. I've got no idea why it happens. All I want to do is to implement a simple concurrency limiter...

I've never experienced such a weird thing. It's like that fmt flushes the variables or something and makes it work.

Also, when I use a regular function instead of a goroutine then it works too.

Here's the following code -

package main

import "fmt"

type ConcurrencyLimit struct {
    active int
    Limit  int
}

func (c *ConcurrencyLimit) Block() {
    for {
        fmt.Println(c.active, c.Limit)
        // If should block
        if c.active == c.Limit {
            continue
        }
        c.active++
        break
    }
}

func (c *ConcurrencyLimit) Decrease() int {
    fmt.Println("decrease")
    if c.active > 0 {
        c.active--
    }
    return c.active
}

func main() {
    c := ConcurrencyLimit{Limit: 1}
    c.Block()
    go func() {
        c.Decrease()
    }()
    c.Block()
}

Clarification: Even though I've accepted @kaedys 's answer(here) a solution was answered by @Kaveh Shahbazian (here)

Community
  • 1
  • 1
Yehonatan
  • 3,168
  • 7
  • 31
  • 39

4 Answers4

7

You're not giving c.Decrease() a chance to run. c.Block() runs an infinite for loop, but it never blocks in that for loop, just calling continue over and over on every iteration. The main thread spins at 100% usage endlessly.

However, when you add an fmt.Print() call, that makes a syscall, which allows the other goroutine to run.

This post has details on how exactly goroutines yield or are pre-empted. Note, however, that it's slightly out of date, as entering a function now has a random chance to yield that thread to another goroutine, to prevent similar style flooding of threads.

Community
  • 1
  • 1
Kaedys
  • 9,600
  • 1
  • 33
  • 40
  • 1
    There is also a race condition on access to c.active. The `sync` package provides a `Cond` type that can help here. Here's [a version](https://play.golang.org/p/UcREvPkaff) of the above code modified to use `sync.Cond`. – 9nut Jun 28 '16 at 06:18
1

As others have pointed out, Block() will never yield; a goroutine is not a thread. You could use Gosched() in the runtime package to force a yield -- but note that spinning this way in Block() is a pretty terrible idea.

There are much better ways to do concurrency limiting. See http://jmoiron.net/blog/limiting-concurrency-in-go/ for one example

1

What you are looking for is called a semaphore. You can apply this pattern using channels

http://www.golangpatterns.info/concurrency/semaphores

The idea is that you create a buffered channel of a desired length. Then you make callers acquire the resource by putting a value into the channel and reading it back out when they want to free the resource. Doing so creates proper synchronization points in your program so that the Go scheduler runs correctly.

What you are doing now is spinning the cpu and blocking the Go scheduler. It depends on how many cpus you have available, the version of Go, and the value of GOMAXPROCS. Given the right combination, there may not be another available thread to service other goroutines while you infinitely spin that particular thread.

jdi
  • 90,542
  • 19
  • 167
  • 203
1

While other answers pretty much covered the reason (not giving a chance for the goroutine to run) - and I'm not sure what you intend to achieve here - you are mutating a value concurrently without proper synchronization. A rewrite of above code with synchronization considered; would be:

type ConcurrencyLimit struct {
    active int
    Limit  int
    cond   *sync.Cond
}

func (c *ConcurrencyLimit) Block() {
    c.cond.L.Lock()
    for c.active == c.Limit {
        c.cond.Wait()
    }
    c.active++
    c.cond.L.Unlock()

    c.cond.Signal()
}

func (c *ConcurrencyLimit) Decrease() int {
    defer c.cond.Signal()

    c.cond.L.Lock()
    defer c.cond.L.Unlock()

    fmt.Println("decrease")
    if c.active > 0 {
        c.active--
    }
    return c.active
}

func main() {
    c := ConcurrencyLimit{
        Limit: 1,
        cond:  &sync.Cond{L: &sync.Mutex{}},
    }

    c.Block()
    go func() {
        c.Decrease()
    }()
    c.Block()
    fmt.Println(c.active, c.Limit)
}

sync.Cond is a synchronization utility designed for times that you want to check if a condition is met, concurrently; while other workers are mutating the data of the condition.

The Lock and Unlock functions work as we expect from a lock. When we are done with checking or mutating, we can call Signal to awake one goroutine (or call Broadcast to awake more than one), so the goroutine knows that is free to act upon the data (or check a condition).

The only part that may seem unusual is the Wait function. It is actually very simple. It is like calling Unlock and instantly call Lock again - with the exception that Wait would not try to lock again, unless triggered by Signal (or Broadcast) in other goroutines; like the workers that are mutating the data (of the condition).

Kaveh Shahbazian
  • 13,088
  • 13
  • 80
  • 139