/** * CSC543DemoImageExplicitLock modified from CSC543DemoImageImplicitLock 2/13/2023 * CSC543DemoImageImplicitLock modified from CSC543DemoImagePipeLineQueues 2/13/2023 * CSC543DemoImagePipeLineQueues drafted 1/30/2023 to show Queue and other synchronization * and communication between a worker thread and a non-thread-safe graphical display thread. * D. Parson **/ import java.util.* ; // https://docs.oracle.com/javase/8/docs/api/index.html?java/util/List.html import java.util.concurrent.* ; import java.io.* ; import java.util.concurrent.locks.ReentrantLock ; import java.util.concurrent.locks.Condition ; // This outer class is @NotThreadSafe because the Processing GUI data structures are // not all guarded or otherwise protected by unsynchronized concurrent access. // https://docs.oracle.com/javase/8/docs/api/index.html?java/util/concurrent/package-summary.html // THESE DATA ARE THREAD-SAFE. nonblocking is IMMUTABLE. final boolean nonblocking = false ; // for nonblocking queues tyhread terminates when all images in queue // final Queue imageQueue = new ConcurrentLinkedQueue(); // final BlockingQueue imageQueue = new LinkedBlockingQueue(10); // THESE DATA ARE NON-THREAD-SAFE and used only inside the display() thread. boolean isTimeToRead = true ; // not accessed by FileReaderActiveClass thread PImage currentImage = null ; // not accessed by FileReaderActiveClass thread FileReaderActiveClass workerBehavior ; // variable not accessed by FileReaderActiveClass thread Thread workerThread ; // variable not accessed by FileReaderActiveClass thread void setup() { // Processing's startup function, called once size(1920, 1080); frameRate(60); println("setup()'s thread is " + Thread.currentThread()); try { workerBehavior = new FileReaderActiveClass(); workerThread = new Thread(workerBehavior); workerThread.setPriority(Thread.MAX_PRIORITY); // try to avoid delays workerThread.start(); } catch (FileNotFoundException fx) { println("Photo list text file not found: " + fx.getMessage()); exit(); } } boolean printme = true ; void draw() { if (printme) { println("draw()'s thread is " + Thread.currentThread()); printme = false ; } // Invoked at frameRate background(0); // black imageMode(CENTER); // reference point for image plotting location if (isTimeToRead) { PImageContainer tmpimg = workerBehavior.poll(); // Do not EVER block a GUI doisplay thread!!! if (tmpimg != null) { currentImage = tmpimg.getImage() ; isTimeToRead = false ; } } if (currentImage != null) { image(currentImage, width/2, height/2); // display centered in display window } } void keyPressed() { // Called an non-draw() time when key press event arrives if (key == 'n') { // 'n' for nect image isTimeToRead = true ; } } @Immutable class PImageContainer { // PImageContainer is here just to illustrate an @Immutable class. // I have used such "dumb data containers" to hold multiple objects // of the same or differing types such as PShape for a vector graphic. // Note how it is necessary to return a copy of the mutable PImage object // on getImage() in order to maintain effective immutability of this // object. private final PImage img ; // init final fields here or in constructor, NOT both public PImageContainer(PImage img) { this.img = img ; // parameter is unqualified "img" } public PImage getImage() { return img.copy() ; } } @ThreadSafe class FileReaderActiveClass implements java.lang.Runnable { /* CSC543 STUDENTS: Why is this class thread-safe? 1. All of its data fields are private and will not leak out of this class unless published by a non-private method (member function). 2. Any field that can be declared final is so declared. This makes that field immutable & also guarantees flush to main memory before accesses to objects of this class. It may point to mutable objects; filenames does, but that mutable object is thread-safe. 3. Non-final fields are declared volatile because the constructor is called from setup() and runs in a thread different from the run() thread explicitly started in setup(). 4. We could have non-final and non-mutable data fields be thread-safe IF every access to them is @GuardedBy an explicit lock (e.g., ReentrantLock) or a synchronized section on a common object (@GuardedBy that object). */ // FEB 13 polledPimageContainer is NOT safe so we guard access with an explicit lock private final ReentrantLock accessLock = new ReentrantLock(); private final Condition accessSignaler = accessLock.newCondition(); @GuardedBy("accessLock") private PImageContainer polledPimageContainer ; private final List filenames ; // MOVED TO IN-THREAD CONFINEMENT private volatile int fileindex = -1 ; private final String sp = sketchPath(""); public FileReaderActiveClass() throws FileNotFoundException { try { accessLock.lock(); // uninterruptible polledPimageContainer = null ; } finally { // STUDENTS MUST ALWAYS USED FINALLY TO RELEASE A LOCK, NO MATTER WHAT HAPPENS INSIDE ^^^ accessLock.unlock(); } Scanner fileNameReader = new Scanner(new File(sp + "/photolist.txt")); int filecount = 0 ; LinkedList notThreadSafeLocal = new LinkedList(); // LinkedList avoids CopyOnWriteArrayList overhead but is not thread safe, thread confined while (fileNameReader.hasNextLine()) { String fname = fileNameReader.nextLine().trim(); if (fname.length() > 0 && ! (fname.startsWith("#") || fname.startsWith("//"))) { notThreadSafeLocal.add(fname); filecount++ ; } } fileNameReader.close(); // if (notThreadSafeLocal.size() > 0) { if (filecount == 0) { // fileindex = 0 ; throw new FileNotFoundException("ERROR, No image files found in " + sp + "/photolist.txt"); } filenames = new CopyOnWriteArrayList(notThreadSafeLocal); // avoid mutate overhead } PImageContainer poll() { // Called from client, thread-safe lock. accessLock.lock(); try { PImageContainer result = null ; if (polledPimageContainer != null) { result = polledPimageContainer ; polledPimageContainer = null ; accessSignaler.signalAll(); // wake up the run thread if it is waiting } return result ; } finally { // STUDENTS MUST ALWAYS USED FINALLY TO RELEASE A LOCK, NO MATTER WHAT HAPPENS INSIDE ^^^ // Even the return statement goes through here. accessLock.unlock(); } } public void run() { int fileindex = 0 ; // moved here from object field for thread confinement final float screenAspect = float(width) / float(height); println("FileReaderActiveClass.run()'s thread is " + Thread.currentThread()); while (true) { try { String nm = filenames.get(fileindex); //println("DEBUG ABOUT TO READ", nm); PImage thing = loadImage(sp + "/" + nm) ; float thingAspect = float(thing.width) / float(thing.height); if (screenAspect > thingAspect) { float multiplier = float(height) / float(thing.height); thing.resize(round(thing.width*multiplier),height); } else if (screenAspect < thingAspect) { float multiplier = float(width) / float(thing.width); thing.resize(width, round(thing.height*multiplier)); } else { thing.resize(width, height); // DEBUG } PImageContainer carrier = new PImageContainer(thing); accessLock.lock(); try { while (polledPimageContainer != null) { try { accessSignaler.await(); // block on this object field until it becomes null } catch (InterruptedException iex) { // Just loop back up & keep waiting. Interrupts not used in this sketch. } } polledPimageContainer = carrier ; } finally { // STUDENTS MUST USE FINALLY TO RELEASE A LOCK, NO MATTER WHAT HAPPENS INSIDE ^^^ accessLock.unlock(); } // PGraphics pg = createGraphics(width, height); // Uses display thread, blows up!!! // pg.background(0); fileindex = (fileindex+1) % filenames.size() ; // println("DEBUG THREAD RECVD ", nm); // } catch (InterruptedException iex) { // println("InterruptedException: " + iex.getMessage()); } catch (Exception xxx) { println("Image File Reading Exception on '" + filenames.get(fileindex) + "': " + xxx.getMessage()); exit(); fileindex = (fileindex+1) % filenames.size() ; } } } }