I am quite frustrated at the amount documentation that says explicitly you should use asynchronous code for networking.
I even read an article which outright states 'Networking is an inherently asynchronous operation.'
https://react-native.org/doc/network.html
So first there is a difference between background processing and asynchronous code.
For example, running code asynchronously doesn't necessarily mean it is on the background. For this we can actually use a background thread.
When you write an iOS app, and you have several view controllers, each accessing the same data which is downloaded by the Model, when you download the data for code asynchronously, you have a frustrating tangle of callbacks and asynchronous messages being passed throughout the application.
When I have multiple view controllers using the same data this poses a problem, how do I make sure I don't have the view controller which is opened accessing the data before it is downloaded? You might not be able to tell which controller was opened first so this poses an issue, how do you make sure they don't access the data until it is finished downloading?
I suppose you solve this using a completion handler and a model which then fires off a Key Value Observing notification calling the controller when it is finished downloading (a push model).
But what happens if the said controller is not loaded when the notification is posted, does this mean it never gets the data? Wouldn't it make more sense to use a pull model, so when the controller is loaded it can check if the data is available, if so how do you handle this with an async paradigm?
But any notification callback cannot access the outlying scope of the rest of the controller.
However I have written some entirely synchronous code which uses locks and semaphores. The model downloads the data synchronously in a background thread. The controller classes (if they are loaded) check the Model class to see if the data is available. Locks mean that the code cannot access the data if the data is not downloaded. The App signals when the data has finished downloading and while all controllers and models use the same shared synchronous DispatchQueue this prevents the controllers accessing the data arrays when they are empty or the data is being downloaded.
Async code commonly produces weakly coupled code that can't access the scope of the rest of the class and you have methods firing at different times and in different places in the app which i think is difficult to keep track of. So why is networking 'an inherently asynchronous operation'?
Can anyone provide sound scientific reasons why asynchronous code is better, or reasons why I should not do what I have done with synchronous code, and also methods of how you can make asynchronous code safer, less spaghetti like, easier to work with and easier to read?
CODE:
Table View Controller
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !fromSelectionCtrllr {
let downloader = Downloader.sharedInstance
let group = downloader.group
group.notify(queue: .main, execute: {
let defaults : UserDefaults = UserDefaults.standard
let firstLaunch = defaults.bool(forKey: "firstLaunch")
if firstLaunch {
self.arrayOfData = Model.sharedInstance.provideData()
} else {
self.arrayOfData = Model.sharedInstance.provideNewData()
}
for object in self.arrayOfData {
if let deviceName = object.chargeDeviceName {
let theSubscript = deviceName.prefix(1)
let theString = String(theSubscript)
if !self.sectionTitles.contains(theString) {
self.sectionTitles.append(theString)
}
} else {
self.sectionTitles.append("")
}
if let deviceName = object.chargeDeviceName {
let string = String(describing: deviceName.prefix(1))
var arry = self.chargingPointDict[string]
if arry == nil {
arry = []
}
arry?.append(object)
self.chargingPointDict.updateValue(arry!, forKey: string)
} else {
self.chargingPointDict[" "]?.append(object)
}
}
self.sectionTitles = self.removeDuplicates(array: self.sectionTitles)
self.sectionTitles = self.sectionTitles.sorted( by: { $0 < $1 })
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.myTableView.reloadData()
}
})
}
fromSelectionCtrllr = false
}
CellForRowAtIndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if (searchController.searchBar.text?.isEmpty)! {
if self.sectionTitles.isEmpty {
cell.textLabel?.text = "Nothing to display"
return cell
} else {
let mySectionIndex = self.sectionTitles[indexPath.section]
if mySectionIndex != "" {
let arrayOfPoints : [ChargingPoint] = self.chargingPointDict[mySectionIndex]!
let object : ChargingPoint = arrayOfPoints[indexPath.row]
cell.textLabel?.text = object.chargeDeviceName
return cell
} else {
return cell
}
}
} else {
let object : ChargingPoint = self.filteredPoints[indexPath.row]
cell.textLabel?.text = object.chargeDeviceName
return cell
}
}
Model Class
class Model: NSObject {
var currentChargingPointArray : [ChargingPoint] = []
var newChargingPointArray : [ChargingPoint] = []
var latitude : Double?
var longitude : Double?
var annotationArray : [ChargingPointAnnotation] = []
var newAnnotationArray : [ChargingPointAnnotation] = []
static let downloader = Downloader.sharedInstance
var savedRegion : MKCoordinateRegion? = nil
/* The model class is a singleton */
static let sharedInstance : Model = {
let instance = Model()
return instance
}()
fileprivate override init( ) {} //This prevents others from using the default '()' initializer for this class.
func setLocation(lat: Double, long: Double) {
self.latitude = lat
self.longitude = long
}
func returnData(array: Array<ChargingPoint>) {
currentChargingPointArray = []
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in array {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
currentChargingPointArray = unique
}
func returnNewData(array: Array<ChargingPoint>) {
newChargingPointArray = []
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in array {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
newChargingPointArray = unique
}
func provideData() -> [ChargingPoint] {
return currentChargingPointArray
}
func provideNewData() -> [ChargingPoint] {
return newChargingPointArray
}
func makeAnnotations() -> [ChargingPointAnnotation] {
let queue = DispatchQueue(label: "com.jackspacie.ChargeFinder", qos: .background, attributes: [])
queue.sync {
self.annotationArray = []
for chargingPoint in currentChargingPointArray {
let location = CLLocationCoordinate2D( latitude: chargingPoint.latitude!, longitude: chargingPoint.longitude!)
let annotation = ChargingPointAnnotation(location: location)
annotation?.title = chargingPoint.chargeDeviceName
annotation?.pointTitle = chargingPoint.chargeDeviceName
annotation?.chargingPoint = chargingPoint
self.annotationArray.append(annotation!)
}
}
return self.annotationArray
}
func makeNewAnnotations() -> [ChargingPointAnnotation] {
let queue = DispatchQueue(label: "com.jackspacie.ChargeFinder", qos: .background, attributes: [])
queue.sync {
self.newAnnotationArray = []
for chargingPoint in newChargingPointArray {
let location = CLLocationCoordinate2D( latitude: chargingPoint.latitude!, longitude: chargingPoint.longitude!)
let annotation = ChargingPointAnnotation(location: location)
annotation?.title = chargingPoint.chargeDeviceName
annotation?.pointTitle = chargingPoint.chargeDeviceName
annotation?.chargingPoint = chargingPoint
self.newAnnotationArray.append(annotation!)
}
}
return self.newAnnotationArray
}
Downloader Class
var group = DispatchGroup()
var model = Model.sharedInstance
/* The downloader class is a singleton */
static let sharedInstance : Downloader = {
let instance = Downloader()
return instance
}()
fileprivate override init() {} //This prevents others from using the default '()' initializer for this class.
func download(lat: Double, long: Double, dist: Int) {
func recursive(lat: Double, long: Double, dist: Int) {
var chargeDeviceArray : [ChargingPoint] = []
let url = URL(string: “https://www.blah.com/lat/\(lat)/long/\(long)/dist/\(dist)/")!
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if error != nil {
print("urlSession Error")
recursive(lat: lat, long: long, dist: dist)
return
} else {
guard let unwrappedData = data else { return }
do {
let jsonDict : [String: Any] = try JSONSerialization.jsonObject(with: unwrappedData, options: [] ) as! [String : Any]
let arrayOfDicts = jsonDict["ChargeDevice"] as? [[String: Any]]
for value in arrayOfDicts! {
let chargePoint = ChargingPoint()
// process data into objects.
chargeDeviceArray.append(chargePoint)
}
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in chargeDeviceArray {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
if self.model.currentChargingPointArray.isEmpty {
self.model.returnData(array: unique)
} else {
self.model.returnNewData(array: unique)
}
} catch {
print("json error: \(error)")
}
semaphore.signal()
}
//print(response)
}
task.resume()
semaphore.wait(timeout: .distantFuture)
}
self.group.enter()
let queue = DispatchQueue(label: "com.myapp.charge”, qos: .background, attributes: [])
queue.sync {
recursive(lat: lat, long: long, dist: dist)
}
self.group.leave()
}
In short, when it comes to synchronous networking there are very limited cases where you would prefer this. Why?
Because 'A synchronous request blocks the client until operation completes' and in general you don't want the client to freeze. The user can't do anything at that time since we are waiting for all the synchronous actions to complete before enabling the client.
When using asynchronous requests you can build UI, show spinners and depending on whether you already cached older data or have other functionalities, still let the user use your client.
When I have multiple view controllers using the same data this poses a problem, how do I make sure I don't have the view controller which
is opened accessing the data before it is downloaded? You might not be
able to tell which controller was opened first so this poses an issue,
how do you make sure they don't access the data until it is finished
downloading?
I suppose you solve this using a completion handler and a model which
then fires off a Key Value Observing notification calling the
controller when it is finished downloading (a push model).
But I ask this, what happens if the said controller is not loaded when
the notification is posted, does this mean it never gets the data?
Wouldn't it make more sense to use a pull model, so when the
controller is loaded it can check if the dat is available, if so how
do you handle this with an async paradigm?
Load UI-> Show Progress Dialog -> Perform ASYNC(LoadData,SetData intoController)-> DismissDialog
Idk if this is what you're refering
I really dont see an oportunity where you will prefer block UI Thread and freeze App until it loads the data synchronically
The article states that "networking is an inherently asynchronous operation" because networking is an inherently asynchronous operation.
Synchronous means "happening at the same time". Specifically in computing it means with reference to a specific time period or clock signal. For example, electrically, the CPU operates synchronously, which is why we talk about the clock speed of a computer.
Synchronous does not mean "blocking" although that is a common (mis)interpretation. In fact, Apple hasn't helped here with their function names. Technically they should be something like DispatchQueue.nonBlockingOperation() and DispatchQueue.blockingOperation() rather than async/sync.
Synchronous systems can operate at much higher speeds but require a very controlled environment, which is why you find synchronous operation at the core of a computer but not so much outside it.
Your code blocks a background queue waiting for the download to complete, but the download still completes asynchronously. If it were completing synchronously you wouldn't need the semaphore. You would know that the data would be available at the specified point in time (say 0.2 seconds from now or whatever).
Your Downloader class also still notifies the view controller about the available data asynchronously via the dispatch group notify.
From what I can see, you have added a bunch of complexity to the solution (By adding a dispatch group and a semaphore) and introduced the potential for deadlocks but you still have asynchronous code. You block the background queue waiting for the completion of the download, but you still notify the data consumers asynchronously.
The same outcome could be achieved using the standard delegation or completion handler patterns with much less complexity. If there are potentially multiple parties interested in knowing about the new data then you can use the NotificationCentre to "broadcast" that information.
By the way, loosely coupled code is generally considered more desirable.
Related
I'm trying to create some coroutines (async) in a loop . I want to start everything in parallel then wait for them all to finish before proceeding. The documentation provides the following example:
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
but this requires that all the class instantiations be known in advance. However with a varying number of instantiations this is not practical. As a work around I found that the following works:
given a mutable List of class objects from class MyObject and MyObject has a method called myDo()
private val mObjects = mutableListOf<MyObject>()
and ignoring error checking and assuming the list has 2 or more objects then the following works but it's kind of clunky and not very elegant
coroutineScope {
val pd = async { myObjects[0].myDo() }
val dds = mutableListOf(pd)
for (i in 1..numObjects - 1) {
dds.add(async {mObjects[i].myDo() })
}
val nds = dds.toList()
nds.awaitAll()
}// end coroutineScope
What I'd hope to do was something like
val dds = mutableListOf<Job>()
for (i in 0..numObjects - 1) {
dds.add(async {mObjects[i].myDo() })
}
val nds = dds.toList()
nds.awaitAll()
but this doesn't work as the async result is a
Deferred<out T> : Job
interface not a Job interface. The problem with this is in the line
val dds = mutableListOf<Job>()
I don't know what to use in place of Job. That is, for async what is T?
Any help or suggestions would be appreciated
T in this case is whatever type myDo() returns.
I think you are overcomplicating it by creating the extra MutableLists. You can do it like this:
val results = coroutineScope {
mObjects.map { obj ->
async { obj.myDo() }
}.awaitAll()
}
results will be a List<MyDoReturnType>.
Edit: I just realized, since it wasn't obvious to you that the type of a Deferred is whatever the async lambda returns, maybe it's because myDo() doesn't return anything (implicitly returns Unit). If that's the case, you should use launch instead of async. The only difference between them is that async's lambda returns something and launch's doesn't. Deferred inherits from Job because a Deferred is a Job with a result. If myDo() doesn't return anything, your code should look like the following, with no result.
coroutineScope {
for (obj in mObjects) launch { obj.myDo() }
}
The answer from TenFour04 provided the key to my answer The following code works for me
coroutineScope {
val dds = mutableListOf<Deferred<Unit>>()
for (item in mObjects) { dds.add(async {item.myDo() }) }
val nds = dds.toList()
nds.awaitAll()
}
Am I stupid!!! or what. After I figured it out, the answer is almost trivial. The best solution I found is
private val mObjects = mutableListOf<MyObject>()
coroutineScope {val deferreds = listOf(mObjects.size){async{mObjects[it].myDo()}}
deferreds.awaitAll()
}// end coroutineScope
I like this better than the map solution as it doesn't create an intermediate Pair set
I have still a little bit of trouble putting all information together about the thread-safety of using coroutines to launch network requests.
Let's say we have following use-case, there is a list of users we get and for each of those users, I will do some specific check which has to run over a network request to the API, giving me some information back about this user.
The userCheck happens inside a library, which doesn't expose suspend functions but rather still uses a callback.
Inside of this library, I have seen code like this to launch each of the network requests:
internal suspend fun <T> doNetworkRequest(request: suspend () -> Response<T>): NetworkResult<T> {
return withContext(Dispatchers.IO) {
try {
val response = request.invoke()
...
According to the documentation, Dispatchers.IO can use multiple threads for the execution of the code, also the request function is simply a function from a Retrofit API.
So what I did is to launch the request for each user, and use a single resultHandler object, which will add the results to a list and check if the length of the result list equals the length of the user list, if so, then all userChecks are done and I know that I can do something with the results, which need to be returned all together.
val userList: List<String>? = getUsers()
val userCheckResultList = mutableListOf<UserCheckResult>()
val handler = object : UserCheckResultHandler {
override fun onResult(
userCheckResult: UserCheckResult?
) {
userCheckResult?.let {
userCheckResultList.add(
it
)
}
if (userCheckResultList.size == userList?.size) {
doSomethingWithResultList()
print("SUCCESS")
}
}
}
userList?.forEach {
checkUser(it, handler)
}
My question is: Is this implementation thread-safe? As far as I know, Kotlin objects should be thread safe, but I have gotten feedback that this is possibly not the best implementation :D
But in theory, even if the requests get launched asynchronous and multiple at the same time, only one at a time can access the lock of the thread the result handler is running on and there will be no race condition or problems with adding items to the list and comparing the sizes.
Am I wrong about this?
Is there any way to handle this scenario in a better way?
If you are executing multiple request in parallel - it's not. List is not thread safe. But it's simple fix for that. Create a Mutex object and then just wrap your operation on list in lock, like that:
val lock = Mutex()
val userList: List<String>? = getUsers()
val userCheckResultList = mutableListOf<UserCheckResult>()
val handler = object : UserCheckResultHandler {
override fun onResult(
userCheckResult: UserCheckResult?
) {
lock.withLock {
userCheckResult?.let {
userCheckResultList.add(
it
)
}
if (userCheckResultList.size == userList?.size) {
doSomethingWithResultList()
print("SUCCESS")
}
}
}
}
userList?.forEach {
checkUser(it, handler)
}
I have to add that this whole solution seems very hacky. I would go completely other route. Run all of your requests wrapping those in async { // network request } which will return Deferred object. Add this object to some list. After that wait for all of those deferred objects using awaitAll(). Like that:
val jobs = mutableListOf<Job>()
userList?.forEach {
// i assume checkUser is suspendable here
jobs += async { checkUser(it, handler) }
}
// wait for all requests
jobs.awaitAll()
// After that you can access all results like this:
val resultOfJob0 = jobs[0].getCompleted()
I'm investigating the use of Kotlin Flow within my current Android application
My application retrieves its data from a remote server via Retrofit API calls.
Some of these API's return 50,000 data items in 500 item pages.
Each API response contains an HTTP Link header containing the Next pages complete URL.
These calls can take up to 2 seconds to complete.
In an attempt to reduce the elapsed time I have employed a Kotlin Flow to concurrently process each page
of data while also making the next page API call.
My flow is defined as follows:
private val persistenceThreadPool = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
private val internalWorkWorkState = MutableStateFlow<Response<List<MyPage>>?>(null)
private val workWorkState = internalWorkWorkState.asStateFlow()
private val myJob: Job
init {
myJob = GlobalScope.launch(persistenceThreadPool) {
workWorkState.collect { page ->
if (page == null) {
} else managePage(page!!)
}
}
}
My Recursive function is defined as follows that fetches all pages:-
private suspend fun managePages(accessToken: String, response: Response<List<MyPage>>) {
when {
result != null -> return
response.isSuccessful -> internalWorkWorkState.emit(response)
else -> {
manageError(response.errorBody())
result = Result.failure()
return
}
}
response.headers().filter { it.first == HTTP_HEADER_LINK && it.second.contains(REL_NEXT) }.forEach {
val parts = it.second.split(OPEN_ANGLE, CLOSE_ANGLE)
if (parts.size >= 2) {
managePages(accessToken, service.myApiCall(accessToken, parts[1]))
}
}
}
private suspend fun managePage(response: Response<List<MyPage>>) {
val pages = response.body()
pages?.let {
persistResponse(it)
}
}
private suspend fun persistResponse(myPage: List<MyPage>) {
val myPageDOs = ArrayList<MyPageDO>()
myPage.forEach { page ->
myPageDOs.add(page.mapDO())
}
database.myPageDAO().insertAsync(myPageDOs)
}
My numerous issues are
This code does not insert all data items that I retrieve
How do complete the flow when all data items have been retrieved
How do I complete the GlobalScope job once all the data items have been retrieved and persisted
UPDATE
By making the following changes I have managed to insert all the data
private val persistenceThreadPool = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
private val completed = CompletableDeferred<Int>()
private val channel = Channel<Response<List<MyPage>>?>(UNLIMITED)
private val channelFlow = channel.consumeAsFlow().flowOn(persistenceThreadPool)
private val frank: Job
init {
frank = GlobalScope.launch(persistenceThreadPool) {
channelFlow.collect { page ->
if (page == null) {
completed.complete(totalItems)
} else managePage(page!!)
}
}
}
...
...
...
channel.send(null)
completed.await()
return result ?: Result.success(outputData)
I do not like having to rely on a CompletableDeferred, is there a better approach than this to know when the Flow has completed everything?
You are looking for the flow builder and Flow.buffer():
suspend fun getData(): Flow<Data> = flow {
var pageData: List<Data>
var pageUrl: String? = "bla"
while (pageUrl != null) {
TODO("fetch pageData from pageUrl and change pageUrl to the next page")
emitAll(pageData)
}
}
.flowOn(Dispatchers.IO /* no need for a thread pool executor, IO does it automatically */)
.buffer(3)
You can use it just like a normal Flow, iterate, etc. If you want to know the total length of the output, you should calculate it on the consumer with a mutable closure variable. Note you shouldn't need to use GlobalScope anywhere (ideally ever).
There are a few ways to achieve the desired behaviour. I would suggest to use coroutineScope which is designed specifically for parallel decomposition. It also provides good cancellation and error handling behaviour out of the box. In conjunction with Channel.close behaviour it makes the implementation pretty simple. Conceptually the implementation may look like this:
suspend fun fetchAllPages() {
coroutineScope {
val channel = Channel<MyPage>(Channel.UNLIMITED)
launch(Dispatchers.IO){ loadData(channel) }
launch(Dispatchers.IO){ processData(channel) }
}
}
suspend fun loadData(sendChannel: SendChannel<MyPage>){
while(hasMoreData()){
sendChannel.send(loadPage())
}
sendChannel.close()
}
suspend fun processData(channel: ReceiveChannel<MyPage>){
for(page in channel){
// process page
}
}
It works in the following way:
coroutineScope suspends until all children are finished. So you don't need CompletableDeferred anymore.
loadData() loads pages in cycle and posts them into the channel. It closes the channel as soon as all pages have been loaded.
processData fetches items from the channel one by one and process them. The cycle will finish as soon as all the items have been processed (and the channel has been closed).
In this implementation the producer coroutine works independently, with no back-pressure, so it can take a lot of memory if the processing is slow. Limit the buffer capacity to have the producer coroutine suspend when the buffer is full.
It might be also a good idea to use channels fan-out behaviour to launch multiple processors to speed up the computation.
As part of my ML project, I want to generate training data for analyzing the face of multiple individuals from different images using the face detector Google Firebase ML-Kit Face detection library. I created a very simple service class to encapsulate the initialization and start the process of face detection:
class FaceDetectorService(private val act: MainActivity) {
private var opts: FirebaseVisionFaceDetectorOptions? = null
private var detector: FirebaseVisionFaceDetector? = null
init {
FirebaseApp.initializeApp(act)
opts = FirebaseVisionFaceDetectorOptions.Builder()
.setPerformanceMode(FirebaseVisionFaceDetectorOptions.ACCURATE)
.setLandmarkMode(FirebaseVisionFaceDetectorOptions.NO_LANDMARKS)
.setClassificationMode(FirebaseVisionFaceDetectorOptions.NO_CLASSIFICATIONS)
.setContourMode(FirebaseVisionFaceDetectorOptions.ALL_CONTOURS)
.build()
detector = FirebaseVision.getInstance()
.getVisionFaceDetector(opts!!)
}
suspend fun analyzeAsync(cont: Context, uri: Uri) : Pair<String, Task<List<FirebaseVisionFace>>> {
val image = FirebaseVisionImage.fromFilePath(cont, uri)
// this is for the UI thread
withContext(Main){
act.addItemToAnalyze(uri.lastPathSegment)
}
// return the filename too
return Pair(uri.lastPathSegment, detector!!.detectInImage(image))
}
}
The function detector!!.detectInImage (FirebaseVisionImage.detectInImage ) returns a Task that represents async operations.
In the onResume() function of my MainActivity, inside a CoroutineScope, I fire up the lib and start iterating over the images converting them to an Uri first then passing it to the face detector:
CoroutineScope(IO).launch {
val executeTime = measureTimeMillis {
for (uri in uris){
val fileNameUnderAnalysis = uri.lastPathSegment
//val tsk = withContext(IO) {
// detector!!.analyzeAsync(act, uri)
//}
val tsk = detector!!.analyzeAsync(act, uri)
tsk.second.addOnCompleteListener { task ->
if (task.isSuccessful && task.result!!.isNotEmpty()) {
try {
// my best
} catch (e: IllegalArgumentException) {
// fire
}
} else if (task.result!!.isEmpty()) {
// not today :(
}
}
tsk.second.addOnFailureListener { e ->
// on error
}
}
}
Log.i("MILLIS", executeTime.toString())
}
Now, although my implementation runs concurrently (that is, starting at the same time), what I actually want is to run them in parallel (running in the same time depending on the number of threads, which is 4 in my case on an emulator), so my goal would be to take the number of available threads and assign an analysis operation to each of them quartering the execution time.
What I tried so far is, inside the CoroutineScope(IO).launch block, encapsulating the call to the library in a task:
val tsk = async {
detector!!.analyzeAsync(act, uri)
}
val result = tsk.await()
and a job:
val tsk = withContext(IO) {
detector!!.analyzeAsync(act, uri)
}
but the async operations I manually start always last only as long as the Firebase tasks are started, not waiting for the inner task to run to completion. I also tried adding different withcontext(...) and ...launch {} variations inside the class FaceDetectorService, but to no avail.
I'm obviously very new to kotlin coroutines, so I think I'm missing something very basic here, but I just cannot wrap my head around it.
(PS: please do not comment on the sloppiness of the code, this is just a prototype :) )
analyzeAsync() is a suspend fun and also returns a future-like Task object. Instead it should return the result of Task.await(), which you can easily implement basically by factoring out your addOnCompleteListener call:
suspend fun <T> Task<T>.await(): T = suspendCancellableCoroutine { cont ->
addOnCompleteListener {
val e = exception
when {
e != null -> cont.resumeWithException(e)
isCanceled -> cont.cancel()
else -> cont.resume(result)
}
}
}
An optimzied version is available in the kotlinx-coroutines-play-services module).
Since the face detection API is already async, it means the thread you call it on is irrelevant and it handles its computation resources internally. Therefore you don't need to launch in the IO dispatcher, you can use Main and freely do GUI work at any point, with no context switches.
As for your main point: I couldn't find explicit details on it, but it is highly likely that a single face detection call already uses all the available CPU or even dedicated ML circuits that are now appearing in smartphones, which mean there's nothing to parallelize from the outside. Just a single face detection request is already getting all the resources working on it.
I have the following ArrayList of File.
var a = ArrayList<File>()
var b = ArrayList<File>()
var c = ArrayList<File>()
var d = ArrayList<File>()
var e = ArrayList<File>()
Once the application has started the above ArrayList will be filed with more than 144,000 files. The total size all these combined would nearly 3.5 GB. I want to sort them by lastModified() or length() within in a second and update the modified ArrayList into RecyclerView.
For ease of sorting I have made above ArrayList into a Array<ArrayList<File>> as follows :
val mList = arrayOf(a,b,c,d,e)
To speed up things I do everything in background thread. Code :
doAsync {
mList.forEach{ index ->
index.sortByDescending { it.lastModified() }
}
activityUiThread {
setRecyclerViewAdapter() // Update RecyclerView with new sorted files
}
}
Link to the library I used sort files do in background thread : https://github.com/Kotlin/anko
The above code takes nearly 3-5 seconds to execute. I want this to be done within a second. How to solve this issue ? I am doing this for android.
If needed Im ready to change the API to do the background task
I want this to be done within a second
Short: it is impossible in general case. File System (on Android or on any other OS) can be overloaded, so this operation can pause your application sometimes. Please note this.
However you can speedup the code by using the following algorithm:
Read file metadata in parallel
Sort files via these results.
Please use example below. Please note that it does a lot of parallel IO operations.
Benefits of this solution:
Metadatas are read from the separate context (which has thread limit, to avoid IO overuse)
Metadata of file X is read only one time.
Sorting algorithm works with operating memory only, e.g. it use ready data, which reduces IO access.
suspend fun sortFiles(files: Iterable<File>): List<File> {
val metadataReadTasks: List<Deferred<FileWithMetadata>> = withContext(Dispatchers.IO)
{
files.map { file ->
async {
FileWithMetadata(file)
}
}
}
val metadatas: List<FileWithMetadata> = metadataReadTasks.awaitAll()
return metadatas
.sorted()
.map {
it.file
}
}
private class FileWithMetadata(
val file: File
) : Comparable<FileWithMetadata> {
private val lastModified = file.lastModified()
private val length = file.length()
override fun compareTo(other: FileWithMetadata): Int {
return when (other.length) {
this.length -> other.lastModified.compareTo(this.lastModified)
else -> other.length.compareTo(this.length)
}
}
}