4

I am trying to analyze a photo concurrently using a background thread from GCD. Here is the code I have written:

dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value), 0)) {
    for (var i = 0; i < 8; i++)
    {
        let color = self.photoAnalyzer.analyzeColors(imageStrips[i])
        colorList.append(color)
    }
}

For clarification on the variable names, here are their descriptions:

photoAnalyzer is an instance of a class I wrote called Analyzer that holds all of the methods to process the image.

analyzeColors is a method inside the Analyzer class that does the majority of the analysis and returns a string with the dominant color of the passed in image

imageStrips is an array of UIImage's that make up the portions of the original image

colorList is an array of strings that stores the return values of the analyzeColor method for each portion of the image.

The above code runs sequentially since the for loop only accesses one image from the imageList at a time. What I am trying to do is analyze each image in imageStrips concurrently, but I had no idea how to go about doing that.

Any suggestions would be greatly appreciated. And if you would like to see all of the code to further help me I can post a GitHub link to it.

EDIT This is my updated code to handle 8 processors concurrently.

dispatch_apply(8, imageQueue) { numStrips -> Void in
    let color = self.photoAnalyzer.analyzeColors(imageStrips[numStrips])
    colorList.append(color)
}

However, if I try to use more than 8 the code actually runs slower than it does sequentially.

8
  • I should also mention that I am extremely new to multithreading on iOS. Commented Apr 27, 2015 at 3:53
  • There's answers in this thread already that will answer your question: stackoverflow.com/questions/24170706/… You've set that up to use a concurrent queue, however dispatch_apply is what you're going to need to take advantage of concurrent processing. Commented Apr 27, 2015 at 3:55
  • @JohnRogers That seemed to work at least a little bit. I tried it for an image split in to 20 pieces, but it only analyzed 8 at a time. Is that restriction because of the number of processors available? Commented Apr 27, 2015 at 4:26
  • You're 100% correct! It will perform one operation per thread, in your case 8. Commented Apr 27, 2015 at 4:27
  • Okay, so I am limited to 8 processors? The reason I ask is because an analysis of a 400x400 pixel image took 75 seconds sequentially and 66 seconds on the 8 processors. I was hoping to be able to split the image up into 16, 32, 48, etc. strips and plot the time increase. Commented Apr 27, 2015 at 4:39

2 Answers 2

4

There are a couple of ways of doing this, but there are a couple of observations before we get to that:

  • To try to maximize performance, if you do any concurrent processing, be aware that you are not guaranteed the order in which they will complete. Thus a simple colorList.append(color) pattern won't work if the order that they appear is important. You can either prepopulate a colorList and then have each iteration simply do colorList[i] = color or you could use a dictionary. (Obviously, if order is not important, then this is not critical.)

  • Because these iterations will be running concurrently, you'll need to synchronize your updating of colorList. So do your expensive analyzeColors concurrently on background queue, but use a serial queue for the updating of colorList, to ensure you don't have multiple updates stepping over each other.

  • When doing concurrent processing, there are points of diminishing returns. For example, taking a complex task and breaking it into 2-4 concurrent loops might yield some performance benefit, but if you start increasing the number of concurrent threads too much, you'll find that the overhead of these threads starts to adversely affect performance. So benchmark this with different degrees of concurrency and don't assume that "more threads" is always better.

In terms of how to achieve this, there are two basic techniques:

  1. If you see Performing Loop Iterations Concurrently in the Concurrency Programming Guide: Dispatch Queues guide, they talk about dispatch_apply, which is designed precisely for this purpose, to run for loops concurrently.

    colorList = [Int](count: 8, repeatedValue: 0)  // I don't know what type this `colorList` array is, so initialize this with whatever type makes sense for your app
    
    let queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)
    
    let qos_attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0)
    let syncQueue = dispatch_queue_create("com.domain.app.sync", qos_attr)
    
    dispatch_apply(8, queue) { iteration in
        let color = self.photoAnalyzer.analyzeColors(imageStrips[iteration])
        dispatch_sync(syncQueue) {
            colorList[iteration] = color
            return
        }
    }
    
    // you can use `colorList` here
    

    Note, while these iterations run concurrently, the whole dispatch_apply loop runs synchronously with respect to the queue from which you initiated it. This means that you will not want to call the above code from the main thread (we never want to block the main thread). So will likely want to dispatch this whole thing to some background queue.

    By the way, dispatch_apply is discussed in WWDC 2011 video Blocks and Grand Central Dispatch in Practice.

  2. Another common pattern is to create a dispatch group, dispatch the tasks to a concurrent queue using that group, and specify a dispatch_group_notify to specify what you want to do when it's done.

    colorList = [Int](count: 8, repeatedValue: 0)  // I don't know what type this `colorList` array is, so initialize this with whatever type makes sense for your app
    
    let group = dispatch_group_create()
    let queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)
    
    let qos_attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0)
    let syncQueue = dispatch_queue_create("com.domain.app.sync", qos_attr)
    
    for i in 0 ..< 8 {
        dispatch_group_async(group, queue) {
            let color = self.photoAnalyzer.analyzeColors(imageStrips[i])
            dispatch_sync(syncQueue) {
                colorList[i] = color
                return
            }
        }
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue()) {
        // use `colorList` here
    }
    
    // but not here (because the above code is running asynchronously)
    

    This approach avoids blocking the main thread altogether, though you have to be careful to not add too many concurrent dispatched tasks (as the worker threads are a very limited resource).

In both of these examples, I created a dedicated serial queue for synchronizing the updates to colorList. That may be overkill. If you're not blocking the main queue (which you shouldn't do anyway), you could dispatch this synchronization code to the main queue (which is a serial queue). But it's probably more precise to have a dedicated serial queue for this purpose. And if this was something that I was going to be interacting with from multiple threads constantly, I'd use a reader-writer pattern. But this is probably good enough for this situation.

Sign up to request clarification or add additional context in comments.

12 Comments

Wow, very thorough answer thank you. All the app does is allow a user to select a photo, press an "Analyze" button and when the photo is finished analyzing an Alert appears telling the user what the dominant color is. From my understanding, keeping the main queue clear is to avoid forcing the user to sit and wait while operations are performing, but since this app literally just performs one task I don't think doing what you did would be overkill.
Also, the colorList just stores a string for the dominant color of that particular sub-image. So as long as the sub-image is processed correctly (which my method does) then all I have to do is count the number of times each color appears in the colorList and thats the dominant color of the whole image.
I was guessing you were trying to do something like that, but had no way of knowing what type colorList was. Clearly, you can just replace my [Int](count: 8, repeatedValue: 0) with [String](count: 8, repeatedValue: "") (or whatever, and the rest is pretty much the same).
As you mentioned, there is a point of diminishing return. I am analyzing a 400x400 pixel photo and it takes over a minute to do so. Obviously I cannot just keep splitting the photo up and increasing the loop (once again, as you mentioned) but is there any other way to increase the speed at which this photo is analyzed? My only thought was to analyze every other pixel.
A 400x400 image is not extraordinarily large, so 1 min sounds like a lot. Instruments' Time Profiler might help you diagnose what the source of the issue is. See WWDC 2012 video Designing Concurrent User Interfaces. It's a bit dated, but the basic techniques still apply. I wonder if the vImage routines might be helpful, too. See the Histogram discussion.
|
-1
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value), 0)) {
    for (var i = 0; i < 8; i++)
    {
 dispatch_async(dispatch_get_main_queue(), ^(){
    //Add method, task you want perform on mainQueue
    //Control UIView, IBOutlet all here
        let color = self.photoAnalyzer.analyzeColors(imageStrips[i])
        colorList.append(color)
    });
    }
}

1 Comment

Code-only answers fall under 'Very Low Quality'. So please add how this code works.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.