Skip to content

Concurrency, (a)synchronicity and background processing

Prepared and tested with Xcode 9 and Swift 4


In this tutorial we cover the following topics

We assume that reader is familiar with basic concepts like processes, threads, concurrency, asynchronicity and background processing.

For more details please refer to Concurrency Programming Guide


General information


Long, long time ago...

Soon or later there will come a time when you will have to do more than one thing at the same time. This would require the creation of one or more additional threads. Unfortunately, creating threaded code manually is realy challenging task. Threads are a low-level elements of code and technical details of running separate threads on a single processor, and, what is much more complicated, coordinating processes that are distributed across multiple processors, is really very difficult stuff.

Even if we will succeed in coding threads, a series of new problems arise.

  • How to select the optimal number of threads for an application.
  • How to manage them dynamically based on the current system load and the underlying hardware.
  • How to synchronize threads without losing performance and general correctness of our code (application execution).

Long, long time ago, despite all these problems and doubts, we were forced to work with threads manually.


Present

This days, both OS X and iOS adopt a more human-friendly approach to the execution of concurrent tasks than is traditionally found in thread-based systems and applications. Rather than creating threads directly, applications need only define specific tasks and then let the system perform them. By letting the system manage the threads, applications gain a level of scalability not possible with raw threads. Application developers also gain a simpler and more efficient programming model.


The following sections provide more information about dispatch queues, operation queues, and some other related asynchronous technologies you can use in your applications.

We will discuss


Synchronicity vs. Asynchronicity

Synchronicity is a very basic paradigm: do one thing after another. Important is that before we start new task (all) previous should be finished. A synchronous function returns control to the caller only after the task is completed. It's simply but in many cases also ineffective. Imagine situation when some resources must be downloaded through slow network connection - it can stop our work for a long time. Much better option is to separate downloading code, execute it and let it call our main code when completed. And that is the essence of asynchronous programming: our code relinquishes control over an asynchronous process until such time that it has completed, failed, or been cancelled. An asynchronous function returns immediately, ordering the task to be done but not waiting for it. Thus, an asynchronous function does not block the current thread of execution from proceeding on to the next function. When called, an asynchronous function does some work behind the scenes to start a task running but returns before that task might actually be complete. Typically, this work involves acquiring a background thread, starting the desired task on that thread, and then sending a notification to the caller (usually through a callback function) when the task is done. Now, OS X and iOS provide technologies to allow us to perform any task asynchronously without having to manage the threads ourself.

The challenges of asynchronous programming

  • The results of asynchronous method calls can be returned in an unpredictable order. HTTP requests are a common case.
  • Some tasks may be dependent on the previous completion of other tasks, and so we will need some sort of dependency management strategy.
  • An asynchronous task may fail to complete. In addition to the function failing, which affects synchronous code too, we need to take into consideration that a program may be suspended or terminated before the asynchronous code returns any result at all.


The Grand Central Dispatch technology elements

One of the technologies for starting tasks asynchronously is Grand Central Dispatch (GCD). This technology takes the thread management code we would normally write in our own applications and moves that code down to the system level. All we have to do is define the tasks we want to execute and add them to an appropriate dispatch queue. GCD takes care of creating the needed threads and of scheduling our tasks to run on those threads. Because the thread management is now part of the system, GCD provides a holistic approach to task management and execution, providing better efficiency than traditional threads.

Starting of a Swift 3, GCD was redesigned, moving from a C-based API to a Swift-like API that included new classes and new data structures.


Dispatch queues

Queue, in general, is a simple data structure following the FIFO pattern (First In, First Out), meaning that the element that comes first will also be taken out first. We can think of it like a queue of humans waiting in front of the counter or like a pipe where we put balls from one side and take them out from the other.

The smallest building element in GDC used with queues is known under the work item name. A work item is literally a block of code that is either written along with the queue creation, or it gets assigned to a queue and it can be used more than once (reused). The work item is what it means exactly: It’s the code that a dispatch queue will run.

Queues can be either serial or concurrent.

  • Serial queues guarantee that only one task (work item) from this queue runs at any given time.
  • Concurrent queues allow multiple tasks to run at the same time.

Tasks are guaranteed to start in the order they were added. Tasks can finish in any order and we have no knowledge of the time it will take for the next task to start. We will not know the amount of time between one task ending and the next one beginning, nor the number of tasks that are running at any given time.

GCD provides three main types of queues

  • Main queue: runs on the main thread and is a serial queue. This queue is a common choice to update the UI after completing work in a task on a concurrent queue. Typically, to do this, we will code one closure inside another.
  • Global queues: concurrent queues that are shared by the whole system. There are four such queues with different priorities: high, default, low, and background. This queue is a common choice to perform non-UI work in the background.
  • Custom queues: serial or concurrent queues that we can create on our own. This queue is a common choice when we want to perform background work and track it.

Tasks submited to a dispatche queue beeing an instance of DispatchQueue are encapsulated by DispatchWorkItem. We can configure the behavior of a DispatchWorkItem such as its QoS class or whether to spawn a new detached thread.

The primary QoS classes are

  • User interactive. This represents tasks that need to be done immediately in order to provide a nice user experience. We will use it for UI updates, event handling and small workloads that require low latency. The total amount of work done in this class during the execution of an application should be small. This should run on the main thread. Work falling into this category is virtually instantaneous.
  • User initiated. This represents tasks that are initiated from the UI and can be performed asynchronously. It should be used when the user is waiting for immediate results, and for tasks required to continue user interaction. This will get mapped into the high priority global queue. Work falling into this category is nearly instantaneous, such as a few seconds or less.
  • Utility. This represents long-running tasks, often related with a progress indicator in UI. We use it for computations, I/O, networking and similar tasks. This class is designed to be energy efficient. This will get mapped into the low priority global queue. Work falling into this category takes a few seconds to a few minutes.
  • Background. This represents tasks that are not very important for a user (beeing more precise: for his UI experience). We use it for tasks that don’t require user interaction and aren’t time sensitive. This will get mapped into the background priority global queue. Work falling into this category takes significant time, such as minutes or hours.

In addition to the primary QoS classes, there are two special types of QoS. In most cases, we won’t be exposed to these classes, but there is still value in knowing they exist.

  • Default. The priority level of this QoS falls between user-initiated and utility. This QoS is not intended to be used by developers to classify work. Work that has no QoS information assigned is treated as default, and the GCD global queue runs at this level. This will get mapped into the default priority global queue.
  • Unspecified. This represents the absence of QoS information and cues the system that an environmental QoS should be inferred.


Dispatch sources

Dispatch sources is another mechanism with C precedesor for processing specific types of system events asynchronously. A dispatch source encapsulates information about a particular type of system event and submits a specific block object or function to a dispatch queue whenever that event occurs. You can use dispatch sources to monitor the following types of system events:

  • Timers. Timer dispatch sources generate periodic notifications.
  • Signal handlers. Signal dispatch sources sends notifications every time a UNIX signal arrives.
  • Descriptor-related events. Descriptor sources sends notifications related to a various file- and socket-based operations, such as:
    1. signal when data is available for reading;
    2. signal when it is possible to write data;
    3. files delete, move, or rename;
    4. files meta information change.
  • Process-related events. Process dispatch sources sends notifications about a process-related events, such as:
    1. a process exits;
    2. a process issues a fork or exec type of call;
    3. a signal is delivered to the process.
  • Mach port events. Mach port dispatch sources sends notifications about Mach-related events.
  • Custom events that we trigger. Custom dispatch sources are ones we define and trigger ourrself.

List of UNIX signals:


The Grand Central Dispatch: queues - examples


The Grand Central Dispatch: queues - examples (basic queue usage)

  1. Start a new macOS/Cocoa App project under the concurrencyDispatchQueues name
  2. Add viewDidAppear method to ViewController.swift file
  3. Add basicQueue method to ViewController.swift file

    Remember that each queue should have a unique label.
  4. Having the queue, we can execute code with it, either synchronously using a method called sync, or asynchronously async. The code to be executed can be provided either as closure or as a DispatchWorkItem object. In this example closure is used.

    Modify basicQueue method

    The first and last loop are executed on the main queue while the second loop is executed in the background. However the program execution will stop in the queue’s block until the queue’s task has finished, because we make a synchronous execution.

    The result should be as follow


    =100
    =101
    =102
    =103
    =104
    ==200
    ==201
    ==202
    ==203
    ==204
    ===300
    ===301
    ===302
    ===303
    ===304

    It won’t continue to the main thread’s loop and it won’t display
  5. Let's modify basicQueue method and replace queue.sync with queue.async

    The result should be similar to the following (you can run your app several times to get various results)


    =100
    =101
    =102
    =103
    =104
    ===300
    ===301
    ==200
    ===302
    ==201
    ===303
    ===304
    ==202
    ==203
    ==204

    This time second and third loop were executed concurrently.


The Grand Central Dispatch: queues - examples (Quality Of Service)

We will use the project started in the previous subsection (however we don't need anything we did so far).

Quality Of Service (QoS in short) is a way we say system that some task are more (less) important than other.

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add QoSQueue method to ViewController.swift file

    The result should be similar to the following (you can run your app several times to get various results)

    ===400
    ===300
    ==200
    =100
    ===401
    ===301
    ===402
    ===302
    ===403
    ===303
    ===404
    ===304
    ==201
    ==202
    ==203
    =101
    ==204
    =102
    =103
    =104

    We should be able to notice that user initiatedqueues have the highest priority, utility queue lower than previous and background the lowest.


The Grand Central Dispatch: queues - examples (concurrent queues)

We will use the project started in the previous subsection (however we don't need anything we did so far).

In the previous example there was four queues but all of them were serial. Now we will test what will happend when queue is concurrent

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add concurrentQueue method to ViewController.swift file

    The result should be similar to the following (you can run your app several times to get various results)

    =100
    =101
    =102
    =103
    =104
    ==200
    ==201
    ==202
    ==203
    ==204
    ===300
    ===301
    ===302
    ===303
    ===304

    As we can see, concurrentQueue queue is not "internally" concurrent. That means that if we assign more than one tasks to this queue, then those tasks are executed one after another, and not all together.
  3. Modify concurrentQueue method and add attributes to the queue initialization


    ===300
    =100
    ==200
    ===301
    =101
    ==201
    ===302
    =102
    ==202
    ===303
    =103
    ==203
    ===304
    =104
    ==204

    This time the tasks are executed pretty much in parallel.


The Grand Central Dispatch: queues - examples (delayed queues)

We will use the project started in the previous subsection (however we don't need anything we did so far).

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add printTime(withComment:) method to ViewController.swift file
  3. Add delayedQueues method to ViewController.swift file


    1: 2017-10-30 22:44:08
    2: 2017-10-30 22:44:09
    3: 2017-10-30 22:44:14


The Grand Central Dispatch: queues - examples (types of queues)

We will use the project started in the previous subsection (however we don't need anything we did so far).

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add typeOfQueue method to ViewController.swift file


    =200
    =100
    =201
    =101
    =202
    =102
    =203
    =103
    =204
    =104
    ==300
    ==301
    ==302
    ==303
    ==304
    ==400
    ==401
    ==402
    ==403
    ==404

    Notice that main queue, even though we specify async, behaves as serial.


The Grand Central Dispatch: queues - examples (using DispatchWorkItem objects)

We will use the project started in the previous subsection (however we don't need anything we did so far).

A DispatchWorkItem is a block of code that can be dispatched on any queue.

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add dispatchWorkItem method to ViewController.swift file


    workItem: 2017-10-31 00:05:50
    workItem: 2017-10-31 00:05:50
    workItem: 2017-10-31 00:05:50
    Notification from a workItem: 2017-10-31 00:05:50


The Grand Central Dispatch: queues - examples (dispatch barriers)

We will use the project started in the previous subsection (however we don't need anything we did so far).

A dispatch barrier allows us to create a synchronization point within a concurrent dispatch queue. In normal operation the queue acts just like a normal concurrent queue. But when the barrier is executing, it essentially acts like a serial queue. That is, the barrier is the only thing executing. After the barrier finishes, the queue goes back to being a normal concurrent queue.

GCD takes note of which blocks of code are submitted to the queue before barrier call, and when they have all completed it will call the passed in barrier block. Also, any further blocks that are submitted to the queue will not be executed until after the barrier block has completed. The barrier call however returns immediately and execute this block asynchronously.

Technically when we submit a DispatchWorkItem to a dispatch queue, we set a flag to indicate that it should be the only item executed on the specified queue for that particular time. All items submitted to the queue prior to the dispatch barrier must complete before this DispatchWorkItem will execute.
When the barrie is executed it is the only one task beeing executed and the queue does not execute any other tasks during that time. Once barrier is finished, the queue returns to its default behaviour.

The queue we specify should be a concurrent queue that we create ourrself. If the queue is a serial queue or one of the global concurrent queues, barrier would not work.

  1. Add / modify viewDidAppear method to ViewController.swift file
  2. Add barrierInAQueue method to ViewController.swift file. Notice that this code is without barrier yet.


    ===300
    =====500
    =100
    ====400
    ==200
    ===301
    =====501
    =101
    ====401
    ==201
    ===302
    =====502
    =102
    ====402
    ==202
    ===303
    =====503
    =103
    ====403
    ==203
    ===304
    =====504
    =104
    ====404
    ==204
  3. In barrierInAQueue method modify third loop


    ==200
    ====400
    =100
    =====500
    ===300
    ==201
    ====401
    =101
    =====501
    ===301
    ==202
    ====402
    =102
    =====502
    ===302
    ==203
    ====403
    =103
    =====503
    ===303
    ==204
    ====404
    =104
    =====504
    ===304
  4. In barrierInAQueue method replace global queue with a custom concurrent queue


    =100
    ==200
    =101
    ==201
    =102
    ==202
    =103
    ==203
    =104
    ==204
    ===300
    ===301
    ===302
    ===303
    ===304
    ====400
    =====500
    ====401
    =====501
    ====402
    =====502
    ====403
    =====503
    ====404
    =====504


The Grand Central Dispatch: queues - examples (dispatch groups)

We will use the project started in the previous subsection (however we don't need anything we did so far).

With dispatch groups we can group together multiple tasks and either wait for them to be completed or be notified once they are complete. Tasks can be asynchronous or synchronous and can even run on different queues. Dispatch groups are managed by DispatchGroup object.

  1. Add / modify viewDidAppear method to ViewController.swift file
  2. We will first use wait method to block current thread until all the group’s enqueued tasks have been completed.

    Add groupsQueue method to ViewController.swift file


    ==300
    =100
    ==400
    =200
    ==301
    =101
    ==401
    =201
    ==302
    =102
    ==402
    =202
    ==303
    =103
    ==403
    =203
    ==304
    =104
    ==404
    =204
    MAIN THREAD
  3. Next we will use notify to be notified when all the group’s tasks are completed.

    Modify groupsQueue method


    MAIN THREAD
    =100
    =200
    ==300
    ==400
    =101
    =201
    ==301
    ==401
    =102
    =202
    ==302
    ==402
    =103
    =203
    ==303
    ==403
    =104
    =204
    ==304
    ==404
    All the group’s tasks are completed


The Grand Central Dispatch: sources - examples


The Grand Central Dispatch: sources - examples (timer)

  1. Start a new macOS/Cocoa App project under the concurrencyDispatchSources name
  2. Add / modify ViewController.swift file
  3. When started, you should be able to see in Xcode's console window

    1: 2017-10-26 18:57:21
    2: 2017-10-26 18:57:21
    3: 2017-10-26 18:57:21
    hello world: 2017-10-26 18:57:31
    hello world: 2017-10-26 18:57:36
    hello world: 2017-10-26 18:57:42
    hello world: 2017-10-26 18:57:47
    hello world: 2017-10-26 18:57:51


The Grand Central Dispatch: sources - examples (signal)


We will use the project started in the previous subsection (however we don't need anything we did so far).

  1. Add / modify ViewController.swift file
  2. Run your application


    1: 2017-10-31 23:48:44
    pl.fulmanski.concurrencyDispatchSources
    68585
    4: 2017-10-31 23:48:44
    5: 2017-10-31 23:48:44
  3. Every time you pause the application with Pause button in Xcode's debug area and then resume it with Continue program execution button, you should see SIGSTOP with date and time printed

    SIGSTOP: 2017-10-31 23:48:49
    SIGSTOP: 2017-10-31 23:49:22
    SIGSTOP: 2017-10-31 23:49:32
  4. Replace SIGSTOP with SIGTERM

  5. Run your application


    1: 2017-11-01 00:00:25
    pl.fulmanski.concurrencyDispatchSources
    68775
    4: 2017-11-01 00:00:25
    5: 2017-11-01 00:00:25
  6. Use printed process ID to terminate it

    macbook-air-piotr:MacOS fulmanp$ ps -A | grep 68775
    68775 ?? 0:00.66 /Users/fulmanp/Library/Developer/Xcode/DerivedData/concurrencyDispatchSources-gjtktoljfnlzbefqcisebftvtobs/Build/Products/Debug/concurrencyDispatchSources.app/Contents/MacOS/concurrencyDispatchSources -NSDocumentRevisionsDebugMode YES
    68779 ttys003 0:00.00 grep 68775
    macbook-air-piotr:MacOS fulmanp$ kill 68775

    Every time we do this, we should resume our application with Continue program execution button located in Xcode's debug area to see SIGTERM with date and time printed


    SIGTERM: 2017-11-01 00:01:23


The Grand Central Dispatch: sources - examples (descriptor)


We will use the project started in the previous subsection (however we don't need anything we did so far).

  1. Add / modify ViewController.swift file
  2. Create somewhere in your home directory a file -- in my case it was

    /Users/fulmanp/Desktop/tutorials/apple/contents/code/concurrency/dispatchSourceDescriptorTest.txt


    macbook-air-piotr:code fulmanp$ pwd
    /Users/fulmanp/Desktop/tutorials/apple/contents/code
    macbook-air-piotr:code fulmanp$ mkdir concurrency
    macbook-air-piotr:code fulmanp$ cd concurrency/
    macbook-air-piotr:concurrency fulmanp$ touch dispatchSourceDescriptorTest.txt
    macbook-air-piotr:concurrency fulmanp$ ls -l
    total 0
    -rw-r--r-- 1 fulmanp staff 0 28 paź 20:22 dispatchSourceDescriptorTest.txt
  3. Run the application and select dispatchSourceDescriptorTest.txt file. You should be able to see this or similar set of messages in Xcode's console window

    1: 2017-10-28 22:43:48
    6: 2017-10-28 22:43:48
    2017-10-28 22:43:48.363443+0200 concurrencyDispatchSources[61740:13587042] warning: determined it was necessary to configure to support remote view vibrancy
    User selected /Users/fulmanp/Desktop/tutorials/apple/contents/code/concurrency/dispatchSourceDescriptorTest.txt
    7: 2017-10-28 22:43:55
  4. Change dispatchSourceDescriptorTest.txt attributes

    macbook-air-piotr:concurrency fulmanp$ chmod 755 dispatchSourceDescriptorTest.txt

    You should see

    File event: 2017-10-28 22:44:35
  5. If you want you can test it without file selection

    In this case you may get the following result


    1: 2017-10-29 00:43:58
    6: 2017-10-29 00:43:58
    User selected /Users/fulmanp/Desktop/tutorials/apple/contents/code/concurrency/dispatchSourceDescriptorTest.txt
    Bad fileDescriptor -1
    Operation not permitted

    To change it, locate a file with extension .entitlements in your Project navigator window. Click on it and you should be able to see image like below

    Change App Sandbox to NO

    save changes and again run your app. This time everything should be OK

    1: 2017-10-29 00:44:41
    6: 2017-10-29 00:44:41
    User selected /Users/fulmanp/Desktop/tutorials/apple/contents/code/concurrency/dispatchSourceDescriptorTest.txt
    7: 2017-10-29 00:44:41
    File event: 2017-10-29 00:44:48


Operation queues

The Foundation's Operation is one another concurrent related framework we can use on Apple's devices. It's like GCD, but much simpler. It provides object level abstractions of the tasks, called operations, that are performed on different threads, as well as abstractions of the operation queues in which they are placed, to be started and managed by the framework's actual threading mechanism; a mechanism that is well hidden and we never have to think of it.

In some sense operation queues acts very much like dispatch queues. We define the tasks we want to execute and then add them to an operation queue, which handles the scheduling and execution of those tasks. As we remember, dispatch queues always execute tasks in first-in, first-out order, wherease operation queues take other factors into account when determining the execution order of tasks. Primary among these factors is whether a given task depends on the completion of other tasks. We configure dependencies when defining our tasks and can use them to create complex execution-order graphs for our tasks. Although operation queues always execute operations concurrently, we can use dependencies to ensure they are executed serially when needed.

An operation queue is the Cocoa equivalent of a concurrent dispatch queue and is implemented by the NSOperationQueue class. The tasks we submit to an operation queue must be instances of the NSOperation class. An operation object encapsulates the work we want to perform and any data needed to perform it. In practice, because the NSOperation class is an abstract class, we should define our custom subclasses to perform our tasks. However, the Foundation framework does include some concrete subclasses that we can create and use to perform "predefined" tasks.

Operation objects generate key-value observing (KVO) notifications, which can be a useful way of monitoring the progress of our task.


Operation queues - examples


Operation queues - examples: operation queue basics

  1. Start a new macOS/Cocoa App project under the concurrencyOperationQueues name
  2. Add printTime(withComment:) method to ViewController.swift file
  3. Add viewDidAppear method to ViewController.swift file
  4. Add operationQueuesBasic method to ViewController.swift file
  5. Run your application. This is trivial application so results are also very basic

    operationQueuesBasic: 2017-11-07 13:02:25


Operation queues - examples: multiple operation

We will use the project started in the previous subsection.

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add operationQueuesMultipleOperations method to ViewController.swift file
  3. Run your application. Note that every time we run an application we might get different results.

    1: 2017-11-07 13:25:05
    4: 2017-11-07 13:25:05
    3: 2017-11-07 13:25:05
    0: 2017-11-07 13:25:05
    2: 2017-11-07 13:25:05


Operation queues - examples: dependencies

We will use the project started in the previous subsection.

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add operationQueuesDependency method to ViewController.swift file
  3. Run your application. Note that every time we run an application we might get different results.

    2: 2017-11-07 16:02:32
    3: 2017-11-07 16:02:32
    4: 2017-11-07 16:02:32
    0: 2017-11-07 16:02:32
    1: 2017-11-07 16:02:32
    dependent task: 2017-11-07 16:02:32


Operation queues - examples: dynamic blocks

We will use the project started in the previous subsection.

As we have seen in the previous example a BlockOperation can be initiated with a block of code. We can also create an empty block operation and add a working code dynamically.

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add operationQueuesDynamicBlocks method to ViewController.swift file
  3. Run your application. Note that every time we run an application we might get different results.

    4: 3: 2017-11-07 16:33:58
    4: 2: 2017-11-07 16:33:58
    3: 3: 2017-11-07 16:33:58
    4: 1: 2017-11-07 16:33:58
    0: 1: 2017-11-07 16:33:58
    2: 2: 2017-11-07 16:33:58
    2: 3: 2017-11-07 16:33:58
    0: 2: 2017-11-07 16:33:58
    1: 1: 2017-11-07 16:33:58
    3: 1: 2017-11-07 16:33:58
    2: 1: 2017-11-07 16:33:58
    1: 2: 2017-11-07 16:33:58
    1: 3: 2017-11-07 16:33:58
    0: 3: 2017-11-07 16:33:58
    3: 2: 2017-11-07 16:33:58


Operation queues - examples: completion blocks

We will use the project started in the previous subsection.

As we have seen in the previous example a BlockOperation can be initiated with a block of code. We can also create an empty block operation and add a working code dynamically.

  1. Add / modify viewDidAppear method to / in ViewController.swift file
  2. Add operationQueuesCompletionBlocks method to ViewController.swift file
  3. Run your application. Note that every time we run an application we might get different results.

    3: 2017-11-07 16:42:21
    2: 2017-11-07 16:42:21
    0: 2017-11-07 16:42:21
    4: 2017-11-07 16:42:21
    1: 2017-11-07 16:42:21
    2: sleep: 2017-11-07 16:42:23
    0: sleep: 2017-11-07 16:42:23
    3: sleep: 2017-11-07 16:42:23
    1: sleep: 2017-11-07 16:42:23
    4: sleep: 2017-11-07 16:42:23
    2: complete: 2017-11-07 16:42:23
    0: complete: 2017-11-07 16:42:23
    1: complete: 2017-11-07 16:42:23
    3: complete: 2017-11-07 16:42:23
    4: complete: 2017-11-07 16:42:23