Same problem when I first started messing with it. It’s very easy to get the threadpool locked up…
By default, there’s nothing really keeping track of the lifetime of any go-loops or threads. It’s very easy to spawn off infinite processes. So…always ensure there’s some lifetime management, such as an atom, timeouts, or a poison pill channel.
I rigged up a system infrastructure to help manage this kind of stuff when I was experimenting. There’s an example here of using it. The basic premise is having a process abstraction to keep a handle for communicating with the async stuff, and providing means to start/stop/kill, etc. So really just any way to provide visibility on the stuff you’re spooling up. I actually found it to be quite a useful exercise to build something like this since it helped me learn more about core.async in the process.
Another one is the default (chan) will have a 1 for 1 channel. I found that - when experimenting early on - using something more forgiving like dropping channels were helpful to at least keep things “moving” when I screwed up.
I think there are some better libraries for process management (even an erlang OTP actor-like one with supervisors). That gets you a bit away from learning core.async, but they could be useful down the road.