In iOS, the Main Thread runs the UI of your app, so every UI-related task (updating the view hierarchy, removing and adding views) mus take place in the main thread.
People often think that asynchronous programming is the act of running multiple tasks at once. This is a different concept called Multithreading.
The deadlock problem has many established solutions. Mutex and Semaphores being the most used ones.
Mutex is short for Mutually exclusive lock (or flag).
Keep in mind that in this specific situation, it means that Thread A and Thread B needs the same resoureces as Thread A and it will wait for them to be free. For this reason, it is important to idnetify tasks that can be run in parallel before designing a multithreaded system.
주의할 점은 뮤텍스에 락을 거는 행위자체가 상당히 무거운 행위이기 때문에 최대한 Lock free computation promgramming을 해야한다
With this solutoin, a task will acquire a lock to resource.
Mutex and Semaphores sound similar, but the key difference lies in what kind of resource they are locking, and if you ever find yourself in a situation in which you need to decide what to protect.
In general, a mutex can be used to protect anything that doesn't have sort of execution on its own, such as files, sockeys, and other filesystem elements.
Semaphore can be used to protect execution in your programe itself such us shared code execution paths. This can be mutating functions or classes with shared state.
A rule of thumb is to always add a timeout whenever a semaphore is acquired and lest processes timeout if they take too long
철학자의 식탁에서 가장 쉬운 해결 방법은 각 포크에다가 세마포를 붙여주는 것이다. -> 상호배제는 해결되나 데드락 문제는 여전히 해결이 안되는 상태 (한명이 양쪽 포크쥐고 다른사람들이 왼쪽 포크 사용하기를 기다리는 상태)
데드락을 해결하기 위해서는 철학자 수를 제한하거나 양쪽 포크를 사용가능할 때 음식을 멋게 하거나 홀수 번째 철학자들만 폭크를 집게한다든가 등등이 있다. -> 위 방법들이 데드락은 해결하나 Starvation은 해결하지 못한다
History log should be locked at the writing operation.
Deadlocks and race conditions are very similar. The main difference, in deadlocks, is we have multiple process waiting for the other to finish, whreas in race onditions we have both processes writing data and corrupting it.
If you want to work with multithreading, you will end up creating multiple
NSThread
objects, and you are responsible for managing them all.
Each
NSThread
object had properties you can use to check the status of the thread, such asexecuting
,cancelled
, andfinished
.
The GCD saves you from doing a lot of work than
pthread
s andNSThread
s. With the GCD, you will never concer yourself managing threads manually.
//The pyramid of doom
func fetchUser() {
userApi.fetchUserData { userData in
DispatchQueue.main.async {
self.usernameLabel.text = userData.username
self.userApi.fetchFavoriteMovies(for: userData.id) { movies in
DispatchQueue.main.async {
self.userMovies = movies
}
}
}
}
}
When your work requires you to do more than one call that depends on the task of another, you code starts looking like a pyramid of doom.
Despites its drawback, the GCD is a very popular way to create concurrent and multithreaded code. It has a lot of protections in place already for you. So you don't hve to be an expert in the theory of multithreading to avoid making mistakes.
Sitting at a higher level than the GCD, the
NSOperation
APIs are a high-level tool to do multithreading.
//Multithreaded usage of the NSOperation APIs
func startCounting() {
/// We will need a queue for this.
let operationQueue = OperationQueue()
/// You can give your queue an optional name, if you need to identify it later.
operationQueue.name = "Counting queue"
/// This will just count from 1 to 10...
let from1To10 = BlockOperation {
for i in (1 ... 10) {
print(i)
}
}
/// ... and this from 11 to 20
let from11To20 = BlockOperation {
for i in (11 ... 20) {
print(i)
}
}
/// Add the operations to the queue
operationQueue.addOperation(from1To10)
operationQueue.addOperation(from11To20)
/// To ensure the program doesn't exit early while the operations are running.
operationQueue.waitUntilAllOperationsAreFinished()
Using these APIs is very simple. You begin by creating an
OperationQueue
. This queue's responsibility is to execute any ask you add to it.
// Ensure the numbers print in order. We do this before adding the operations to the queue.
from11To20.addDependency(from1To10)
/// Add the operations to the queue
operationQueue.addOperation(from1To10)
operationQueue.addOperation(from11To20)
It's worth to note that while you don't have management abilities with this framework, you can check the status of each operation(
isCancelled
,isFinished
, etc).
Using the NSOperation APIs is simple, as you have evidenced. It can still be a great tool when you have simple multithreaded needs.
@MainActor
func fetchUser() async {
let userData = await userApi.fetchUserData()
usernameLabel.text = userData.username
let movies = userApi.fetchMovies(for: userData.id)
userMovies = movies
}
The
@MainActor
will ensure that members updates within a function or class marked with it will run in the main thread.
Every call to an asunc function must be paired up with the
await
keyword at some point.await
itself has special semantics, and it's not just syntax to call async functions.
If during your code's execution the
await
keyword is encountered, you program can choose to suspend the currently executin function. For this reason, theawait
keyword is also commonly known as a suspension point.
Anything that happens after an
await
call is a continuation. We need to continue doing the work we were doing before we were suspended.
Using async/await does not save you from having to do UI work on the main thread, so if you need to update your UI, you need to defer that work to the main thread.
func fetchUserInfo() async throws -> UserInfo {
let url = URL(string: "https://www.andyibanez.com/fairesepages.github.io/books/async-await/user_profile.json")!
let session = URLSession.shared
let (userInfoData, response) = try await session.data(from: url)
let userInfo = try JSONDecoder().decode(UserInfo.self, from: userInfoData)
return userInfo
}
When executing reaches the
let (userInfoData, response) = try await session.data(from: url)
line, our code is suspended, and the thread it's executing on is free to do work.
@MainActor
class UserProfileViewModel: ObservableObject {/*..*/}
`@MainActor (global actor) will ensure that whenever a property is updated, it will happen on the main thread. This is important, because SwiftUI uses Observables to update the UI, and thus updating them needs to be done on the main thread.
func fetchUserInfo(
completionHandler: @escaping (_ userInfo: UserInfo?, _ error: Error?) -> Void
) {
let url = URL(string: "https://www.andyibanez.com/fairesepages.github.io/books/async-await/user_profile.json")!
let session = URLSession.shared
let dataTask = session.dataTask(with: url) { data, response, error in
if let error = error {
completionHandler(nil, error)
} else if let data = data {
do {
let userInfo = try JSONDecoder().decode(UserInfo.self, from: data)
completionHandler(userInfo, nil)
} catch {
completionHandler(nil, error)
}
}
}
dataTask.resume()
}
Continuations are useful, because not only will you be able to provide
async/await
versions of your own closure-based code, but also for third-party libraries and frameworks.
In short, a Continuation is whatever happens after an
async
call is finished.
Structured Concurrency follows the same idea as structured programming. It's all about writing code in the order we expect it to run ans seeing variables have a well-defined scope, and you can expect your code to not jump to other places that break the reading flow of your code.
func fetchData() async throws -> (userInfo: UserInfo, followingFollowersInfo: FollowerFollowingInfo) {
let userApi = UserAPI()
async let userInfo = userApi.fetchUserInfo()
async let followers = userApi.fetchFollowingAndFollowers()
return (try await userInfo, try await followers)
}
To run code concurrently, all you need to do is to tack the
async
keyword before alet
declaration. the res of your declaration looks like a normal swift variable initialization.
Imagine you have an app that allows users to download a dynamic number of images, or you want to apply filters to a variable number of images at once. In these case, we could need one taks, two tasks, 300 tasks, or more. For these situations, the async/await system provides us with Task Groups.
/// Downloads all the images in the array and returns an array of URLs to the local files
func download(serverImages: [ServerImage]) async throws -> [URL] {
var urls: [URL] = []
try await withThrowingTaskGroup(of: URL.self) { group in
for image in serverImages {
group.addTask(priority: .userInitiated) {
let imageUrl = try await self.download(image)
return imageUrl
}
}
for try await imageUrl in group {
urls.append(imageUrl)
}
}
return urls
}
One important thing to keep in mind is that tasks added to the group may not execute in the order they were submitted. If you want to use task groups, it's important you design your code in a way that makes the order irrelevant.
So We use taks groups when want to run multiple tasks at the same time, but we don't know the exact amount of concurrency involved, such as downloading multiple images from the netwok at the same time.
func authenticateAndFetchProfile() {
Task {
//...
let userProfile = try await fetchUserInfo()
//...
}
}
Task is an object that can store a value, an error, both, or neither. To create an asynchronous, unstructured task, we call Task and initialize it with a closure which is the code we want to run asynchronously.
You can have multiple
Task
objects, store them in variables, or even in collections.
func download(serverImages: [ServerImage]) {
for imageIndex in (0..<serverImages.count) {
let downloadTask = Task<Void, Never> {
do {
let imageUrl = try await download(serverImages[imageIndex])
self.images[imageIndex] = ImageDownload(status: .downloaded(imageUrl))
} catch {
self.images[imageIndex] = ImageDownload(status: .error(error))
}
}
images.append(ImageDownload(status: .downloading(downloadTask)))
}
}
This is where part of the magic will happen. We will start by iterating over all the ServerImages we have downloaded. For each server image, we will create an explicit Task.
The new concurrency system in Swift is driven by an underlying structure called the task tree. Like the names states, the task tree gives us a mental model of how concurrency works behind the scenes.
Tasks spawn child tasks and these child tasks inherit all these attributes from their parents.
func fetchData() async throws -> (userInfo: UserInfo, followingFollowersInfo: FollowerFollowingInfo) {
let userApi = UserAPI()
async let userInfo = userApi.fetchUserInfo()
async let followers = userApi.fetchFollowingAndFollowers()
return (try await userInfo, try await followers)
}
This needs to be repeated and it's very important you don't forget. Tasks are always created explicitly. One task can run multiple concurrent child tasks, with the same actor, priority, and taks local variables.
Another important aspect of the task tree you need to consider is that a parent task can only finish its work when all its children have finished their work.
If an error is thrown, it will be propagated up the task tree until it reaches the caller. Any other child tasks that are either awaiting or running concurrently will be marked as cancelled
Cancellation is cooperative. This means that the tasks will not be explicitly and automatically cancelled by anyone.