Swift Concurrency: Feedback Wanted!

If you have a correctly-scoped unsafe pointer, yes, that would be fine. It would be quite reasonable to build a parallel map with Array(unsafeInitializedCapacity:initializingWith:), for example. Unsafe pointers are not atomic, and they have no memory-ordering effects, but everything that happens on a task happens-before the conclusion of awaiting it, so as long as different subtasks don't touch the same element there's no problem.

1 Like

I see. It's probably how splitting access would've worked in non-concurrent access too. This looks very similar to Rust's splitting borrow.

Can you not now wrap an Array in a var of an actor instance to do this sort of thing in a thread-safe manner? I thought that was the whole point. Maybe I’m not getting it.

1 Like

actor is a ref-type, so you can just capture a let instance of one by-value.

Hello,

I posted a seperate topic before I noticed this thread, which is the correct place to ask I suppose.
I am starting to play with the new Swift Concurrency features and so far I love them a lot.
I tried to convert some logic that used dispatch queues to the new Actor types. There is one situation where I have no clear clue how to translate.

I use DispatchWorkItem and I dispatch work on a custom queue object using asyncAfter as well as cancel when I want to cancel. Additionally I use custom queue objects as schedulers for combine operations.
How can I achieve similar functionaliy within an Actor's context?

Explicitly cancelling tasks can be done if you create an unstructured task via Task{ call your actor here } and keeping that task handle around. You can call cancel() on it whenever you want. Do note that cancellation is co-operative, so your code would have to be checking and reactig to cancellation.

Custom executors is something we're thinking about, but are not shipping today, see here Support custom executors in Swift concurrency. That'd eventually address your question for more control over where exactly things execute. For now you can generally think of a single actor "as if" it had it's own mailbox queue (they don't, they share a global pool, but semantically it is "as if" they did.) So that's not quite a thing yet in Swift Concurrency.

You may also enjoy the https://developer.apple.com/wwdc21/10134 Explore structured concurrency in Swift talk which discusses a little bit about cancellation and tasks in general - which is at which level cancelation happens (not really at the actor level per se).

Depending on more specific examples maybe we can offer some advice though.

1 Like

Thank you for your feedback.
Here is an example. I want an actor to encapsulate CoreBluetooth.
When initializing a CBCentralManager you can set the dispatch queue which it is used to dispatch all delegate callbacks on:
- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate queue:(nullable dispatch_queue_t)queue;
or on my call site
CBCentralManager(delegate: self, queue: myBleQueue)
Additionally I use asyncAfter on that same queue to implement custom polling, timeouts etc.
How would I go about converting this class which we enforce isolation manually (using my myBleQueue) to an Actor?

Of course. This is what actor are designed to do, but you no longer are accessing the array from multiple threads, but only from the actor thread*, and all accesses must be thought awaitable calls.

*: I know that "thread" is not accurate here as actor may not even have a dedicated thread and may guarantee safe concurrent access by any other mean.

To be clear, I meant an Array var member of an actor, not a var reference to an actor.

I'm try to play with async/await and actor. The API for async/wait is really clear and neat read.
However I got a crash with Task.sleep(1000) (if I change time from 1000 to 100 than everything is fine). Please let me know if I used the API in a wrong way

The downloader actor is:

actor ImageDownloader {
  private var caches: [URL: UIImage] = [:]
  func image(from url: URL) async throws -> UIImage? {
    if let image = caches[url] {
      print("---> Return image from cache for url:\(url.absoluteString)")
      return image
    }
    do {
      let image = try await downloadImage(from: url)
      caches[url] = image
      print("---> Return image after download for url:\(url.absoluteString)")
      return image
    } catch {
      caches[url] = nil
      throw error
    }
  }
  
  private func downloadImage(from url: URL) async throws -> UIImage {
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
      throw NSError(domain: "download image", code: 0, userInfo: ["message": "Invalid server response"])
    }
    guard let image = UIImage(data: data) else {
      throw NSError(domain: "download image", code: 0, userInfo: ["message": "bad image data"])
    }
    return image
  }
}

Thanks, we're aware of this crash -- we'll look into it!

Sorry for the trouble, but for now in Beta 1 please avoid using the asynchronous Task.sleep :pray:

8 Likes

There's a workaround for the short term: set SWIFT_DEBUG_CONCURRENCY_ENABLE_COOPERATIVE_QUEUES=NO in the environment when launching your process.

Doug

14 Likes

Hmm how do I initialize a global actor from the Main actor?

Whenever i try to do that I get this error Call to global actor 'TestGlobalActor'-isolated initializer 'init()' in a synchronous main actor-isolated context
Is there some pattern for initializing that I am not following?

Thanks ;)

You can call the initializer from asynchronous code, and await the result. Or you could make the initializer nonisolated so it can be called from anywhere.

Doug

2 Likes

Should this piece of code at least warn that there is no handler for the thrown error within the async block?

// main.swift
struct StringError: Error {
    let m: String
    init(_ m: String) { self.m = m }
}

async {
    print("hello")
    throw StringError("Hello, error!")
    print("goodbye")
}

dispatchMain()

currently this compiles fine warning free and runs with no error (hangs as expected by dM())

This is fine though, the returned value is Task<Void, Error> (well Task.Handle<Void, Error> in Beta 1), and you dropped it. This is as designed really.

The async block returns a Task (handle) and the handle will contain the error.

See the definition:

public func async<T>(
  priority: TaskPriority? = nil,
  @_inheritActorContext @_implicitSelfCapture operation: __owned @Sendable @escaping () async throws -> T
) -> Task<T, Error>

so as you can see the closure is throwing, and the thrown error will simply be caught in the Task.

It’d be rethrown if you do:

let task = async { throw … }

try await task.get() // rethrows the error

So this is as-designed really.


Would the argument be that the throwing task creations should not be @discardableResult? Not sure about that hm...

A small piece of feedback about async let. The concurrency section of the language book starts out by drawing an important distinction between "asynchronous" and "parallel".

Yet the main "run some code in parallel" mechanism is spelled async let.

I wonder whether something like parallel let was considered.

In general (by no means just in Swift) there seems to be some imprecision in use of the two terms (as well as "concurrent"), but I am heartened to see Swift trying to make a cleaner distinction, if only in documentation at the moment.

8 Likes

Thanks for the suggestion, Matt!

I (personally) don't think that would be the right word to use -- it depends on the runtime if this will actually be parallel or not. There exist single-threaded runtimes on which swift runs perfectly fine, so we don't want to over-promise that this indeed is parallel when in fact it might not be. Parallelism is a runtime property really, not something we can statically promise in the language.

We had a lot of naming iterations on the feature but we always end up back at async as the least confusing / least ugly of all the ones we found so far...

3 Likes

This doesn't compile but I would like it to.

func f() async -> String? { "blerg" }
func g() async -> String { "nertz"}

func h() async -> String { await f() ?? await g() }
// error: 'async' call in an autoclosure that does not support concurrency
2 Likes

I started playing with concurrency features since about a month ago, and often find my adoption of it blocked by incomplete support of async/await in the standard library.

For example, optional-coalescing operator doesn't work:

actor Menu {
	init() { ... }
	var popularFood: Food? // optional because it can run out
	var unpopularFood: Food
}

func orderFood(from menu: Menu) {
	let selectedFood =  await menu.popularFood ?? await menu.unpopularFood
	// error: 'await' cannot appear to the right of a non-assignment operator
	// error: 'await' in an autoclosure that does not support concurrency
	// error: Actor-isolated property 'unpopularFood' can only be referenced from inside the actor
	return selectedFood
}

It's also difficult to conform types to protocols because protocol requirements (except for AsyncSequence's) are all synchronous.

For example, conformance to Decodable doesn't work, if there is some async work during the initialization:

actor PublisherDatabase {
	var authors: [Person] = []

	func author(name: String) -> Person {
		if let author = authors.first(where: { $0.name == name } ) {
			return author
		} else {
			let newAuthor = Person(name: name)
			authors.append(newAuthor)
			return newAuthor
		}
	}
}

struct Book: Decodable {
	let title: String
	let author: Person

	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		title = try container.decode(String.self, forKey: .title)
		let authorName = try container.decode(String.self, forKey: .authorName)
		author = await database.author(name: authorName)
		// error: 'await' in a function that does not support concurrency
		// fix-it: Add 'async' to function 'init(from:)' to make it asynchronous
		// error: Actor-isolated instance method 'author(name:)' can only be referenced from inside the actor
	}

	enum CodingKeys: CodingKey {
		case title
		case authorName
	}
}

I guess things like ?? can be improved by introducing reasync, and things like Decodable conformance can be improved by upgrading sync requirements to async ones. I don't know if they have any effect on ABI stability though.

Terms of Service

Privacy Policy

Cookie Policy