2

I'm aware of the sync package and its waitgroup options, I don't want to use it for this test. I'm testing a kind of semaphore.

So I've got:

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {

    fmt.Print("wassap")

    jobs := make(chan int)
    processStarted := make(chan struct{}, 1)
    processCompleted := make(chan struct{}, 1)

    createJobs(jobs)

    go func() {
        worker(jobs, processStarted, processCompleted)
    }()

    go func() {
        sync(processStarted, processCompleted)
    }()

    time.Sleep(3600 * time.Second)
    fmt.Print("\nend of main...")

    interrupt := make(chan os.Signal)
    <-interrupt

}

func createJobs(jobs chan<- int) {
    defer close(jobs)
    for i := 1; i < 20; i++ {
        jobs <- i
    }
}

func worker(jobs <-chan int, processStarted <-chan struct{}, processCompleted <-chan struct{}) {

    for {
        select {
        case i := <-jobs:
            fmt.Printf("\nFetching job #%d from channel", i)
            time.Sleep(2 * time.Second)
        case <-processStarted:
            fmt.Print("\nProcess Started. Waiting for it to be completed")
            <-processCompleted
            fmt.Print("\nProcess completed")
        }

    }
}

func sync(processStarted chan<- struct{}, processCompleted chan<- struct{}) {

    // acquire semaphore. Send signal to channel to indicate that it is busy
    processStarted <- struct{}{}

    for i := 1; i < 5; i++ {
        fmt.Printf("\nprocessing %d", i)
        time.Sleep(5 * time.Second)
    }

    // release semaphore
    processCompleted <- struct{}{}
}

What I'm trying to test is fairly simple: I've got a createJobs function whose only purpose is to add elements to a channel, in this case an int channel. Then I've got a worker that is going to pull out objects from that channel and sleep for 2 seconds before pulling the next element.

Now, there's also a sync function. The sole purpose of this function is to simulate a process that was initiated while worker was running. If this process is active, then processing of jobs elements should be stopped while sync ends, reason why I've got two channels, one to indicate that the process started and one that the process ended.

When running my code I'm getting the following error:

fatal error: all goroutines are asleep - deadlock!

If I modify the way createJobs is called by wrapping it out in a goroutine like this:

go func() {
        createJobs(jobs)
    }()

then my code runs correctly.

I just want to understand why this is happening. I mean: main routine is being executed, then it hits the call to createJobs (no wrap) so main routine is supposed to be blocked until this call ends. Once createJobs has ended, it means there are elements in the channel. main continues execution and spin up the other goroutines worker and sync to do their stuff. Before main ends, I'm simply adding a sleeper to give time to the previously created goroutines to finish.

I'm not asking to other solutions to this problem, I just want to know what's going on when createJobs occurs outside a goroutine.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
MrCujo
  • 1,218
  • 3
  • 31
  • 56
  • Does this answer your question? [What's the difference between c:=make(chan int) and c:=make(chan int,1)?](https://stackoverflow.com/questions/23233381/whats-the-difference-between-c-makechan-int-and-c-makechan-int-1) – Zeke Lu Mar 28 '23 at 23:06

2 Answers2

3

You are declaring jobs as an unbuffered channel and then trying to push synchronously 20 values into it. This will block your main function when you call createJobs(jobs).

Changing line 13 to:

    jobs := make(chan int, 20)

...will solve the deadlock.


EDIT - clarifications requested in the comments:

Unbuffered channels have no capacity and block the execution of the producer until a consumer receives the message.

A pretty good analogy for an unbuffered channel is a pipe and in this example the process looks like this:

+------------------+     +------------+      +-------------+
| PRODUCER         |     | PIPE       |      | CONSUMER    |
|                  +---->|            +----->|             |
| createJobs(jobs) |     | unbuffered |      | worker(...) |
|                  |     | channel    |      |             |
+------------------+     +------------+      +-------------+

The deadlock occurs because createJobs(jobs) is invoked synchronously, and there's no consumer yet running.

It is working when the function(PRODUCER) is called within a goroutine because basically the insertion into the channel and the read from it are happening in parallel?

Yes. If the producer is invoked asynchronously, it won't block the main() function and therefore the consumer will get the chance to be invoked as well. In this case the producer will push all its tasks, one by one, as they are consumed by the worker, one by one.

Neo Anderson
  • 5,957
  • 2
  • 12
  • 29
  • the fact that it is unbuffered, means that it will block main forever as it has no way to know when the channel will be closed? is that right? by making it a buffered channel, it knows that after filling it up with 20 elements it can conclude the call to that function? – MrCujo Mar 28 '23 at 23:17
  • Unbuffered channels have no capacity and therefore require both goroutines to be ready to make any exchange. When a goroutine attempts to send a resource to an unbuffered channel and there is no goroutine waiting to receive the resource, the channel will lock the sending goroutine and make it wait – Neo Anderson Mar 28 '23 at 23:18
  • not 100% accurate: think about it as having capacity 1. When createJobs() is invoked, the first job will get in the channel, but if the consumer routines isn't started yet, the createJobs() will wait to push the second job in the channel forever – Neo Anderson Mar 28 '23 at 23:21
  • 1
    ahh, i see, that's what i wanted to understand. Thanks for the explanation! – MrCujo Mar 28 '23 at 23:22
  • one last question. It is working when the function is called within a goroutine becasue basically the insertion into the channel and the read from it are happening in parallel? – MrCujo Mar 28 '23 at 23:24
  • 1
    @MrCujo yes. it is the base example producer -> pipe -> consumer, running in parallel, with no more than one job in the pipe at any given time. Difference is that the producer will push jobs in the channel once the worker freed the unbuffered channel. When you create a buffer with capacity 20, your producer will be able to push all the jobs from the for loop, synchronously, and later on the consumer is triggered into execution – Neo Anderson Mar 28 '23 at 23:33
0

Previous solution totally working. But you can also use

    go func() {
        createJobs(jobs)
    }()

If you still want to keep default channel size.

Loïc Madiès
  • 277
  • 1
  • 5
  • 19
  • I know that this works, if you read the question I already stated that this is the way I got it to work, i don't wanna know how to make it work, just want to understand why the initial way of doing it is not working – MrCujo Mar 28 '23 at 23:18