Threads of execution

Threads are used to achieve concurrency inside an application. They are ordered sequence of instructions for specific tasks. Note that threads do not carry out the execution themselves. The CPU handles the actual execution of the tasks.

If an application has only one thread, i.e. a non-concurrent or single-threaded application, that one thread holds all the instructions for all the tasks. The CPU will not receive instructions to execute the next task until it has completed the previous task. Each task has to finish before the next task can start. This can cause the application to slow down if one task happens to be a blocker.

In a multi-threaded application, there are multiple threads. A single-core device can switch back and forth between threads in order to execute multiple tasks concurrently. A multi-core device can execute multiple tasks on multiple threads via paralellism.

When working with multiple threads, we have to be careful about shared resources. Because threads in the same application have access to all the same data structures. If two threads try to manipulate the same data, they might introduce bugs to the system. I ran into this problem when I was trying to convert my single-threaded Swift server to a multi-threaded one with the code below.

In this example, I utilize Grand Central Dispatch, which handles threads management for me. With GCD, all the tasks get added into a queue, which handles the scheduling of these tasks on a pool of threads. The advantage of GCD is our threads are managed by the system so we don’t have to worry about it. Having multiple threads and managing these threads also take away from the application’s efficiency. Incidentally creating too many threads can slow down our application due to execessive memory consumption and CPU time to manage these threads. Therefore, using GCD to achieve concurrency is a great alternative to creating threads on your own.

In this snippet of code, the server create a socket on the main thread. This socket listens and accepts incoming client connections. The request handling is done on background threads.

public class Server {

    var listeningSocket: Socket!
    var clientSocket: Socket!
    var parser: RequestParser
    var router: Router

    public init(parser: RequestParser, router: Router) {
        self.parser = parser
        self.router = router
    }

    public func run() {
	let queue = DispatchQueue.global(qos: .userInteractive)
        do {
            try self.listeningSocket = Socket.create()
            try self.listeningSocket.listen(on: 5000)

            repeat {
                try clientSocket = self.listeningSocket.acceptClientConnection()
                queue.async {
                    self.handleRequest(socket: self.clientSocket) #this method close the socket at the end
                }
            } while true
        } catch let error {
            print (error.localizedDescription)
        }
    }
}

This implementation doesn’t work! Notice I have set clientSocket as an instance variable. As multiple requests come in, we can imagine the chain of events like this:

Request A comes in -> clientSocket = socketA -> handleRequest(socketA)

Request B comes in -> clientSocket = socketB -> handleRequest(socketB)

Because clientSocket is a shared resource and because the instance type might very well be a reference type, when socketB is created and set to clientSocket, it might inadvertently change the reference ofclientSocket in the previous request from socketA to socketB. SocketA now becomes an “orphan” socket that never get closed which might cause problem. These cycle continues and the problem gets worse as more requests comes in.

So instead of having clientSocket as an instance variable, I can create a new clientSocket every time the listeningSocket accept a new connection, hence, eliminate the shared resource problem. And everything works perfectly!

public class Server {

    var listeningSocket: Socket!
    var parser: RequestParser
    var router: Router

    public init(parser: RequestParser, router: Router) {
        self.parser = parser
        self.router = router
    }

    public func run() {
	let queue = DispatchQueue.global(qos: .userInteractive)
        do {
            try self.listeningSocket = Socket.create()
            try self.listeningSocket.listen(on: 5000)

            repeat {
                    let clientSocket = try self.listeningSocket.acceptClientConnection()
                    queue.async {
                        self.handleRequest(socket: clientSocket)
                    }
                }
            } while true
        } catch let error {
            print (error.localizedDescription)
        }
    }
}