2
\$\begingroup\$

Aside: This code assumes a class Option that can be Some or None to represent the presence (Some) or absence (None) of the value it contains. Haskell calls it Maybe.

Here is my first pass at wrapping Iterator in a thread-safe way:

public synchronized Option<T> nextBlocking() {
    return iter.hasNext() ? Option.of(iter.next())
                          : Option.none();
}

I don't like that it's synchronized - it requires locking in order to work across multiple threads. On the other hand, if we remove the word synchronized, one thread could call iter.hasNext() which returns true (e.g. for the last item in the Iterator), then a second thread calls iter.next() before the first thread does, using up that last item and causing the first thread to get a NoSuchElementException when it finally calls iter.next().

The following isn't going to win any object-oriented style points, but I think it mostly works around this issue in a non-blocking way:

public Option<T> nextNonBlocking() {
    try {
         return Option.of(iter.next());
    } catch (NoSuchElementException nsee) {
        if(iter.hasNext()) {
            // Oops, exception was thrown for reason other than end-of-iterator.
            // Rethrow that exception.
            throw nsee;
        }
    }
    return Option.none();
}

That could still fail if a nsee is thrown for some reason before end-of-iterator, then another thread reaches the true end-of-iterator before the first thread calls iter.hasNext(). While possible, this situation probably represents a coding failure in the underlying Iterable. It's very tempting to trade this tiny weakness for a non-blocking iterator wrapper.

\$\endgroup\$
4
\$\begingroup\$

Looking at your first code example, it's somewhat sensible. The synchronized method creates a "happens before" memory barrier, and as a result, all threads see the same version of the data at that point in the code.

There is a vulnerability in that code, though. The synchronized method means that your lock is publicly accessible. Anyone can synchronize on your wrapper, and create a conflicting lock, perhaps creating a "denial of service" situation, where you can never get in to your method because some other thread has your instance locked. This is rare, but it does happen in some programs.

About your second block of code.... this is just plain broken from a multi-threaded perspective:

public Option<T> nextNonBlocking() {
    try {
         return Option.of(iter.next());
    } catch (NoSuchElementException nsee) {
        if(iter.hasNext()) {
            // Oops, exception was thrown for reason other than end-of-iterator.
            // Rethrow that exception.
            throw nsee;
        }
    }
    return Option.none();
}

That code will seldom produce what you expect in an actual multi-threaded situation. For a start, it reverses the logic of the hasNext() and next() calls. You should call hasNext first. Secondly, in an actual multi-core, especially in a multi-memory-controller computer (multi-socket), you will run in to stale memory accesses. It is possible for all threads to iterate through all members of the iterator, as they will all be seeing different "caches" of the iterator.

The second block is just "plain wrong".

As an aside, Java 8 now has an Optional class. You should use it.

\$\endgroup\$
  • \$\begingroup\$ Optional treats null as the absence of a value. Sometimes that's OK, but I generally need to treat null as a present value. I need to think about your other advice more. A lot more. Is there a book that talks about stale memory access and multiple caches of the same data that you would recommend? \$\endgroup\$ – GlenPeterson May 19 '15 at 12:18
  • \$\begingroup\$ The reference book for these types of things is "Java Concurrency in Practice" - It is still relevant, but is in need for a refresh.... I recommend borrowing a copy. \$\endgroup\$ – rolfl May 19 '15 at 12:20
  • \$\begingroup\$ About the 'null as the absence', what if the iterator returns a null value (a null in an ArrayList, for example...)? Optional should not treat null as a trigger for specific behaviour (and the Java one does not). \$\endgroup\$ – rolfl May 19 '15 at 12:24
  • \$\begingroup\$ Your comments are very helpful - thank you. You would put a synchronized block inside the nextBlocking() method that locked on the iterator, not on the containing object? \$\endgroup\$ – GlenPeterson May 19 '15 at 12:31
  • \$\begingroup\$ @GlenPeterson - Depending on the visibility of the wrapped Iterator, yes (i.e. if the iterator was created inside the wrapper, then yes, otherwise I would have a dedicated instance private final Object lock = new Object(); \$\endgroup\$ – rolfl May 19 '15 at 12:32
1
\$\begingroup\$

The whole idea of wrapping an iterator to be thread-safe seems a bit dubious to me. If this is supposed to be a (well-performing) producer-consumer thing, then you should model it as such, with a queue and all that. Otherwise you should try to avoid multithreading the iterator entirely.

If you cannot do that, at least use nextBlocking(), which does not require a link to this discussion in the code to be maintainable.

\$\endgroup\$
0
\$\begingroup\$

I just did some timings. When the methods process different underlying iterators, the blocking method works 4 times faster. Under heavy machine load, it works about 4.5 times faster.

I wrote a second speed test that had the two methods are fighting for access to the same underlying iterator. In that case, they process roughly the same number of items.

One time that I ran the second test, nextBlocking() threw a NoSuchElementException. Presumably for the reason that I outlined above. So if you are wrapping a shared iterator, you have to check for that exception. If you get to wrap your own new, private iterator from an Iterable, you can use the nextBlocking() method as is.

So I think that nextBlocking() is the clear winner if you can wrap a private iterator. nextNonBlocking() may be better if you can't. I think I just answered my own question, but since I went to the trouble of writing it up, I might as well post it and see what other ideas people may have about wrapping iterators.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.