I recommend using “worker pool” pattern I described in a quick tutorial Concurrency patterns for controlled parallelisms (discussion) which is just a simple wrapper around a channel and tasks.
Semaphore may be useful for simple things sometimes but lock and lock-like constructs often yield non-composable and hard-to-read code. “Don’t communicate by sharing memory, share memory by communicating.” is one of very practical Go Proverbs. Since a lot of good practical ideas in concurrency are developed in Go, I think it’d be useful to steal some patterns from Go. Some of them are discussed in my tutorial I linked above.
It is also a waste of resource to allocate tasks and then limit how many of them can run. It is usually much cleaner to match the bound and the number of tasks in the first place, in terms of performance and code structure.
Channel
uses lock internally.
A nitpick: There’s no way in Julia to “change tasks to threads.” There is no language-level API to create an OS thread. The only thing you can do is to create a task which comes with two flavors:
- A task scheduled with
Threads.@spawn
can be executed by arbitrary worker threads. - A task scheduled with
@async
uses the same worker thread as the parent task.
(I’m sure you already know this since you are mentioning the “sticky bit.” This is rather for avoiding confusion of other readers.)