📜 ⬆️ ⬇️

Concurrency in Swift 3 and 4. Operation and OperationQueue




If you want to achieve the UI responsiveness of your iOS application by executing such time-consuming pieces of code as downloading data from the network or image processing, then you need to use advanced patterns related to multithreading ( oncurrency ), otherwise the work of your user interface ( U I) it starts to slow down a lot and can even lead to its complete “freezing”. You need to remove resource-expensive tasks with main thread (main thread), which is responsible for executing code that displays your user interface ( UI ).

In the current version of Swift 3 and the nearest Swift 4 (autumn 2017), this can be done in two ways that are not yet related to Swift built-in language constructs, which will only be implemented in Swift 5 (end of 2018).
')
One of them uses the GCD (Grand Central Dispatch) and the previous article is devoted to it. In this article, we will show how to achieve responsiveness to UI in iOS applications using such abstract concepts as Operation Operation and the Operation operation queue. We will also show the difference between these two approaches and which of them is better to use in what situations.

The code for this article can be viewed on Github .

What is Operation ? A good definition of Operation given in NSHipster:
Operation is a complete task and is an abstract class that provides you with a flow-safe structure to simulate the status of an operation, its priority, dependencies on other Operations and control this operation.

Basic concepts. Operation


The simplest Operation Operation can be represented by a normal closure, which can also be executed on a DispatchQueue . But this form of the operation can only be applied provided that you add it to the OperationQueue using the addOperation method:


A full Operation Operation can be constructed using a BlockOperation initializer. It is launched with its own start () method:


If you want to get something reusable like an asynchronous version of a synchronous function, you need to create a custom subclass class Operation and get its instance:


The FilterOperation operation of obtaining a blurred image using the appropriate filter is defined as a user-defined subclass of the Operation class. You can see that a custom class can have both input and output properties, as well as other auxiliary functions. To accommodate the functional part of the operation, we overridden ( override ) the main () method.

The Operation class allows you to create some task that you can run on the OperationQueue queue in the future, but for now it can wait for other Operations .

Operation has a state mashine , which is the “life cycle” of Operation Operation :



Possible states of Operation : pending (pending), ready (ready to execute), executing (executed), finished (finished), and cancelled (destroyed).

When you create an Operation and place it on an OperationQueue , you set the operation to pending . After some time, it takes a ready state (ready for execution), and at any time can be sent to OperationQueue for execution, switching to the executing state, which can last from milliseconds to several minutes or longer. After completion, Operation Operation enters the final state of finished . At any point in this simple “life cycle”, Operation Operation can be destroyed and will transition to the cancelled state.

API Operation class API reflects this “life cycle” of the operation and is presented below:


We can start Operation Operation for execution using the start() method, but most often we will add an operation to the OperationQueue operation queue, and this queue will automatically start the operation. It should be remembered that a separate Operation Operation , launched using start() , is performed SYNCHRONOUSLY on the current thread. In order to run it outside the current thread, you need to use either OperationQueue or DispatchQueue .

The current state of Operation Operation at any point of the application can be monitored using Boolean properties: isReady , isExecuting , isFinished , isCancelled using KVO ( key-value observation ) mechanisms, since the operation itself can be performed on any stream, and we may most likely need information on the main thread ( main thread ) or on any other thread other than the one on which the operation itself is performed.

If we want to add functionality to Operation , we need to create a subclass Operation . In the simplest case, in this subclass we need to override the main() method of the Operation class. The Operation class itself automatically manages the change in the status of the operation, but in more complex cases presented below, we will have to do it manually.

We can provide the operation with a completion closure of the completionBlock , which is performed after the operation is completed, as well as with the “quality of service” qualityOfService , which affects the priority of the operation to be performed on the OperationQueue .

As we can see, the Operation class has a cancel() method; however, using this method only sets the isCancelled property to true , and what semantically means “deleting” an operation can only be defined when creating a subclass Operation . For example, in the case of downloading data from a network, cancel() can be defined as disabling an operation from a network interaction.

Basic concepts. OperationQueue


Instead of starting operations ourselves, we will manage them using the OperationQueue operation queue. The queue of OperationQueue operations can be viewed as a high-priority “wrapper» of DispatchQueue , endowed with additional functionality: the ability to destroy operations performed, perform dependent operations, etc.

Let's look at the API of the OperationQueue class:



Here we see the simplest initializer of the OperationQueue () operation queue and two class properties: current and main , which specify the current OperationQueue.current and main queue operations — OperationQueue.main , which is used to update the user interface ( UI ) similar to DispatchQueue.main in GCD . A very important property maxConcurrentOperationCount sets the number of simultaneous operations on this queue and, setting it equal to 1 , we set the serial ( serial ) queue of operations.


By default, the value of the maxConcurrentOperationCount property maxConcurrentOperationCount set to Default , which means the maximum possible number of simultaneous operations:



You can directly add an OperationQueue operation (or any of its subclass ), a closure, or a whole array of operations with the ability to block the current thread until the full array of operations is completed.

The OperationQueue operation queue performs the operations placed on it according to their qualityOfService priority, “readiness” (the isReady property isReady set to true ) and dependencies ( dependencies ) on other operations. If all of these characteristics are equal, then the operations are sent to "execute" in the order in which they were queued. If an operation is placed in a queue of operations, then it cannot be placed again in any of these queues. If an operation has been performed, it cannot be re-executed on any of the operation queues, the operation is a one-time thing, so it makes sense to create subclasses of the Operation class and use them, if necessary, to re-receive an instance of this operation.

You can send a cancel() message to all operations in the queue using the cancellAllOperations () method, for example, if the application “goes” to the background ( background ) mode. Using the waitUntilAllOperationsAreFinished() method, you can block the current thread until all operations on this operation queue have been completed. But NEVER do this on the main queue . If you really need to do something only after the completion of all operations, then create a private serial queue and wait for the completion of your operations there.

The OperationQueue operation queue behaves like a DispatchGroup . You can add operations with different qualityOfService to the OperationQueue , and they will be launched according to their priority. You can also set qualityOfService at a higher level — for the operation queue as a whole, but this value will be overridden by the qualityOfService value for a particular operation.

The default qualityOfService for OperationQueue is .background .

You can also stop the execution of operations on the OperationQueue by setting the isSuspended property to true . The operations performed on this queue will continue, but the newly added ones will not be sent for execution until you change the isSuspended property to false . The default value for the isSuspended property is false .

Let's do some experiments with the Operation operations and the Operation operation queue on the Playground .

Experiment 1. Creating an OperationQueue and Adding Closures


The code can be viewed on the OperationQueue.playground on Github .

Create an empty printerQueue :


Add operations in the form of closures to the printerQueue :


Operations start asynchronously with respect to the current thread, as soon as we add them to printerQueue , and they are in the ready state. We estimate the execution time of all operations using the waitUntilAllOperationsAreFinished() method, which, synchronously with respect to the current thread, waits for the end of operations. On the main queue in the application it is better not to do this, but on our Playground nothing happens to U I and we can afford it. The total execution time of all 7 operations is slightly more than 2 seconds and corresponds to the execution time of the sleep(2) operator, therefore, printerQueue runs all 7 operations simultaneously on many threads.

Let's change the printerQueue queue printerQueue , and set the maxConcurrentOperationCount property to 2 :



We see that it takes a very short time to put all operations on the printerQueue , and a little more than 8 seconds to perform all operations, since they start in pairs and the last 7th operation starts alone in the fourth “pair”.

Now add another concatenationOperation operation with an increased qualityOfService equal to .userInitiated :



We see that at the first opportunity an operation with a higher priority is performed before the others, while the total time for performing all operations practically does not change - just over 8 seconds.

Let's turn the printerQueue to serial ( serial ) by setting the maxConcurrentOperationCount property to 1 :



We see that in this case, operations are performed sequentially, one after another, priorities do not work and the total time for performing all operations increases to 16 seconds.

Consider a more complicated case when, to obtain an array of filtered source images



A filtering operation FilterOperation , familiar to us from the previous section, is applied:



Create a filterQueue for performing filtering operations and a serial ( serial ) appendQueue for appendQueue -safely adding the filtered image to the array. The fact is that many filtering operations will simultaneously access a shared ( shared ) resource — an filteredImages array — to add their own element. Remember? We used a serial ( serial ) DispatchQueue queue to change shared resources? Here is the same, but will be performed for Operation . Of course, the serial ( serial ) DispatchQueue queue will be more efficient, but we will show the creation and use of the serial ( serial ) OperatioQueue queue.

Swift arrays are value type and are copied when writing ( copy on write ), so you do not have to worry about multi-threaded changes, but there are messages in the forums that there are problems with this, especially if the array is a property of an object of type class. Therefore, we show a way to preserve the multithreaded security ( thread safe ) of an array when adding new elements to it in different threads.



Then we create a filtering operation for each image and add it to the filterQueue for asynchronous execution. After the filtering is done, we add the resulting image to the filteredImages array and do this in the completionBlock , where we use another queue of operations, appendQueue . The completionBlock no input parameters and it returns nothing.

We are waiting for all filtering operations and check the filtered image array:



The execution time of all operations of 1.19 seconds is comparable to the execution time of one operation (see previous section), that is, there is a multi-threaded execution of operations on the filterQueue queue. On the Playground we see an array of filtered images, but they are very small to see the filtering effect. We can click on the quick view button and see the filtering effect:



Asynchronous operation


The code can be viewed on the AsyncOperations.playground on Github .

So far, we have used operations for SYNCHRONIC tasks, that is, functions that use the current thread and do not return until they have completed their task entirely. ASYNCHRONIC tasks (functions) behave quite differently: they immediately return control on the current thread, and perform their task on another thread, and let you know that the task is completed, causing the completionHandler to close on another thread. A classic example is URLSession :



We can “wrap” the URLSession functionality in Operation Operation , but we will have to manually manage the operation states.

To create custom operations for SYNCHRONOUS functions, we only needed to override the method of the main() operation. If we do the same with the ASYNCHRONIC function, then in main() it will immediately return control to the current thread and “leave” to work on another thread, and we will end up at the end of the main() method and OperationQueue immediately “throw” our operation out of the queue, without completing our ASYNCHRONOUS function. This is the logic of OperationQueue .

ASYNCHRONOUS OPERATION has a completely different logic of operation.



If the operation is “ready” ( isReady = true ), the OperationQueue operation queue calls the start() method, in which we must set the operation to the “running” state ( isExecuting = true ) and call the main() method, which in turn will call the ASYNCHRONOUS function . The ASYNCHRONIC function performs something on an OTHER stream, but the isExecuting property must remain true , even if it does not perform anything on the CURRENT stream, but only represents a task that is running on another thread. When the ASYNCHRONOUS function calls completionHandler , which indicates the end of the ASYNCHRONOUS function, we must set the completionHandler property to isFinished , true , and the isExecuting property to false .

Therefore, for an ASYNCHRONOUS operation, we will have to override ( override ) more than just the main() method. We need to override the following methods and properties:



Let's create an abstract custom class AsyncOperaton , inherited from Operation and suitable for performing any ASYNCHRONOUS operation. Its abstract nature lies in the fact that it will not have a main() method to perform an asynchronous operation. Here is his diagram:



If you use the ASYNCHRONOUS operation yourself, without an OperationQueue , then you need to override the isAsynchronous property and return true . We need to override the start() method to actually start the ASYNCHRONOUS function and keep the isExecuting property isExecuting to true . We also need to learn how to manage the properties that determine the status of the operation: isReady , isExecuting , isFinished . These properties use the OperationQueue operation queue to track the status of operations and organize the execution of dependent operations.

When you define dependencies between operations, this means that one operation must end before another operation begins, so it is very important to know when the operation ends an OperationQueue when the operation ends. For a SYNCHRONOUS operation, this is not a problem, because the SYNCHRONOUS operation ends when the SYNCHRONOUS function ends. But the ASYNCHRONIC function ends outside the current thread, so we need some way to tell the OperationQueue operation queue about the actual end of the ASYNCHRONOUS function. If we recall GCD , then when adding an ASYNCHRONOUS function to a group, we clearly marked the beginning and end of the ASYNCHRONOUS function using the enter() and leave() methods. But in the case of Operation Operation situation is much more complicated, since the operation has states: isReady , isExecuting , isFinished , isCancelled , etc .. When adding an ASYNCHRONOUS function to Operation , we must manage these states manually. In order to facilitate this work, we have created a special abstract user subclass of the Operation class with an AsyncOperaton , whose main task is to control the change of the operation state. For your own ASYNCHRONOUS function, you will create a subclass class, defining there only main () , from which you will call your ASYNCHRONOUS function. And this new operation will be added to OperatonQueue .

But the problem is that I cannot write, for example, isExecuting = true , because all the properties associated with the state of the operation: isReady , isExecuting , isFinished , are readonly ( {get} ), and we cannot set them directly . We can only do something that will cause the isExecuting property to return true , while informing the system that the status of the AsyncOperaton operation AsyncOperaton changed.

The Operation class uses the KVO ( key-value observation ) mechanism and the willChangeValueForKe and didChangeValueForKey methods and notifications about changes in state properties like isReady , isExecuting , isFinished .

Therefore, for the convenience of wealth management operations, we will create a new data type - proper enumeration enum Stateto represent the state of the asynchronous operation its own variants: ready, executing, finished.


The enumeration Statealso contains a fileprivateproperty with a name keyPath, which we will use as a switch for KVO notifications. The property keyPathis computed and is made up of a string "is"connected to rawValue, which is the name of the enumeration element Statewith a capital letter.

Then we define a type AsyncOperatonvariable in the abstract class to represent the current state of the operation; by default, this value is . Every time we change a variable, we need to switch KVO notifications. We will do this with the help of Observers and properties :var stateStatereadystatewillSet {}didSet {}state


Before switching the operation state state, for example, from executingto finished, we need to send KVO notifications willChangeValueabout the upcoming change of both operations: the new state newValue( finished) and the current state state( executing). After the state switch statehappens, we send KVO notifications didChangeValuefor keyPathboth states: oldValue( executing) and state( finished).

This will cause the system to read the new values "native" state variables operation isReady, isExecuting, isFinished. Therefore, the asynchronous operation AsyncOperationwe need to redefine the "native" state variables operations isReady, isExecuting,isFinishedusing the new property stateas calculated variables that return the correct values. We will do this in a extensionclass extension AsyncOperation:


At some point in time, the property of an operation isReadybecomes equal true, and we must use the property isReadyfor superclass, which “perceives” dependencies ( dependencies) on other operations. By combining our own logic with property isReadyfor superclass, we can be sure that the operation is really “ready.” Note that if a variable stateis equal .finished, then the property superclass isFinishedis equal trueand the property isExecutingis false. We will also override the property isAsynchronousby returning true, and two methods: start()and cancell().

In the method, start()we check whether the operation is destroyed, that is, the property isCancelledis equal true. If so, then we need to set a new value for the variable.state- .finished. If the operation is not destroyed, we call main(). Remember that there main()is an ASYNCHRONOUS function, and it returns immediately, so we should manually set the operation state statto be equal .executing. When the ASYNCHRONOUS function returns completionHanller, in it we need to set the status of the operation .finished. It is very important to remember that in the case of an ASYNCHRONOUS function, we cannot use in the method a start()call to a similar method for superclass- super.start(), since this would mean a synchronous start of the function main(), but we need exactly the opposite.
In the method, cancell()we also need to set the status of the operation .finished.

As a result, we got an abstract classAsyncOperation, and we can use it for our own ASYNCHRONIC operations. To do this, you must perform the following procedure:


As an example, take the function of asynchronous slow (inside is sleep (1)) the addition of two numbers:


Using AsyncOperationas superclass, we have to redefine main ()and remember to set the operation state statein .finishedto callback:


callbackreturns the result resultthat we assign to the operation property self.resultand set the operation state stateto .finished, which informs the operation queue about the completion of asynchronous addition of numbers and that it is no longer necessary to work with this operation.

Let's use our SumOperationto get an array of the sum of pairs of numbers:


For each pair of numbers, we create an operation SumOperationand place it in the operation queue in the additionQueueusual way. We see that the order of execution of asynchronous operations is slightly different from the order of the pairs of numbers in the array. It speaks about multi-threaded execution of our asynchronous operations.

The second example is related to the asynchronous loading of an image as given URLby URLSession:



Create an ASYNCHRONOUS operation ImageLoadOperation, which, like last time, is a subclass of a class AsyncOperation. For the operation ImageLoadOperation, as well as last time, we redefine main ()and do not forget to set the status of the operation statein .finishedto completion:



Create the operation operationLoad, get the image operationLoad.outputImageand map to view:



You can look at the code AsyncOperations.playgroundon Github .

Dependencies ( dependencies)


The code can be viewed at LoadAndFilter.playground on Github .

In this section, we will consider how the result of one operation is transferred to another operation, while forcing the second operation to begin only when the first one ends.



In the picture you see two operations: the first one loads the image from the network, and the second “mists” the upper and lower parts of the image - filtering.
Both of these operations work very well with each other: the image “downloader” directly transfers its data to the “filter”.

We could create one operation that includes both of these operations, but this is not a very flexible design.



It is desirable to have some modularity of operations when working with images in order to use them in any order in different places of the application.

A more flexible solution is associated with performing a “chain” of operations with transferring an image from the first operation to the second. We can determine that the “filter” depends on the “loader”, and then the queue of operations OperationQueuewill know that the “filter” is set to the state readyonly after the “loader” operation ends. This is a truly remarkable "ability" queue of operations OperationQueue. You can create a very complex graph of "dependencies" and thus force the OperationQueueimplementation to automatically launch operations as you need. APIclass Operationthat supports work with "dependencies" (dependencies) - very simple, but reveals a fantastic power when working.



You can add and remove “dependencies” ( dependency), as well as get a list of “dependencies” dependenciesadded for this operation. Below we will use the list dependenciesto get the input image of the dependent filtering operation.

When you create dependencies, there is a high probability of getting a deadlock ( deadlock ):



The appearance of closed cycles in the “dependencies” column leads to the appearance of a deadlock and there is no universal way to eliminate them, except by identifying them by visual analysis.
The next problem with “dependencies” of operations is how to transfer data along a “chain of dependencies”? For example, in the above example, when the “loader” first works, and then the “filter”, for which the input image is the output image of the “loader”:



How do we achieve this? We create a protocol ImagePassthat will supply us with the necessary data, in our caseUIImage? :



Loader "is a class of ImageLoadOperationimage loading operation already known to us from the network. At the input, the URLimage is set as a string urlString, and the output is the image itself outputImage.



The class ImageLoadOperation" confirms "the protocol ImagePassand returns the imageoutput loaded image as a protocol property outputImage.

In turn, the operation" filtering "- class FilterOperation- in the absence of an input image, it _inputImageanalyzes its" dependencies " dependenciesand is interested only in those that" confirmed "the protocol ImagePass. He chooses the first such dependent operation and extracts his own inputImage: From the



" filter "at the input - input imageinputImage, and the output is the filterImage (image:)image filtered by the function outputImage. This is a normal synchronous operation, so we only need to redefine it main().

We want to make these two operations work together, so that the “filter” uses the inputImageoutput image of the “load” operation as the input image . To do this, we initialize the filtering operation filterwith the value nil:




The code is LoadAndFilter.playgroundon Github .

Destruction of operations on OperationQueue


The code is Cancellation.playgroundon Github .

We will consider another great queue OperationQueueopportunity - the ability to destroy operations.

After you have placed your operation Operationin the queue OperationQueue, you have no way to influence its execution, since the operation queue has its own plan for launching operations and it completely controls your operation. But you have the ability to destroy Operationusing the method cancel().



You may think that calling a method cancel()will instantly stop the operation, but it is not. The method cancel()only sets the isCancelledoperation property to true. If the operation has not yet started, then the default methodstart()will not allow the operation to be performed and set its property isFinishedto true. If you override the ( override) method start(), then you must retain the ability of your to start()prevent the operation from starting if the property of the operation is isCancelledset to true. And if you look at the abstract class AsyncOperation, you will see that we did just that.

Next in the main()operation method , especially before doing something slow or resource-consuming, you need to test the property isCancelledto determine whether the operation has already been destroyed. And if the operation is destroyed and it shows the value of the trueproperty isCancelled, then you have to carry out the necessary actions to stop the operation. If operationOperationperformed locally, for example, image conversion, then you can stop the operation. If the operation is related to accessing the network, such as downloading an image from a server, then you cannot stop such an operation until the server returns the result to you.

You need to add "logic" between the various "steps" of an operation to check whether it is worth continuing to perform this operation or to set the operation to the isFinishedequal state true.

APIclass Operation, designed to remove the operation is very simple and consists of only two positions:


You call a method cancel()— it sets the property of the isCancelledoperation to true. It is important to note that when a method is called cancel(), the properties isExecutingand isFinishedalso change to falseand, truerespectively.


For an operation it is perfectly normal to be destroyed ( isCancelled = true) and will not end ( isFinished = true). The property isCancelledinforms the operation that it should stop, and the property isFinishedinforms the system that the operation has already been stopped.

Our abstract ASYNCHRONOUS operation AsyncOperationredefines the method cancel()in such a way that it sets the state of the operation stateto .finished, and this change leads to a change in the properties of the operation isFinishedand isExecuting:


A queue OperationQueuecan destroy all operations:


Using the example of some user operations, let's see how we can achieve the correct response of an operation to a method call cancel().

The code is Cancellation.playgroundon Github .

The operation ArraySumOperationhas an input array of inputArraytuples consisting of a pair of integers and forms an array of outputArraytheir output totals:



For each pair of numbers we use the “slow” addition function slowAddpresented in the folder Sourceon the Cancellation.playground and add to the output array outputArray.

Set the input array of numbers, form the operation sumOperation, add it to the operation queuequeueand start the timer, which will allow us to further regulate the time after which we will be able to check the operation response sumOperationto the method call cancel(). In addition, the operation has completionBlock, in which we stop the timer, show on Playground outputArrayand complete the work on Playground:



So, the operation sumOperationtakes a little more than 5 seconds. Now we will try to destroy this operation, 2 seconds after the start, calling the method cancel ():



We got an unexpected result - the operation was sumOperationcompleted, no operation was destroyed. What is the matter?But the fact is that the method cancel ()only sets the property isCancelledto true, and the actions necessary to delete the operation fall on the developer of the operation. We need to respond to the fact that the property is isCancelledset to true. We will loop through each addition to the output array to check whether the operation is not destroyed. And if destroyed, then we interrupt the cycle:



Let's re-launch Playground:



We stopped a little later than 2 seconds and managed to get 2 sums, and when we were going to get the third sum, we received a signal to destroy the operation and stopped receiving further sums. This example clearly shows how to get the reaction of a user operation to a command cancel ().

Let's look at another operation AnotherArraySumOperation, which differs in that another function is used slowAddArrayto get the output array of a loop through the array of tuples:



The difference from the previous case is that the loop on the elements of the array of tuples is not in the main()operation method , but in another function and is difficult for us abort the loop if the operation is destroyed. But there is such a possibility, although it is very sophisticated:



At the input of a function, an slowAddArrayarray of inputpairs of integers, in addition, it has an argument progress, which is a Optionalfunction that takes a variable depth of array processing

Double(results.count) / Double(input.count

and returns it as an argument Bool. This Booldefines the continuation of array processing.

In the methodmain()operations AnotherArraySumOperation(the previous figure), we passed an slowAddArrayarray to the function inputArray, and the argument was progressformatted as a “tail” closure, in which we used the property progressfor printing. The property progressis Double, so we multiplied it by 100 and got% of the end of array processing and completion of the operation. Then we return the response to the destruction of the operation, which is a signal to continue or interrupt array processing. A reaction is an inversion of a property isCancelled.

Replace the previous operation SumOperationwith a new operation AnotherArraySumOperation:



Interrupting the operation after 2 seconds, we got the same result - we managed to process only 2 array elements out of 5-ti, that is, 40% , before the operation was destroyed.

Set the interrupt delay of the operation 4 seconds: 4 elements of the array of 5 were



processed , that is, 80% , before the operation was destroyed. It is very important to make sure that individual operations respond to the property and, therefore, can be destroyed. But in addition to the destruction of individual operations using the method , you can destroy all started operations on the operation queue using the method

isCancelled

cancel ()OprationQueuecancellAllOperations. This is especially useful if you have a set of operations that work for a single purpose. This goal may be to run multiple independent operations in parallel or to represent a graph of dependent operations executed one after another. Consider both of these cases.

Pattern 1. Working with a group of independent operations


The code is CancellationGroup.playgroundon Github .

We set the task to achieve the same result as the operation ArraySumOperationpresented in the previous section. This operation takes an array of tuples (Int, Int), and, using the slow addition function slowAdd(), creates an array of sums of the numbers that make up the tuple. The loop on the components of the input array is hidden inside ArraySumOperation. Let's create a group of individual very simple type operations SumOperation. The operation SumOperationadds two numbers from the input pair inputPairusing the slow addition function slowAdd()and returns the result output:



Create the most common class GroupAddthat manages the privatequeue of operations queueand a variety of operationsSumOperationin order to calculate the sum of all pairs in the input array and place in the output array outputArraytuples ( Int, Int, Int )consisting of input data and result:



When initializing an instance of a class, an GroupAddinput array of inputpairs of numbers is formed from which type operations are formed SumOperation. In completionBlockeach operation, the result is added to the output array outputArray, which is performed on a single privateserial line operations appendOperationto avoid race condition .

The class has all the typical operation Operationmethods: start(), cancel (), wait (), so we can consider it as a "complex operation".

Create eq emplyar classGroupAdd, giving the input an array of pairs of numbers:



We start groupAdd, wait for 1 second and use the method cancel ()to remove all the summation operations from the operation queue. As a result, after the completion of all operations (we use wait(), which can NOT be used on main queue, but can be on Playground), we get a shortened output array:



The result can be viewed Playground CancelletionGroup.playgroundon Github .

Pattern 2. We work with a group of dependent operations


The code is CancellationFourImages.playgroundon Github .

As a group of dependent operations, we consider a group of interrelated operations already known to us for downloading an image from the network, filtering and modifying it UI. Let's try to arrange this sequence into a separate class ImageProviderthat will manage these operations on OperationQueueusing the methods start (), wait ()and cancel ().

We will have two abstract operations (that is, those that have no implementation of the method main()). One is the asynchronous operation already familiar to us AsyncOperation, and the other is the operation of ImageTakeOperationextracting the input image inputImagefrom the dependencies dependecies.

BasedAsyncOperationcreate an image download operation from the “network” at the specified URL address:



This operation confirms the protocol ImagePassfor transmitting the resulting image outputImage further along the chain of operations.

An abstract operation ImageTakeOperationextracts the input image inputImagefrom the dependencies dependecies, if it is not specified when initializing this operation, and allows you to “pick up” the output image using the already familiar protocol ImagePassused to transfer images in a chain of sequential operations: The



abstract class is ImageTakeOperationvery convenient to use as an superclassoperation. participating in a chain of dependent operations. For example, for filtering operation Filter:



Or for the operation of “aging” images in the style of “hipster” PostProcessImageOperation:



Or for the operation of “throwing out” the input image into the external environment with the help of a closure ImageOutputOperation:



Now let's take a class ImageProvider. Create the regular class ImageProviderthat controls the privatequeue operations operationQueue, and the sequence of operations dataLoad, filterand outputto load an image from a given the URL , filter it and pass in the circuit completion:



Class ImageProviderhas all the typical operation Operationmethods: start(), cancel (), wait (), so we can consider it as a "complex operation ".

Create 4 instances of the class ImageProvider:



We start loading images:



We are waiting for the completion of operations and we get 4 images: The



duration of all operations is a little more than 10 seconds.

We start loading images, wait 6 seconds and use the method cancel ()to delete all operations. As a result, we get the download of only three images - 1st, 3rd and 4th:



The result can be viewed on the Playground CancelletionFourImages.playgroundon Github .

Pattern 3. We work with TableViewController and CollectionViewController


The project code is in a folder OperationTableViewControlleron Github .

Very often, tables in iOS applications contain images that require access to the server, and sometimes additional actions with the resulting “filtering” image, which was mentioned in the previous section. All this takes considerable time and for smooth scrolling of the table, all manipulations with images must be performed asynchronously outside main queue. Let's consider the application of the class presented in the previous section ImageProvider, which performs a group of interrelated operations already known to us for downloading an image from the network, filtering and modifying it UI.

Consider as an example a very simple application consisting of only oneImage Table View Controller, whose table cells contain only images downloaded from the Internet and an activity indicator showing the loading process:



Here’s what the class ImageTableViewControllerthat serves the screen fragment looks like Image Table View Controller: The



model for the class ImageTableViewControlleris an array of 8 URLs :

  1. The Eiffel Tower
  2. Venice - loaded and filtered much longer than the rest
  3. Scottish castle
  4. Arctic-02
  5. The Eiffel Tower
  6. Arctic -16
  7. Arctic -15
  8. Arctic -12




The class ImageTableViewCellfor the cell of the table into which the image is loaded has the form:



Public APIthis class is a string imageURLStringcontaining the URL address of the image. But if we set imageURLStringnot equal nil, then the image will not be loaded, only the indicator in the form of a “spinning wheel” will start working. But if we already have a somehow loaded and processed image image, then calling the method updateImageViewWithImage, we will show it in this cell on the screen with the help of a light animation. In this class there is an activity indicator spinnerthat starts if you assign a imageURLStringvalue in the method tableView( _ : cellForRowAt:).

The image will be loaded in the delegate methods UITableViewDelegateresponsible for the interaction of the cell UITableViewCellwith the table.UITableView :





A request for asynchronous loading of an image using a class ImageProviderwill be moved out of the method tableView( _ : cellForRowAt:)to make this method as easy as possible. It will be located in the tableView( _ : willDisplay:forRowAt:)delegate method that prepares the cell to become visible. Another tableView( _ : didEndDisplaying:forRowAt:)delegate method will be used to destroy any request to load an image, which it will not have completed by the time the cell leaves the screen. This is a fairly general approach and can be used in any application that works with TableView. This will improve the scrolling performance of the table.

But first, back to the class ImageProviderthat will be used in this application. Unlike the class variant ImageProviderthat was used in the previous section onPlayground, we will use its simplified form. Namely, we do not need to ImageProviderstop ( isSuspended = true) the operation queue when initializing an instance of a class , and then specifically start an instance of the class ImageProviderusing the method start()- we immediately start the chain of dependent operations during initialization and set waitUntilFinishedequal false, since this is not the Playgroundapplication, and we cannot use synchronous method wait():



Thus, in the class ImageProviderwe have an initializer, at the input of which we have to submit a string imageURLStringwith the URL address of the image and the closure completion, which genericin a way returns an image like the UIImage?one who created this instance of the classImageProvider, instead of using a computed property image. The input closure completionhas a signature (UIImage?) -> (), that is, takes an image UIImage?and returns nothing. It can be used to return to UITableViewController.

In addition, we must allow to destroy an instance of the class ImageProvider, which will lead to the destruction of all the started operations if the table cell leaves the screen before all operations are completed. Therefore, we have a method cancel()in class ImageProvider.

So, the class ImageProviderprovides asynchronous execution of a group of dependent operations for loading an image from a server, filtering it and delivering it to a method UITableViewDelegate. If necessary, we can remove an instance of this class.

Go back to ImageTableViewController.

Instead of loading an image in a method tableView( _ tableView:, cellForRowAt indexPath:), we will do it in another delegate method - tableView( _ tableView: , willDisplay cell:, forRowAt indexPath:)and then we will delete the images in the method tableView( _ tableView: , didEndDisplaying cell:, forRowAt indexPath:).

Let's create an extension extensionfor these two methods. Let's start with the method tableView( _ tableView: , willDisplay cell:, forRowAt indexPath:).



As in the method tableView( _ tableView:, cellForRowAt indexPath:), we will have a cell celland indexPath. First, we perform the standard procedure for verifying that the cell is of type ImageTableViewCell. Then we create imageProviderwith a string imageURLs [ (indexPath as NSIndexPath).row ]as the URL of the image address and a closure that is designed as a “tail” closure. In the closure, we get the image imagethat needs to be shown in this cell of the table. This UI, and we should use to update it.main queue, because if you try to do an update UIon the background queue ( background queue), then this will not work, and you will wonder why this image does not appear. We have a mainclass property OperationQueuefor main queue, and all we need to do is call a method updateImageViewWithImage( image )on main queuethat will update the cell we need UITableViewCell.

Now we need to think about the possible removal of the operation. For this we need not to lose the link to the created one imageProvider, otherwise we will not be able to find it later and delete the operations associated with it.

Go to the very beginning of the class ImageTableViewControllerand add a new property with the name imageProviders:



The property imageProvidersis a set of objects of the type imageProvider, which is initially empty.

Let's look at the bottom of the ImageProvider.swift file . You will see there an already existing extension of the extensionclass ImageProvider, which confirms the protocol Hashablerequired for the sets Set:



We get the calculated property hashValueand the comparison operator for equality ==. And now we can set and compare instances of objects ImageProvider. Go back to ImageTableViewController. Now we can track instances of objects ImageProviderand add them to the set imageProvidersthat are currently involved:



Let's go through this code to see step by step what is happening. This is delegate method.tableView, which is called just before the cell appears on the screen. At this point, we create ImageProviderthat asynchronously loads, filters the image and returns the resulting image imageto completionHandler. We use imageto update UIImageViewon main queue. Then we memorize ImageProviderin the set imageProvidersso that we can destroy it later. We will do this in the following delegate method tableViewwith a name tableView( _ tableView:, didEndDisplaying cell:, forRowAt indexPath:)that is called immediately after the cell leaves the screen. It is here that we need to destroy all the operations of this ImageProvider:



For this we find ImageProviderfor this cell celland use the method of cancel ()thisImageProvider, which removes all operations of this provider, and then we delete the provider from my set imageProviders. As always, we first perform the standard procedure for verifying that the cell has a type ImageTableViewCell, and then we find all providers that have the same row ImageURLStringas for the given cell. We go through all these providers and remove them, and then remove them from the set imageProviders. This is all we need to do.

Let's run the application.



You can see that the activity indicators are working while loading images and scrolling now works very quickly without any delay. Images are empty until they are loaded, and then they are animated. Perfectly.
The project code is in a folder OperationTableViewControlleron Github .

Operation OperationQueue GCD


GCDand Operationshave a lot of similar features, but the table shows their differences.



DispatchGroupand OperationQueuecan handle the event associated with the complete completion of all tasks, but you must be very careful when running the method waitUntilAllOperationsAreFinishedfor the queue OperationQueue, which in no case MUST NOT be main queue.

As for dependencies ( dependecies), then all you can do GCDis implement a chain of tasks for privatesequential ( serial) DispatchQueue. But this is the strongest side OperationQueue. Dependencies ( dependecies) on OperationQueuecan be more complex than just chains, and operations can be performed on different queues OperationQueue.

You can use barriers inGCDto solve the problem of "writers" and "readers", if the sequential ( serial) line is DispatchQueuenot suitable. The appropriate solution to this problem is OperationQueuevery confusing and requires flagsvery special dependencies.

In GCDyou can delete only DispatchWorkItems. Operations Operationscan be deleted using their own method cancel()or all operations immediately on OperationQueue. You can delete the circuit in BlockOperation.

Both can GCDand Operationsperform a SYNCHRONOUS function ASYNCHRONIC. In doing so, the operation Operationsupplies us with an object-oriented model for encapsulating all the data for this reusable function, including the implementationsubclasses Operation. But often, for simpler tasks that are not burdened with complex dependencies, it is more convenient to use lighter methods GCDthan to create an operation Operation. In addition, the Dispatchblocks GCDtake less time to complete: from nanoseconds to milliseconds, and the operation Operationusually takes from a few milliseconds to minutes.

Conclusion


In this article, we examined the following questions regarding the operation and the Operationqueue of operations OperationQueue:



1. An operation Operationcan encapsulate a task and data in one object that has a “life cycle” and properties that reflect its states.

2. BlockOperationis an object-oriented "wrapper" around DispatchQueue.global(), which allows you to track the execution of a closure group instead of losing control of this group on DispatchQueue.global(). BlockOperationIt is convenient to use for simple operations as an alternative GCDif you are already using your application Operationsand do not want to interfere with them DispatchQueue.

3. Operations Operationreveal their main potential if they are launched onOperationQueue. Once you have prepared an operation Operation, you transfer it to OperationQueue, which controls the order of execution of all operations, essentially being a very simple model that hides the complexity of multi-threaded programming. OperationQueuesimilar to DispatchGroup, for which you can mix operations with different levels qualityOfServiceand wait until all operations end. But you must be very careful when you call this syncmethod.

4. To include ASYNCHRONOUS functions in the operation Operation, we must do something special in order to accurately record its completion. We must manage the ASYNCHRONOUS operation AsyncOperationmanually using KVO .

5. Unsurpassed opportunity operationsOperationis that you can combine them into chains of operations to produce a complex dependency graph ( dependencies). This means that you can very easily define an operation Operationthat cannot start until one or more other operations Operationhave completed. The article shows how a protocol can be used protocolto transfer data between operations Operationfor a dependency graph ( dependencies). But you must examine your dependency graph in order to avoid cycles that can cause deadLock(intractable deadlock), especially if there are dependencies between operations on different ones OperationQueue.

6. Once you have transferred the transaction Operationto the queueOperationQueue, you have lost control over this operation, because now the queue OperationQueueitself is the schedule for launching operations for execution and manages their implementation. However, you can use the method cancel ()in order to prevent the operation from starting. The article shows how to take property into account isCancelledwhen constructing an operation Operationand how you can delete absolutely all operations on the operation queue OperationQueue.

7. The conclusion shows the development of an application that reflects the real scenario when you need to scroll through a table with images obtained from the Internet and requiring additional actions like “filtering” or “aging”. All this takes considerable time and for smooth scrolling of the table, all manipulations with images must be performed asynchronously outsidemain queue. In this application, we used a wide range of techniques for working with an operation Operation, in particular, with ASYNCHRONOUS OPERATION AsyncOperationand its dependencies ( dependencies), which allowed us to achieve a significant improvement in our own UI.

This article, together with the previous one, gives you a complete picture of the multithreaded processing in Swift 3and 4on iOS that currently exists. Now you can take a full part in the discussion of future multithreaded processing capabilities in Swift, which is being laid right now, when for the version, multi-threading ( ) is declared a Swift 5priority direction in addition to ABIstability concurrency. VersionSwift 5It assumes only the beginning of work on a completely new multithreading model, the implementation of which will continue in subsequent versions. There are already proposals for a future multithreading model in Swift 5 . So “turn on the motors!” And forward.

The evolution of Swift can now be viewed here .

Links


→ WWDC 2015.Advanced NSOperations (session 226).
→ Having fun with NSOperations in iOS
→ NSOperation and NSOperation Query Tutorial in Swift
→ iOS Concurrency with GCD and Operations
→ CONCURRENCY IN IOS
→ Concurrency in Swift: One possible approach

Source: https://habr.com/ru/post/335756/


All Articles