5

I'm using server.socket to stream data to multiple clients, server.socket uses Threads for each client connection. I currently have something like this:

(def clients (atom ())) ; connected clients defined globally for that namespace

(swap! clients conj a)  ; adds a client (which is an atom itself as well), this is in a function that is run on the client's thread

;I want to better the process of removing a client!
(dosync (reset! clients (remove #{a} @clients))) ; removes client from list, run in a function on the client's thread

I run a function which runs through each client and grabs the content, it is in an infinite loop on each of the multiple client threads, so it's run simultaneously:

(doseq [c @clients]
  (print ((deref c) :content))
  (flush))

I sort of came to the conclusion using Atoms in the threads really makes the program work smoothly and allows non-blocking Reads, so I'm happy with this except I feel that resetting the global client's Atom just so I can remove a single client from the list is a bad move. Is there a more appropriate way to accomplish this using swap! ? I chose list for the clients atom as I'm running doseq on each connected client to grab the content and flush it to the output stream socket.

scape
  • 652
  • 12
  • 27

2 Answers2

9

Avoid deref-ing the atom within a swap! or reset!.

Here swap! is going to give you what you need. It takes a function that receives the current value, which you can use for your update:

(def clients (atom '(:a :b :c :d)))
(swap! clients (fn [s] (remove #{:a} s)))

You may be used to not seeing the function argument of swap! as explicitly as above because swap! will apply the function to any additional args provided, so if they are in the correct order, e.g. if we used set for clients, we can

(def clients (atom #{:a :b :c :d}))
(swap! clients disj :a)
A. Webb
  • 26,227
  • 1
  • 63
  • 95
3

Sure, you can use swap!, see A. Webb's answer.

You might want to consider whether it's the best choice to store you clients in a list; a set or a map would be a more natural choice (for use with disj / dissoc). (Unless there is always a very small number of clients, in which case it may make sense to use the least complicated data structure available.)

Also, the dosync does nothing here. dosync is for use with Refs (and alter, commute, ref-set, ensure).

I'll also point out that if you run a loop like this:

(doseq [c @clients]
   ...)

then it'll always loop across the value of clients at the time the doseq form was entered, regardless of any swap!s to the clients Atom which might have occurred in the meantime. Not that it's likely to be a problem, just something to keep in mind.

Another thing to keep in mind is that Clojure's reference types are designed to (1) hold immutable data, (2) be updated with pure functions (in swap! / alter / send and friends). Putting Atoms in Atoms breaks (1); it may not be a problem here, but it's your responsibility to make sure it isn't. (Breaking (2) would be a problem pretty much always, except in special cases like debug traces which you totally want to be printed again on failed CAS etc.)

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
Michał Marczyk
  • 83,634
  • 13
  • 201
  • 212
  • Thanks for your input, I was hoping dosync would help while referencing the client atom. I see what you're saying after reading the function more closely. I chose List as I continuously run a doseq on it from multiple threads, seems natural instead of creating a seq out of a vector or map. I feel like I read somewhere that doseq on anything but a list takes a small performance hit. – scape Jul 19 '13 at 13:07
  • 2
    Actually a `doseq` on a vector is particularly fast because of chunking. On a set or map -- might be slower, sure, but it's not a big deal (your `dosync` body will surely dominate if it does any work at all). And `disj` from a set will be a lot faster than `remove` from a long list. – Michał Marczyk Jul 19 '13 at 13:09
  • I understand, I'm find with getting a 'snapshot' during the doseq stage, I run the loop around doseq continuously so each time it's run it should reference new values, which it does. – scape Jul 19 '13 at 13:10
  • How does atoms in atoms break immutability? I appreciate your input, as I'm a novice. Basically I have 1 atom that holds all client atoms. The main atom called Clients is only updated when a new client joins or disconnects. Each client themselves updates their own atom with updated information. To me this made sense for what I was trying to accomplish-- share all data amongst everyone associated. – scape Jul 19 '13 at 13:21
  • 2
    @scape Atoms are mutable themselves, but their content is meant to be immutable. An atom within an atom means the outer atom would have content which is mutable, i.e. the inner atom. This would defeat the point -- atoms are intended to provide controlled access to state, but in this case the state of the outer atom can be changed by mutating the inner atom without consulting the outer. The second problem is that fact that `swap!` etc may be issued multiple times behind the scenes, so should be free of side effects. An example of a side effect is changing state. Risk of very subtle bugs. – A. Webb Jul 19 '13 at 13:31
  • So instead you may want to use a top-level Atom holding a map of client id to client data; each client would then handle its own data (perhaps using `swap! update-in [:my-id ...] ...`) and addition / removal of clients could be accomplished using `swap! assoc` / `dissoc`. You don't have to give clients access to the top-level Atom for this to work either; instead, they can provide a coordinator of some sort with appropriate functions, which that coordinator can then apply to the appropriate fragments of the top-level map in the fashion described above. – Michał Marczyk Jul 19 '13 at 14:20
  • In my experience, as the complexity of the code grows, a plain old exclusive lock gives you a much simpler concurrent model overall. You can rake and rake your head with all the possible interactions between all the possible atomic updates, or you can introduce just one simple and banal *lock*. – Marko Topolnik Jul 19 '13 at 20:49