Reputation: 105
I have some weird example related to Swift concurrency:
First, we have this code, and I am using Swift 6
typealias Reply = (Any) -> Void
func callReply1(reply: @escaping Reply) {
Task {
reply("hello")
}
}
In the above code, callReply
is a global non-isolated function.
This fails to compile as expected, because reply is not Sendable
:
Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
Then I mark reply to be nonisolated(unsafe)
:
func callReply2(reply: @escaping Reply) {
nonisolated(unsafe) let unsafeReply = reply
Task {
unsafeReply("hello")
}
}
I got the same error.
However, weirdly, if I mark the Task as @MainActor
, it worked:
func callReply3(reply: @escaping Reply) {
nonisolated(unsafe) let unsafeReply = reply
Task { @MainActor in
unsafeReply("hello")
}
}
This is very strange - because "non-isolated" means it can be called in any isolation domain.
I wonder why callRely2
fails to compile but callReply3
is fine?
Also is it possible to make callReply2
work without using @MainActor
?
Note that in real production code, I can't change the reply
type to Sendable
since it's an API from Objective-C code owned by another team that I am not allowed to change.
Edit: As requested by the comment, here's a more complete reproducible code:
Firstly, I have this objc API that I am not allowed to change. The code looks like this (with some details removed):
// Foo.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^Reply)(id data);
typedef void (^Handler)(Reply reply);
@interface Foo : NSObject
- (id)initWithQueue: (dispatch_queue_t)queue;
- (void)setupHandler: (Handler)handler;
@end
NS_ASSUME_NONNULL_END
// Foo.m
#import "Foo.h"
@implementation Foo {
dispatch_queue_t _queue;
}
- (id)initWithQueue: (dispatch_queue_t)queue {
if ((self = [super init])) {
_queue = queue;
}
return self;
}
- (void)setupHandler:(Handler)handler {
dispatch_async(_queue, ^{
handler(^(id data){
NSLog(@"data is %@", data);
});
});
}
@end
This code simply calls handler
in the specified queue.
Then I have 2 existing async APIs for me to use. Again, not owned by me, so I can't change:
@MainActor
func SomeMainIsolatedAPI() async -> String {
return "hello"
}
func SomeNonIsolatedAPI() async -> String {
return "hello"
}
My job is to make sure this works:
func runMainSetup() {
let foo = Foo(queue: .main)
foo.setupHandler { reply in
Task { @MainActor in
let data = await SomeMainIsolatedAPI()
reply(data)
}
}
}
func runBackgroundSetup() {
let foo = Foo(queue: .global())
foo.setupHandler { reply in
Task {
let data = await SomeNonIsolatedAPI()
reply(data)
}
}
}
But it fails to compile because reply
is not sendable.
The linked question does not answer my question. This is more about asking for a solution, while the other question is more about weird behavior in "@preconcurrency import".
Upvotes: 2
Views: 82