Toy Factory Simulation Using core.async (2)

Hiring(Increasing Concurrency)

Guowei

3 minute read

In Part 1 we have already defined the process of building toy cars, by one worker. Here is the code again for easy reference.

(defn build-car [n]
  (prn n "Starting build")
  (let [body (loop []
               (let [part (take-part)]
                 (if (body? part)
                   part
                   (recur))))
        _ (prn n "Got body")
        wheel1 (loop []
                 (let [part (take-part)]
                   (if (wheel? part)
                     part
                     (recur))))
        _ (prn n "Got wheel1")
        wheel2 (loop []
                 (let [part (take-part)]
                   (if (wheel? part)
                     part
                     (recur))))
        _ (prn n "Got wheel2")
        bw (attach-wheel body wheel1)
        _ (prn n "Attach wheel1")
        bww (attach-wheel bw wheel2)
        _ (prn n "Attach wheel2")
        box (box-up bww)
        _ (prn n "Box up")]
    (put-in-truck box)
    (prn "Done!")))

If the goal is to build 10 toy cars and put them in the truck, can we speed things up by simply hiring 10 workers to work all at the same time.

We know it takes around 16 secs for 1 worker to build 1 car, that means 160 secs for 10 cars. If we put 10 workers to work at the same time, the total time would be again 16 secs. Let’s test this out.

To simulate workers working simultaneously, we use the go block from core.async. Each go block will launch a separate process to run what’s inside it.

(defn start-ten []
  (dotimes [x 10]
    (go
      (time
       (build-car x)))))

The above code simply runs build-car on 10 processes simultaneously and time them.

"Elapsed time: 72652.796992 msecs"
9 "Attach wheel2"
"Done!"
"Elapsed time: 74654.849449 msecs"
9 "Box up"
"Done!"
"Elapsed time: 26313.086461 msecs"

As we can see, the result is better than 160 secs for sure, but not nearly as good as 16 secs we were hoping for. The last worker to finish is like 74 secs.

Emm, to understand what is going on, let’s take things apart.

What if we only run 3 take-part simultaneously?

(defn start-three []
  (dotimes [x 3]
    (go
      (time (prn (take-part))))))
lispcast-clojure-core-async.core> (start-three)
nil:wheel
"Elapsed time: 1000.964435 msecs"
#atom[{:body [], :status :free} 0x50915a28]
"Elapsed time: 1902.513706 msecs"
#atom[{:body [], :status :free} 0x42cb6f0b]
"Elapsed time: 2802.817667 msecs"

Seems that take-part can only be run one at a time. This actually makes sense, since all workers are taking parts from one box, so the box is like a bottle neck. The implementation of take-part also tells about this already (check Part 1).

We can do the same analysis on attach-wheel, box-up and put-in-truck.

The result shows that attach-wheel and box-up can be run simultaneously, but put-in-truck has to be run one by one.

This explains why by simply putting more workers to work at the same time might not be a good idea, because they are competing over the shared resources: parts box and the truck.

comments powered by Disqus