iSO development skills—Notification and multithreading

iSO development skills—Notification and multithreading

A few days ago, I discussed with my colleagues the issue of forwarding Notification under multi-threading, so I would like to sort it out.

Let's take a look at the official documentation first, which is written like this:

In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, which may not be the same thread in which an observer registered itself.

Translated:

In a multi-threaded application, Notification is forwarded in the thread in which it is posted, not necessarily in the thread in which the observer is registered.

That is to say, the sending and receiving of Notification are all done in the same thread. To illustrate this, let's take a look at an example:

Code Listing 1: Sending and processing Notification

  1. @implementation ViewController
  2.  
  3. - ( void )viewDidLoad {
  4. [ super viewDidLoad];
  5.  
  6. NSLog(@ "current thread = %@" , [NSThread currentThread]);
  7.  
  8. [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector (handleNotification:) name:TEST_NOTIFICATION object:nil];
  9.  
  10. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
  11.  
  12. [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
  13. });
  14. }
  15.  
  16. - ( void )handleNotification:(NSNotification *)notification
  17. {
  18. NSLog(@ "current thread = %@" , [NSThread currentThread]);
  19.  
  20. NSLog(@ "test notification" );
  21. }
  22.  
  23. @end  

The output is as follows:

  1. 2015-03-11   22 : 05 : 12.856 test[ 865 : 45102 ] current thread = <NSThread: 0x7fbb23412f30 >{number = 1 , name = main}
  2. 2015-03-11   22 : 05 : 12.857 test[ 865 : 45174 ] current thread = <NSThread: 0x7fbb23552370 >{number = 2 , name = ( null )}
  3. 2015-03-11   22 : 05 : 12.857 test[ 865 : 45174 ]test notification

As you can see, although we registered the notification observer in the main thread, the Notification posted in the global queue is not processed in the main thread. Therefore, we need to pay attention at this time. If we want to process UI-related operations in the callback, we need to ensure that the callback is executed in the main thread.

At this point, there is a problem. If our Notification is posted in the secondary thread, how can we process this Notification in the main thread? Or to put it another way, if we want the post thread of a Notification to be different from the forwarding thread, what should we do? Let's see what the official document says:

For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.

Here we talk about "redirection", which means that we capture these distributed notifications in the default thread where Notification is located, and then redirect them to the specified thread.

One way to implement redirection is to customize a notification queue (note, not an NSNotificationQueue object, but an array) and let this queue maintain the Notifications we need to redirect. We still register a notification observer as usual. When a Notification comes, we first check whether the thread that posts this Notification is the thread we expect. If not, we store this Notification in our queue and send a signal to the expected thread to tell this thread that it needs to process a Notification. After receiving the signal, the specified thread removes the Notification from the queue and processes it.

The official documentation has given sample code, which is borrowed here to test the actual results:

Code Listing 2: Posting and forwarding a Notification in different threads

  1. @interface ViewController () <NSMachPortDelegate>
  2.  
  3. @property (nonatomic) NSMutableArray *notifications; // Notification queue  
  4. @property (nonatomic) NSThread *notificationThread; // expected thread  
  5. @property (nonatomic) NSLock *notificationLock; // Lock object used to lock the notification queue to avoid thread conflicts  
  6. @property (nonatomic) NSMachPort *notificationPort; // Communication port used to send signals to the desired thread  
  7.  
  8. @end  
  9.  
  10. @implementation ViewController
  11.  
  12. - ( void )viewDidLoad {
  13. [ super viewDidLoad];
  14.  
  15. NSLog(@ "current thread = %@" , [NSThread currentThread]);
  16.  
  17. // Initialization  
  18. self.notifications = [[NSMutableArray alloc] init];
  19. self.notificationLock = [[NSLock alloc] init];
  20.  
  21. self.notificationThread = [NSThread currentThread];
  22. self.notificationPort = [[NSMachPort alloc] init];
  23. self.notificationPort.delegate = self;
  24.  
  25. // Add the port source to the run loop of the current thread  
  26. // When a Mach message arrives and the receiving thread's run loop is not running, the kernel saves the message until the next time it enters the run loop  
  27. [[NSRunLoop currentRunLoop] addPort:self.notificationPort
  28. forMode:(__bridge NSString *)kCFRunLoopCommonModes];
  29.  
  30. [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector (processNotification:) name:@ "TestNotification" object:nil];
  31.  
  32. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
  33.  
  34. [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
  35.  
  36. });
  37. }
  38.  
  39. - ( void )handleMachMessage:( void *)msg {
  40.  
  41. [self.notificationLock lock];
  42.  
  43. while ([self.notifications count]) {
  44. NSNotification *notification = [self.notifications objectAtIndex: 0 ];
  45. [self.notifications removeObjectAtIndex: 0 ];
  46. [self.notificationLock unlock];
  47. [self processNotification:notification];
  48. [self.notificationLock lock];
  49. };
  50.  
  51. [self.notificationLock unlock];
  52. }
  53.  
  54. - ( void )processNotification:(NSNotification *)notification {
  55.  
  56. if ([NSThread currentThread] != _notificationThread) {
  57. // Forward the notification to the correct thread.  
  58. [self.notificationLock lock];
  59. [self.notifications addObject:notification];
  60. [self.notificationLock unlock];
  61. [self.notificationPort sendBeforeDate:[NSDate date]
  62. components:nil
  63. from:nil
  64. reserved: 0 ];
  65. }
  66. else {
  67. // Process the notification here;  
  68. NSLog(@ "current thread = %@" , [NSThread currentThread]);
  69. NSLog(@ "process notification" );
  70. }
  71. }
  72.  
  73. @end  

After running, the output is as follows:

  1. 2015-03-11   23 : 38 : 31.637 test[ 1474 : 92483 ] current thread = <NSThread: 0x7ffa4070ed50 >{number = 1 , name = main}
  2. 2015-03-11   23 : 38 : 31.663 test[ 1474 : 92483 ] current thread = <NSThread: 0x7ffa4070ed50 >{number = 1 , name = main}
  3. 2015-03-11   23 : 38 : 31.663 test[ 1474 : 92483 ]process notification

As you can see, the Notification we threw in the global dispatch queue was received in the main thread as expected.

For the specific analysis and limitations of this implementation, please refer to the official document Delivering Notifications To Particular Threads. Of course, a better way may be to subclass NSNotificationCenter ourselves, or write a separate class to handle this forwarding.
Thread safety of NSNotificationCenter

Apple's strategy of posting and forwarding the same message in the same thread is probably based on thread safety considerations. The official documentation tells us that NSNotificationCenter is a thread-safe class, and we can use the same NSNotificationCenter object in a multi-threaded environment without locking. The original text is in the Threading Programming Guide, as follows:

  1. The following classes and functions are considered generally to be thread-safe. You can use the same instance from multiple threads without first acquiring a lock.
  2.  
  3. NSArray
  4. ...
  5. NSNotification
  6. NSNotificationCenter

We can add/remove notification observers in any thread, and we can post a notification in any thread.

NSNotificationCenter has done a lot of work in terms of thread safety, does that mean we can sit back and relax? Let's go back to the last example and modify it a little bit, step by step:

Code Listing 3: Common pattern for NSNotificationCenter

  1. @interfaceObserver : NSObject
  2.  
  3. @end  
  4.  
  5. @implementation Observer
  6.  
  7. - (instancetype)init
  8. {
  9. self = [ super init];
  10.  
  11. if (self)
  12. {
  13. _poster = [[Poster alloc] init];
  14.  
  15. [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector (handleNotification:) name:TEST_NOTIFICATION object:nil]
  16. }
  17.  
  18. return self;
  19. }
  20.  
  21. - ( void )handleNotification:(NSNotification *)notification
  22. {
  23. NSLog(@ "handle notification " );
  24. }
  25.  
  26. - ( void )dealloc
  27. {
  28. [[NSNotificationCenter defaultCenter] removeObserver:self];
  29. }
  30.  
  31. @end  
  32.  
  33. // Other places  
  34. [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];

The above code is what we usually do: add a notification listener, define a callback, and remove the listener when the object to which it belongs is released; then post a notification somewhere in the program. Simple and clear, if all this happens in one thread, or at least the dealloc method is run in the thread of -postNotificationName: (Note: NSNotification post and forward are synchronous), then everything is OK, there is no thread safety problem. But what problems will arise if the dealloc method and -postNotificationName: method are not run in the same thread?

Let's modify the above code again:

Code Listing 4: Thread safety issues caused by NSNotificationCenter

  1. #pragma mark - Poster
  2.  
  3. @interface Poster : NSObject
  4.  
  5. @end  
  6.  
  7. @implementation Poster
  8.  
  9. - (instancetype)init
  10. {
  11. self = [ super init];
  12.  
  13. if (self)
  14. {
  15. [self performSelectorInBackground: @selector (postNotification) withObject:nil];
  16. }
  17.  
  18. return self;
  19. }
  20.  
  21. - ( void )postNotification
  22. {
  23. [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
  24. }
  25.  
  26. @end  
  27.  
  28. #pragma mark - Observer
  29.  
  30. @interfaceObserver : NSObject
  31. {
  32. Poster *_poster;
  33. }
  34.  
  35. @property (nonatomic, assign) NSInteger i;
  36.  
  37. @end  
  38.  
  39. @implementation Observer
  40.  
  41. - (instancetype)init
  42. {
  43. self = [ super init];
  44.  
  45. if (self)
  46. {
  47. _poster = [[Poster alloc] init];
  48.  
  49. [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector (handleNotification:) name:TEST_NOTIFICATION object:nil];
  50. }
  51.  
  52. return self;
  53. }
  54.  
  55. - ( void )handleNotification:(NSNotification *)notification
  56. {
  57. NSLog(@ "handle notification begin" );
  58. sleep( 1 );
  59. NSLog(@ "handle notification end" );
  60.  
  61. self.i = 10 ;
  62. }
  63.  
  64. - ( void )dealloc
  65. {
  66. [[NSNotificationCenter defaultCenter] removeObserver:self];
  67.  
  68. NSLog(@ "Observer dealloc" );
  69. }
  70.  
  71. @end  
  72.  
  73. #pragma mark - ViewController
  74.  
  75. @implementation ViewController
  76.  
  77. - ( void )viewDidLoad {
  78. [ super viewDidLoad];
  79.  
  80. __autoreleasing Observer *observer = [[Observer alloc] init];
  81. }
  82.  
  83. @end  

This code adds a TEST_NOTIFICATION notification listener to the main thread and removes it from the main thread, while our NSNotification is posted in the background thread. In the notification processing function, we let the callback thread sleep for 1 second and then set the property i value. What will happen at this time? Let's take a look at the output first:

  1. 2015-03-14   00 : 31 : 41.286 SKTest[ 932 : 88791 ] handle notification begin
  2. 2015-03-14   00 : 31 : 41.291 SKTest[ 932 : 88713 ] Observer dealloc
  3. 2015-03-14   00 : 31 : 42.361 SKTest[ 932 : 88791 ] handle notification end
  4. (lldb)
  5.  
  6. // The program throws "Thread 6: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)" at self.i = 10  

A classic memory error, the program crashed. In fact, from the output, we can see what happened. Let's briefly describe:

When we register an observer, the notification center holds a weak reference to the observer to ensure that the observer is available.
The main thread calls the dealloc operation, which reduces the reference count of the Observer object to 0, and the object will be released.
The background thread sends a notification. If the Observer has not been released at this time, it will forward the message to it and execute the callback method. If the object is released during the callback execution, the above problem will occur.

Of course, the above example is intentional, but it does not rule out the possibility of encountering similar problems in actual coding. Although NSNotificationCenter is thread-safe, it does not mean that we can guarantee thread safety when using it. If we are not careful, thread problems will still occur.

So what should we do? Here are some good suggestions:

Try to handle notification-related operations in one thread. In most cases, this will ensure that notifications work properly. However, we cannot be sure in which thread the dealloc method will be called, so this is still difficult.
When registering listeners, use a block-based API. In this way, if we continue to call properties or methods of self in the block, we can handle it in a weak-strong way. You can modify the above code to see what the effect is.
Use objects with a safe lifecycle, which is perfect for singleton objects, as they will not be released during the entire lifecycle of the application.
Use a proxy.

<<:  It’s terrible: On average, a malicious program is created every 18 seconds on Android

>>:  Apple's officially certified mobile phone cases are here, what do you think?

Recommend

How should operations perform user segmentation? 8 steps to clear your mind

If the thinking section gives us the angle to thi...

No need for sunscreen in autumn? Dermatologists say

"The sun isn't that strong in autumn, so...

"Hydrogen Wind" Xu Lai: Unveiling the mystery of green hydrogen

Produced by Guangdong Science and Technology News...

Why can a weak drop of water penetrate a hard rock?

Listen to some geological knowledge and understan...

iOS Development - Exploring the Block Principle

1. Overview In iOS development, everyone is famil...

10 classic brand marketing in 2020

Younger, It is always the main theme of the brand...

SEM One Hundred Thousand Whys? What does sem do?

Baidu's paid ranking is based on Baidu's ...

What is the Ultimate Form of TV? A Comprehensive Analysis of TV Technology

For many people, 1080P once represented high-qual...

How much does it cost to develop an appointment registration app in Shaoyang?

WeChat Mini Program is an application that users ...