core-async

souenzzo 2019-07-11T00:19:17.205200Z

There is some docs/reference about how to "asyncfy" a library like clojure.jdbc ? I know that I need to create a worker pool, inset in a queue and wait in a channel. But I'm not sure about the wait part (who will give me a channel?)

2019-07-11T00:22:14.206100Z

a decent pattern is to use one channel, and N threads consuming, running a function that takes two items: something describing the work, and a channel on which to put the result

2019-07-11T00:22:31.206400Z

the N decides your maximum parallelization

2019-07-11T00:22:55.206700Z

you don't need a separate queue - a channel is already a queue

2019-07-11T00:23:59.207500Z

any consumer that wants a result would write a message tuple [params, c], then wait for a result on c

2019-07-11T00:47:06.210100Z

You can also use async/thread or pipeline-blocking for all your io, you don't need to setup your own threadpool

souenzzo 2019-07-11T00:49:56.210500Z

async/thread is nice! I will use it for ASAP

Yehonathan Sharvit 2019-07-11T13:43:08.212900Z

Hi there, A question related to the performances of go processes. I am executing a similar code in thread vs. go processes and it seems that go processes takes more time. Here is my code:

(do
    (let [c (chan)
          _ (put! c 2)
          t (. System (nanoTime))]
      (thread
        (let [val (<!! c)
              elapsed (- (. System (nanoTime)) t)]
          (println (str "taken value, delivery time with thread -- " val " time: " (float  (/ elapsed 1e6)) "ms" "\n")))))

    (let [c (chan)
          _ (put! c 1)
          t (. System (nanoTime))]
      (go (let [val (<! c)
                elapsed (- (. System (nanoTime)) t)]
            (println (str "taken value, delivery time with go: -- " val " time: " (float  (/ elapsed 1e6)) "ms" "\n")))))))
The output is:
taken value, delivery time with thread -- 2 time: 0.228456ms
taken value, delivery time with go: -- 1 time: 1.516011ms
I was expecting go processes to run faster than threads.

mpenet 2019-07-11T13:47:47.213900Z

a go block is nothing more than a "coordination block", in the end it all runs in threads, it's not a lightweight thread per say

mpenet 2019-07-11T13:59:06.216Z

thread has no bookkeeping to do about parking put/take, it just runs the body in a thread, returns a chan with the result and bye.

ghadi 2019-07-11T14:11:11.217100Z

@viebel the benchmark is flawed to the point of not telling you anything. > expecting go processes to run faster than threads. Why?

Yehonathan Sharvit 2019-07-11T14:11:46.217600Z

because go processes are kind og lightweight threads

ghadi 2019-07-11T14:12:18.218300Z

they still run within threads

ghadi 2019-07-11T14:12:34.219300Z

they have to do more work to suspend and unsuspend

mpenet 2019-07-11T14:12:37.219400Z

it's just a coordination primitive that can run blocking stuff (wrapped in chan put/takes) on a fixed threadpool, like in go

Yehonathan Sharvit 2019-07-11T14:12:47.219700Z

Within threads, yes but no need to create a new thread each time

ghadi 2019-07-11T14:12:52.220Z

right

ghadi 2019-07-11T14:12:58.220300Z

you can run more of them than threads

mpenet 2019-07-11T14:13:04.220800Z

if you have 1 blocking task and nothing else, for sure it will be slower

ghadi 2019-07-11T14:13:07.220900Z

you can run 2 million go blocks, easy

ghadi 2019-07-11T14:13:13.221200Z

can't do that with threads

ghadi 2019-07-11T14:13:23.221500Z

but pound-for-pound, go blocks will be slower

ghadi 2019-07-11T14:14:12.222700Z

your benchmark isn't actually doing much work, and it's incorrectly calling blocking I/O (`println`) within the go block

ghadi 2019-07-11T14:14:54.223600Z

blocking that isn't channel operations is not recommended because it can cause deadlocks

ghadi 2019-07-11T14:16:16.224900Z

try some benchmarks that exercise more than 20000 processes

ghadi 2019-07-11T14:17:24.226200Z

and things that have more operations per process: put, take, especially with unbuffered channels where they might not be available to proceed

Yehonathan Sharvit 2019-07-11T15:07:57.227500Z

something is not clear to me: Does a go block creates a new thread or does it use a thread that is already ready to run?

Yehonathan Sharvit 2019-07-11T15:08:30.228200Z

Let’s say I create 10 go blocks sequentially, will the thread be reused or will it be recreated?

markmarkmark 2019-07-11T15:31:30.230100Z

core async dispatches its work on a thread pool that defaults to 8 threads.

markmarkmark 2019-07-11T15:31:52.230600Z

if you created 10 go blocks sequentially it would likely create all 8 threads and would then begin reusing them as needed

alexmiller 2019-07-11T16:18:52.233Z

Pounds are not a unit of speed :)

Yehonathan Sharvit 2019-07-11T17:41:07.233900Z

And between two go blocks execution, the thread from the thread pool is not kept alive. Right?

2019-07-11T17:42:27.234500Z

it's in a waiting state where the vm won't try to execute it between running tasks

2019-07-11T17:42:42.234800Z

it's still allocated, it just doesn't try to run

2019-07-11T17:44:03.236500Z

(it might be more a question of whether the underlying OS tries to wake it up at that point, but regardless the execution dispatcher knows it's not ready to do anything)

Yehonathan Sharvit 2019-07-11T17:45:34.236700Z

Is running a thread from a waiting state cheaper than creating a new thread or is it the same?

2019-07-11T17:45:54.237100Z

it's cheaper, this is why we have thread pools - allocation and initialization aren't free

markmarkmark 2019-07-11T17:47:14.238600Z

the threads in the core async dispatch pool are never destroyed once they've been created

markmarkmark 2019-07-11T17:47:24.238900Z

unless there's an exception

markmarkmark 2019-07-11T17:47:32.239200Z

in which case a new one will be created if it's needed

Yehonathan Sharvit 2019-07-11T17:48:40.241Z

Now I am back to my original question: if go blocks are cheaper to create than thread, why running sequentially go blocks is not faster that running threads sequentially?

2019-07-11T17:49:05.241500Z

because creating the thread isn't the expensive part of running a task on a thread

2019-07-11T17:49:14.241700Z

(or at least it shouldn't be)

2019-07-11T17:50:10.242600Z

what core.async improves is the time it takes to context switch (thanks to reusing small callbacks on a small thread pool), and the correctness of code that coordinates light weight channel ops

2019-07-11T17:51:02.243500Z

but if all you are doing is a sequence of actions (IO or CPU) and you aren't juggling async results, core.async can only match regular perf at best, and likely slows things down

markmarkmark 2019-07-11T17:56:15.245300Z

@viebel in your example that you pasted earlier, what is the thread that is used? the core async thread macro?

Yehonathan Sharvit 2019-07-11T17:57:42.246Z

I have improved my example. Now it’s

(let [iterations 10000]
    [(let [t (. System (nanoTime))]
        (dotimes [_ iterations]
          (let [c (chan)
                _ (put! c 1)]
            (deref (future (<!! c)))))
        (let [elapsed (- (. System (nanoTime)) t)]
          (str (float  (/ elapsed 1e6)) "ms")))

     (let [t (. System (nanoTime))]
       (dotimes [_ iterations]
         (let [c (chan)
               _ (put! c 1)]
           (<!! (go (<! c)))))
       (let [elapsed (- (. System (nanoTime)) t)]
         (str (float  (/ elapsed 1e6)) "ms")))])

Yehonathan Sharvit 2019-07-11T17:57:57.246300Z

It’s go vs future

ghadi 2019-07-11T18:00:05.247800Z

sorry, this is still misconceived

Yehonathan Sharvit 2019-07-11T18:00:06.247900Z

On my MacBook Pro, the result is:

["191.04631ms" "211.95337ms"]

Yehonathan Sharvit 2019-07-11T18:00:13.248100Z

future is faster than go

Yehonathan Sharvit 2019-07-11T18:00:32.248500Z

why is it misconceived @ghadi ?

ghadi 2019-07-11T18:01:30.248900Z

For one, no real world task looks like this

ghadi 2019-07-11T18:01:49.249300Z

deriving conclusions like "x is faster" isn't useful

ghadi 2019-07-11T18:02:34.250Z

none of the channels block on either test, they're already ready to read

ghadi 2019-07-11T18:04:18.251200Z

I don't know what the goal is here

ghadi 2019-07-11T18:04:36.251800Z

the same code in go vs ordinary block is going to be slower

ghadi 2019-07-11T18:05:04.252400Z

it's clear if you macroexpand the go block that there are (small) costs to the go machinery itself

ghadi 2019-07-11T18:05:38.253100Z

But, if a channel blocks in a thread or future, that thread is useless until the channel yields

ghadi 2019-07-11T18:05:45.253300Z

not so with a go block

ghadi 2019-07-11T18:06:02.253800Z

the thread that the go block is running upon will start running some other go block

ghadi 2019-07-11T18:07:22.254400Z

The benchmark also waits for the go block to finish before scheduling another go block

ghadi 2019-07-11T18:07:34.254800Z

which defeats the advantage

Yehonathan Sharvit 2019-07-11T18:57:20.257900Z

@ghadi I understand that the real value of go blocks, is when you run them in parallel. I brought this sequential example in order to help understand how things work

Yehonathan Sharvit 2019-07-11T18:57:48.258400Z

When I say future is faster than go, I don’t mean to “blame” go

Yehonathan Sharvit 2019-07-11T18:58:30.259300Z

I was assuming that creating a thread was an expensive OS operation and that’s why I thought go blocks would be futures even in the sequential case

Yehonathan Sharvit 2019-07-11T18:58:46.259700Z

I still don’t get it 100%

2019-07-11T18:59:03.260100Z

future also uses a thread pool

Yehonathan Sharvit 2019-07-11T18:59:08.260400Z

ah

Yehonathan Sharvit 2019-07-11T19:21:43.260800Z

What about core.async thread? Does it also use a thread pool?

markmarkmark 2019-07-11T19:23:32.261Z

yes

markmarkmark 2019-07-11T19:23:50.261400Z

and the thread pools that future and core.async thread use are actually completely unbounded

markmarkmark 2019-07-11T19:24:53.262300Z

though, I guess them being unbounded doesn't matter as much in your example since you wait for the future before you make the next one

Yehonathan Sharvit 2019-07-11T19:25:11.262600Z

What do you mean by unbounded?

markmarkmark 2019-07-11T19:25:41.263Z

there's no limit to how many threads they can start up

markmarkmark 2019-07-11T19:25:50.263300Z

besides the obvious memory limits and whatnot

Yehonathan Sharvit 2019-07-11T19:26:35.264Z

I added a core.async/thread scenario

(let [iterations 100]
    [(let [t (. System (nanoTime))]
        (dotimes [_ iterations]
          (let [c (chan)
                _ (put! c 1)]
            (deref (future (<!! c)))))
        (let [elapsed (- (. System (nanoTime)) t)]
          (str (float  (/ elapsed 1e6)) "ms")))

     (let [t (. System (nanoTime))]
       (dotimes [_ iterations]
         (let [c (chan)
               _ (put! c 1)]
           (<!! (thread (<! c)))))
       (let [elapsed (- (. System (nanoTime)) t)]
         (str (float  (/ elapsed 1e6)) "ms")))

     (let [t (. System (nanoTime))]
       (dotimes [_ iterations]
         (let [c (chan)
               _ (put! c 1)]
           (<!! (go (<! c)))))
       (let [elapsed (- (. System (nanoTime)) t)]
         (str (float  (/ elapsed 1e6)) "ms")))])

Yehonathan Sharvit 2019-07-11T19:26:52.264200Z

The results are: ["3.231052ms" "20.063465ms" "5.740195ms"]

Yehonathan Sharvit 2019-07-11T19:27:01.264600Z

thread is much slower!

Yehonathan Sharvit 2019-07-11T19:27:38.265400Z

And when I raise the number of iterations to 10,000 the thread code triggers an out of memory exception

robertfw 2019-07-11T23:22:39.265500Z

I thought that the async threadpool by default is a fixed 8 thread pool?

markmarkmark 2019-07-11T23:36:47.265700Z

the threadpool used by the go blocks are by default fixed at 8. the threadpool used by the thread macro is unbounded.