NSURLConnection with NSRunLoopCommonModes

I wrote my own implementation of HTTPClient for my iOS application to download the contents of the specified URL asynchronously. HTTPClient uses an NSOperationQueue to host NSURLConnection requests. I chose NSOperationQueue because I wanted to cancel any or all ongoing NSURLConnection at any given time.

I did a lot of research on how to implement my HTTPClient, and I had two options to run NSURLConnection:

1) Run each nested NSURLConnection on a separate secondary thread. An NSOperationQueue performs each second-thread-bound operation in the background, and therefore, I don’t have to explicitly do anything to start secondary threads other than starting my NSURLConnection in an overridden method of starting a subclass of NSOperation and running runloop for the generated secondary thread until while connectionDidFinishLoading or ConnectionDidFailWithError is raised. It looks like this:

if (self.connection != nil) { do { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } while (!self.isFinished); } 

2) Perform each completed NSURLConnection in the main thread. To do this, inside the start method, I used the performSelectorOnMainThread function and again called the start method in the main thread. With this approach, I planned NSURLConnection with NSRunLoopCommonModes, as shown below:

 [self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 

I chose this second approach and implemented it. From my research, this second approach seemed better because it does not start a separate secondary thread for each NSURLConnection. Now, at any given time, the application can have many requests simultaneously with the first approach, which means that the same number of secondary threads will be generated and will not return to the pool until the corresponding URL requests are completed.

I got the impression that I was still working simultaneously with the second approach, planning an NSURLConnection with NSRunLoopCommonModes. In other terms with this approach, I thought that I was using NSRunLoopCommonModes instead of multithreading for concurrency so that observers for NSURLConnection would either call connectionDidFinishLaunching or connectionDidFailWithError as soon as it could, regardless of what the main thread was doing with the user interface, at that time.

Unfortunately, my whole understanding turned out to be wrong when one of my colleagues showed me this morning that with the current implementation, NSURLConnection does not return until the scroll view on one of the view controllers stops scrolling. The NSURLRequest to receive data is triggered when the scroll view is about to stop scrolling, but even if it was completed before the scroll view stops ringing, somehow NSURLConnection does not call connectionDidFinishLoading or connectionDidFailWithError feedback until the scroll view stops scrolling completely. This means that the whole idea of ​​scheduling NSURLConnection with NSRunLoopCommonModes in the main thread to get real concurrency using UI operations (touch / scroll) turned out to be wrong, and NSURLConnection is still waiting while the main thread is busy scrolling scroll.

I tried switching to the first approach of using secondary streams, and it works like a charm. NSURLConnection still calls one of its protocol methods, while scroll scrolling still scrolls. This is understandable because now NSURLConnection is not working in the main thread, so it will not wait for the scroll to view to stop scrolling.

I really don't want to use the first approach, because it is expensive due to multithreading.

Can someone please tell me if my understanding of the second approach is wrong? If this is correct, what could be the reason for scheduling an NSURLConnection with NSRunLoopCommonModes not working properly?

I would really appreciate it if the answer was a little more visual, because it should clarify to me more doubts about how NSRunLoop and NSRunLoopModes work. Just to indicate that I have read the documentation many times.

+6
source share
4 answers

It turned out that the problem was easier than I imagined.

I had this in the NSOperation subclass start method

 self.connection = [[NSURLConnection alloc] initWithRequest:self.urlRequest delegate:self]; [self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 

Now the problem is in the above initWithRequest: delegate: method actually schedules the NSURLConnection in runloop by default with NSDefaultRunLoopMode and completely ignores the next line, where I'm actually trying to schedule it using NSRunLoopCommonModes. Changing the above two lines, working as early as possible.

 self.connection = [[NSURLConnection alloc] initWithRequest:self.urlRequest delegate:self startImmediately:NO]; [self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; [self.connection start]; 

The actual problem here was that I have to initialize the NSURLConnection using the constructor method with the startImmediately parameter. When I pass NO for the startImmediately parameter, the connection is not scheduled with the default start loop. It can be scheduled at run time and in select mode by calling the schedule InRunLoop: forMode: method.

Now NSURLConnection is triggered by the scrollViewWillEndDragging: withVelocity: targetContentOffset method calls its delegation methods connectionDidFinishLoading / connectionDidFailWithError, while the scroll view is still scrolling and has not completed scrolling yet.

Hope this helps someone.

+18
source

Planning a trigger loop source prevents you from triggering source callbacks simultaneously with other source callbacks.

In the case of network communications, things handled by the kernel, such as receiving and buffering packets, happen simultaneously no matter what your application does. The kernel then marks the socket as readable or writable, which can, for example, wake a select() or kevent() call if the thread was blocked in such a call. If your thread does something else, for example, handles scroll events, it will not notice socket readability / writeability until execution returns to the execution loop. Only then will NSURLConnection run the loop source code for its callback, allowing NSURLConnection to handle socket state changes and possibly call your delegation methods.

The next question is what happens when a cycle cycle has several sources and more than one is ready. For example, there are more scroll events in the event queue, and your socket is also readable or writable. Ideally, you might need a fair algorithm to serve the sources of the startup loop. In fact, it is possible that GUI events are prioritized over other sources in the run loop. In addition, triggering cycle sources may have an inherent priority (β€œorder”) relative to other sources.

It is usually not critical that, say, NSURLConnection serviced instantly. This is usually normal to allow it to wait for the main thread cycle cycle to get around it. Consider that for the same reason that the NSURLConnection run loop source will not be serviced during scrolling, there is no way that processing it on a background thread could have a user-visible effect. For example, how will this affect the user interface of your application? He would use -performSelectorOnMainThread:.. or something similar to schedule an update. But this is just as likely as the starving source of the NSURLConnection launch.

However, if you absolutely cannot fulfill this possible delay, there is an intermediate point between scheduling your NSURLConnection in the main thread or scheduling them all in separate threads. You can schedule all of them in one thread, but not in the main thread. You can create one thread that you park in your run loop. Then, where you are doing -performSelectorOnMainThread:... , instead you can do -performSelector:onThread:...

+1
source

My testing was successful for "scheduleInRunLoop: (NSRunLoop *) aRunLoop forMode: (NSString *) mode" in the second thread, it can also schedule InRunLoop back to the main run loop from the second thread.

Partial code as below:

 NSRunLoop *runloop; //global self.connection = [[NSURLConnection alloc] initWithRequest:self.urlRequest delegate:self startImmediately:NO]; [self.connection scheduleInRunLoop:runloop forMode:NSRunLoopCommonModes]; [self.connection start]; 

If you want to start NSURLConnection in another thread, you must create a loop loop like this in the main thread method (the thread must be started before the previous code runs):

 runloop = [NSRunLoop currentRunLoop]; while (!finished) { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; } 

The official docs are very convenient:

By default, a connection is scheduled for the current thread in the default when it is created. If you create a connection with the initWithRequest: delegate: startImmediately: method and provide NO for the startImmediately parameter, you can schedule the connection to another startup loop or mode before starting using the start method. You can schedule a connection in several cycles and startup modes, or on the same startup cycle in several modes. You cannot transfer after it starts.

0
source

Using the manual from Ken Thomases answer, I produced this for the type of code copies of encoders:

 static NSThread *connectionProcessingThread; static NSTimer *keepRunloopBusy; static NSRunLoop *oauth2runLoop; + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ connectionProcessingThread = [[NSThread alloc] initWithBlock:^{ oauth2runLoop = [NSRunLoop currentRunLoop]; keepRunloopBusy = [NSTimer timerWithTimeInterval:DBL_MAX repeats:YES block:^(NSTimer* timer) { NSLog(@"runloop is kept busy with this keepalive work"); }]; [oauth2runLoop addTimer:keepRunloopBusy forMode:NSRunLoopCommonModes]; [oauth2runLoop run]; }]; [connectionProcessingThread start]; atomic_thread_fence(memory_order_release); }); } 

and then you would fork

 NSURLConnection *aConnection = [[NSURLConnection alloc] initWithRequest:startRequest delegate:self startImmediately:NO]; // don't start yet if( [NSRunLoop currentRunLoop] != [NSRunLoop mainRunLoop]) { atomic_thread_fence(memory_order_acquire); [aConnection scheduleInRunLoop:oauth2runLoop forMode:NSRunLoopCommonModes]; } else { [aConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; // let first schedule it in the main runloop. } [aConnection start]; // now start 
0
source

Source: https://habr.com/ru/post/913087/


All Articles