Hello folks,
I am consuming a C library (libssh2) in an iOS app of mine, and while it works great, I realized that my current design will not work well with multiple ssh streams but also that async could help me solve this problem, but I have not found much information on how to solve this.
Let me explain the challenge, and will present a simplified picture.
For the purpose of this discussion, the APIs that I need to use are not thread-safe, so I can not have two threads calling into them in parallel. This is not much of a problem, as I can use a queue for it.
To prevent blocking, invoking certain APIs will return the equivalent of a Unix syscall EAGAIN
, which means that the operation should be retried in the future (once more data is available). Before I tried to use multiple streams per connection, this would have been enough:
func readFile (path: String) -> String {
var ret = 0
var buffer = ""
repeat {
ret = libssh2_do_reading (path, &buffer);
} while ret == LIBSSH2_ERROR_EAGAIN
return buffer
}
The problem with this approach are two:
- it consumes the entire thread while waiting for data to be delivered - so no other operations on other channels can process any data, or send any data as the queue I have designated to call into libssh2 is in use
- It is effectively a busy loop, waiting for data from the network that might or might not arrive. Until data arrives to fulfill the request, that while loop will be killing glorious cycles from my precious iPhone. How can you ask an electron to be the last one to die for a pointless operation?
So I figured that maybe async could solve this problem for me, but I am approaching with my .NET baggage, and can’t seem to find my way around Swift’s async.
What I would like to do is to keep the async function suspended until I know that there is new data available, to avoid the busy loop, but to add insult to injury, I would want this to resume execution in its designated queue.
I figured I could write the above like this:
func readFile (path: String) async -> String {
var ret = 0
var buffer = ""
repeat {
ret = libssh2_do_reading (path, &buffer)
if ret == LIBSSH2_ERROR_EAGAIN {
Task.yield ()
}
} while ret == LIBSSH2_ERROR_EAGAIN
return buffer
}
This I imagine is a mild improvement, but the documentation for Task.yield
is merely that it will let other folks get in on the fun. What I would ideally want is something similar to .NET, where the function returns a Task that can be signaled externally for execution to be resumed.
This a spiritual version of what I would like to accomplish would be something like this:
var pingable_tasks [Task] = []
func readFile (path: String) async -> Task<String> {
var task: Task
task = Task {
var ret = 0
repeat {
ret = libssh2_do_reading (path)
if ret == LIBSSH2_ERROR_EAGAIN {
Task.suspend ()
}
} while ret == LIBSSH2_ERROR_EAGAIN
pingable_tasks.remove (task)
return buffer;
}
pingable_tasks.append (task)
return task
}
// Elsewhere, when we have received data from the network, I can ping all
// tasks to let them know they should try to get some data
func dataReceived () {
for task in pingable_tasks {
task.resume ()
}
}
Now, the above is incorrect, as I used Task {
to launch a new task, with no respect for the sanctity of calling libssh2 from a single thread at a time. I guess if worse comes to worst, I could wrap the entire libssh2 API in an actor, but that does feel a bit over the top. What I would love is to control for the Task to always be scheduled on a specific queue.