2

I've toyed around with Swift Playground and noticed the following issue:

The code below describes a series of object connected to one another in the following way:

objectC --> ObjectB -- weak ref to another C --> another C --> Object B etc..

Each objectC consists of

- a ref to a object B 
- a weak ref to a delegate => this one becomes nil!!

Each objectB consists of

- A var integer
- A weak ref to another object C

The code does the following:

objectC call a function, say run(), which will evaluate (objectB.weak_ref_to_another_C), and call objectB.weak_ref_to_another_C.run() in a serial Queue.

After calling .run() a couple of times, C's delegate mysteriously becomes nil....

Any idea what I'm doing wrong? To start the code, simply call test_recursive_serial() on Swift Playground.

let serialQueue = DispatchQueue(label: "myQueue");

public protocol my_protocol:class  {
    func do_something(ofValue:Int,completion:((Int) -> Void))
}

public class classA:my_protocol {

    public let some_value:Int;

    public init(value:Int){
        self.some_value = value;
    }
    public func do_something(ofValue:Int,completion:((Int) -> Void)) {
        print("A:\(some_value) in current thread \(Thread.current) is executing \(Thread.current.isExecuting)");
        if self.some_value == ofValue {
            completion(ofValue);
        }
    }

}

public class classB {

    public weak var jump_to_C:classC?;

    public var value:Int = 0;

}


public class classC {
    weak var delegate:my_protocol?{
        willSet {
            if (newValue == nil) { print("target set to nil") }
            else { print("target set to delegate") }
        }
    }

    var someB:classB?

    public func do_something_else() {
        print(self.delegate!)
    }

    public func do_another(withValue:Int,completion:((Int) -> Void)) {

    }

    public func run(completion:@escaping ((Int) -> Void)) {
        print("\(self.someB?.value)");
        assert(self.delegate != nil, "not here");
        if let obj = someB?.jump_to_C, obj !== self  {
            someB?.value += 1;
            print("\(someB!)")
            usleep(10000);
            if let value = someB?.value, value > 100 {
                completion(someB!.value);
            } else {
                serialQueue.async {
                    print("lauching...")
                    obj.run(completion: completion);
                }
            }
        }else{
            print("pointing to self or nil...\(someB)")
        }
    }
}


public func test_recursive_serial() {

    let my_a = classA(value:100);

    let arrayC:[classC] = (0..<10).map { (i) -> classC in
        let c = classC();
        c.delegate = my_a;
        return c;
    }

    let arrayB:[classB] = (0..<10).map { (i) -> classB in
        let b = classB();
        let ii = (i + 1 >= 10) ? 0 : i + 1;
        b.jump_to_C = arrayC[ii]
        return b;
    }

    arrayC.forEach { (cc) in

        cc.someB = arrayB[Int(arc4random())%arrayB.count];
    }

    arrayC.first!.run() { (value) in
        print("done!");
    }

}

Important note: if test_recursive_serial() content is directly called from the playground, that is not through a function, the problem doesn't appear.

Edit: You'll need to add 'PlaygroundPage.current.needsIndefiniteExecution = true' to the playground code.

Edit: Ok, I feel I need to add this. Big mistake on my side, test_recursive_serial() doesn't keep a reference on any of the called objects, so obviously, they all become nil after the code leaves the function. Hence the problem. Thanks to Guy Kogus for pointing that out.

Final edit: Adding this, in the hope it might help. Swift playground are great to test-drive code, but can sometime become very busy. Within the current issue, the solution requires to set the variables first, and then pass them to test_recursive_serial() which in turn adds to the chatty appearance of the playground. Here's another option to keep your code tidy and self-contained, while dealing with async functions of various flavours...

If you have an async task - one that doesn't fit into URL fetch -, say:

myObject.myNonBlockingTask(){ print("I'm done!"}

First, include XCTest at the top of your file.

import XCTest

then add the following:

func waitForNotificationNamed(_ notificationName: String,timeout:TimeInterval = 5.0) -> Bool {
    let expectation = XCTNSNotificationExpectation(name: notificationName)
    let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
    return result == .completed
}

finally, change your completion block to:

myObject.myNonBlockingTask(){
    print("I'm done!")
    let name = NSNotification.Name(rawValue: "foobar");
    NotificationCenter.default.post(name:name , object: nil)
}
XCTAssert(waitForNotificationNamed("foobar", timeout: 90));

the full playground code will look like:

public func my_function() {
    let somevar:Int = 123
    let myObject = MyClass(somevar);
    myObject.myNonBlockingTask(){
        print("I'm done!")
        let name = NSNotification.Name(rawValue: "foobar");
        NotificationCenter.default.post(name:name , object: nil)
    }
    XCTAssert(waitForNotificationNamed("foobar", timeout: 90));
}

Playground will wait on the notification before going any further, and also generate an exception if it times out. All locally created objects will remain valid until the execution completes.

Hope this helps.

Alex
  • 1,581
  • 1
  • 11
  • 27

1 Answers1

3

The main issue is that you're testing this in Playgrounds, which doesn't necessarily play nicely with multithreading. Following from this SO question, change the test_recursive_serial function to:

arrayC.first!.run() { (value) in
    print("done! \(value)")
    XCPlaygroundPage.currentPage.needsIndefiniteExecution = false
}

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

while XCPlaygroundPage.currentPage.needsIndefiniteExecution {

}

(You'll need to add import XCPlayground at the top of the code to make it work.)

If you don't add that code change, then my_a is released after you leave that function, which is why delegate becomes nil on the second call to run.

I also found that in run, if you don't call the completion closure in the else case like so:

public func run(completion:@escaping ((Int) -> Void)) {
    ...
    if let obj = someB?.jump_to_C, obj !== self  {
        ...
    }else{
        print("pointing to self or nil...\(someB)")
        completion(-1) // Added fallback
    }
}

Then the program gets stuck. By adding that it runs to the end, although I haven't actually worked out why.

Also, please get rid of all your ;s, this isn't Objective-C

Guy Kogus
  • 7,251
  • 1
  • 27
  • 32
  • Awesome! Indeed, serialQueue, so the code eventually leaves the function hence the reference is released, :p shame on me. My whole example is simply incorrect... woaaa. Thanks man for spending time on this non-issue, much appreciated. Cheers! – Alex Jan 08 '18 at 12:10