I want to perform few operations and need to start the next operation only upon completion of the previous one. The operation I'm adding will send async call to the server and receive data. I want to start the next operation only after the first call to the server finish receiving data from the server. How to do that?
{....
PhotoDownloader *pd = [[PhotoDownloader alloc] init];
[GetGlobalOperationQueue addOperation:pd];
}
Inside the PhotoDownloader I will allocate the required parameters and call a Global function which handles all the request
[GlobalCommunicationUtil sendServerReq:reqObj withResponseHandler:self];
Inside the sendServerReq method I will construct the URL request and send it to the server and this call is a "sendAsynchronousRequest" call. The PhotoDownloader will have the CommunicationUtil's delegate methods.
There are two parts to this question:
You asked:
How do I make one operation not start until the previous operation finishes?
To do this, you could, theoretically, simply make a serial queue (which is fine if you want to make all operations wait until the prior one finishes). With an NSOperationQueue, you achieve that simply by setting maxConcurrentOperationCount to 1.
Or, a little more flexible, you could establish dependencies between operations where dependencies are needed, but otherwise enjoy concurrency. For example, if you wanted to make two network requests dependent upon the completion of a third, you could do something like:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4; // generally with network requests, you don't want to exceed 4 or 5 concurrent operations;
// it doesn't matter too much here, since there are only 3 operations, but don't
// try to run more than 4 or 5 network requests at the same time
NSOperation *operation1 = [[NetworkOperation alloc] initWithRequest:request1 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request1 error:error];
}];
NSOperation *operation2 = [[NetworkOperation alloc] initWithRequest:request2 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request2 error:error];
}];
NSOperation *operation3 = [[NetworkOperation alloc] initWithRequest:request3 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request3 error:error];
}];
[operation2 addDependency:operation1]; // don't start operation2 or 3 until operation1 is done
[operation3 addDependency:operation1];
[queue addOperation:operation1]; // now add all three to the queue
[queue addOperation:operation2];
[queue addOperation:operation3];
You asked:
How do I ensure that an operation will not complete until the asynchronous network request it issued has finished as well?
Again, there are different approaches here. Sometimes you can avail yourself with semaphores to make asynchronous process synchronous. But, much better is to use a concurrent NSOperation subclass.
An "asynchronous" NSOperation is simply one that will not complete until it issues a isFinished notification (thereby allowing any asynchronous tasks it initiates to finish). And an NSOperation class specifies itself as an asynchronous operation simply by returning YES in its isAsynchronous implementation. Thus, an abstract class implementation of an asynchronous operation might look like:
// AsynchronousOperation.h
#import Foundation;
#interface AsynchronousOperation : NSOperation
/**
Complete the asynchronous operation.
If you create an asynchronous operation, you _must_ call this for all paths of execution
or else the operation will not terminate (and dependent operations and/or available
concurrent threads for the operation queue (`maxConcurrentOperationCount`) will be blocked.
*/
- (void)completeOperation;
#end
and
//
// AsynchronousOperation.m
//
#import "AsynchronousOperation.h"
#interface AsynchronousOperation ()
#property (getter = isFinished, readwrite) BOOL finished;
#property (getter = isExecuting, readwrite) BOOL executing;
#end
#implementation AsynchronousOperation
#synthesize finished = _finished;
#synthesize executing = _executing;
- (instancetype)init {
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (void)start {
if (self.isCancelled) {
if (!self.isFinished) self.finished = YES;
return;
}
self.executing = YES;
[self main];
}
- (void)completeOperation {
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
#pragma mark - NSOperation methods
- (BOOL)isAsynchronous {
return YES;
}
- (BOOL)isExecuting {
#synchronized(self) { return _executing; }
}
- (BOOL)isFinished {
#synchronized(self) { return _finished; }
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:#"isExecuting"];
#synchronized(self) { _executing = executing; }
[self didChangeValueForKey:#"isExecuting"];
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:#"isFinished"];
#synchronized(self) { _finished = finished; }
[self didChangeValueForKey:#"isFinished"];
}
#end
Now that we have that abstract, asynchronous NSOperation subclass, we can use it in our concrete NetworkOperation class:
#import "AsynchronousOperation.h"
NS_ASSUME_NONNULL_BEGIN
typedef void(^NetworkOperationCompletionBlock)(NSData * _Nullable data, NSError * _Nullable error);
#interface NetworkOperation : AsynchronousOperation
#property (nullable, nonatomic, copy) NetworkOperationCompletionBlock networkOperationCompletionBlock;
#property (nonatomic, copy) NSURLRequest *request;
- (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(NetworkOperationCompletionBlock)completionHandler;
#end
NS_ASSUME_NONNULL_END
and
// NetworkOperation.m
#import "NetworkOperation.h"
#interface NetworkOperation ()
#property (nonatomic, weak) NSURLSessionTask *task;
#end
#implementation NetworkOperation
- (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(NetworkOperationCompletionBlock)completionHandler {
self = [self init];
if (self) {
self.request = request;
self.networkOperationCompletionBlock = completionHandler;
}
return self;
}
- (void)main {
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionTask *task = [session dataTaskWithRequest:self.request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (self.networkOperationCompletionBlock) {
self.networkOperationCompletionBlock(data, error);
self.networkOperationCompletionBlock = nil;
}
[self completeOperation];
}];
[task resume];
self.task = task;
}
- (void)cancel {
[super cancel];
[self.task cancel];
}
#end
Now, in this example, I'm using block-based implementation of these asynchronous network requests, but the idea works equally well in delegate-based connections/sessions, too. (The only hassle is that NSURLSession specifies its task-related delegate methods to be part of the session, not the network task.)
Clearly the implementation of your own NetworkOperation class may differ wildly (use delegate patterns or completion block patterns, etc.), but hopefully this illustrates the idea of a concurrent operation. For more information, see the Operation Queues chapter of the Concurrency Programming Guide, notably the section titled "Configuring Operations for Concurrent Execution".
A swift version for an asynchronous operation (which was not very obvious):
final class NetworkOperation: Operation {
lazy var session: NSURLSession = {
return NSURLSession.sharedSession()
}()
private var _finished = false {
willSet {
willChangeValue(forKey: "isFinished")
}
didSet {
didChangeValue(forKey: "isFinished")
}
}
private var _executing = false {
willSet {
willChangeValue(forKey: "isExecuting")
}
didSet {
didChangeValue(forKey: "isExecuting")
}
}
override var isAsynchronous: Bool {
return true
}
override var isFinished: Bool {
return _finished
}
override var isExecuting: Bool {
return _executing
}
override func start() {
_executing = true
execute()
}
func execute() {
task = session.downloadTaskWithURL(NSURL(string: "yourURL")!) {
(url, response, error) in
if error == nil {
// Notify the response by means of a closure or what you prefer
// Remember to run in the main thread since NSURLSession runs its
// task on background by default
} else {
// Notify the failure by means of a closure or what you prefer
// Remember to run in the main thread since NSURLSession runs its
// task on background by default
}
// Remember to tell the operation queue that the execution has completed
self.finish()
}
}
func finish() {
//Async task complete and hence the operation is complete
_executing = false
_finished = true
}
}
To serialize the operations:
let operationQueue = OperationQueue()
let operation1 = NetworkOperation()
let operation2 = NetworkOperation()
operation2.addDependency(operation1)
operationQueue.addOperations([operation1, operation2], waitUntilFinished: false)
Is it manadatory to use NSOperationQueue?
This behaviour is very easy to implement with serial queues
https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html
Assuming you have a class to manage the operations, you would create a serial dispatch queue in your init method with
queue = dispatch_queue_create("com.example.MyQueue", NULL);
And you would have a method to enqueue request, something like this
- (void) enqueueRequest:(NSURL *)requestURL
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_sync(queue, ^{ /* get data from requestURL */ }) });
}
This way, only one request is active at one time, even though each request will be executed in a separated background thread, and several requests will be enqueued until the active request finishes.
Related
My network requests need to happen in a FIFO serial fashion. If one fails due to a network issue (i.e. offline, timeout) I need the failed request to retry before continuing the queue. How can I accomplish this?
When the operation fails, I set executing and finished both to - assuming that would work as a 'reset' for the NSOperation. But the queue never resumes.
Is there something I'm missing?
You could build a network operation that performs retries, and only sets isFinished when the network completion block is called, and it determines that no retry is needed:
class NetworkOperationWithRetry: AsynchronousOperation {
var session: NSURLSession
var request: NSURLRequest
var task: NSURLSessionTask?
var networkCompletionHandler: ((NSData?, NSURLResponse?, NSError?) -> ())?
init(session: NSURLSession = NSURLSession.sharedSession(), request: NSURLRequest, networkCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> ()) {
self.session = session
self.request = request
self.networkCompletionHandler = networkCompletionHandler
}
override func main() {
attemptRequest()
}
func attemptRequest() {
print("attempting \(request.URL!.lastPathComponent)")
task = session.dataTaskWithRequest(request) { data, response, error in
if error?.domain == NSURLErrorDomain && error?.code == NSURLErrorNotConnectedToInternet {
print("will retry \(self.request.URL!.lastPathComponent)")
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * Int64(NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
self.attemptRequest()
}
return
}
print("finished \(self.request.URL!.lastPathComponent)")
self.networkCompletionHandler?(data, response, error)
self.networkCompletionHandler = nil
self.completeOperation()
}
task?.resume()
}
override func cancel() {
task?.cancel()
super.cancel()
}
}
And you could call it like so:
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 1 // I wouldn't generally do this, but just to illustrate that it's honoring operation queue dependencies/concurrency settings
let requests = urlStrings.map { NSURLRequest(URL: NSURL(string: $0)!) }
requests.forEach { request in
queue.addOperation(NetworkOperationWithRetry(request: request) { data, response, error in
// do something with the `data`, `response`, and `error`
})
}
Now, that's only retrying on NSURLErrorNotConnectedToInternet, but you could change that logic to be whatever you want. Likewise, I'd probably be inclined to add some "max retries" logic. Also, if the lack of Internet connectivity is really the issue, rather than retrying until Internet connectivity is achieved, I'd probably be inclined to set the operation's isReady notification tied into Reachability.
By the way, the above uses the following AsynchronousOperation:
/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `completeOperation()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `completeOperation()` is called.
public class AsynchronousOperation : NSOperation {
override public var asynchronous: Bool { return true }
private let stateLock = NSLock()
private var _executing: Bool = false
override private(set) public var executing: Bool {
get {
return stateLock.withCriticalScope { _executing }
}
set {
willChangeValueForKey("isExecuting")
stateLock.withCriticalScope { _executing = newValue }
didChangeValueForKey("isExecuting")
}
}
private var _finished: Bool = false
override private(set) public var finished: Bool {
get {
return stateLock.withCriticalScope { _finished }
}
set {
willChangeValueForKey("isFinished")
stateLock.withCriticalScope { _finished = newValue }
didChangeValueForKey("isFinished")
}
}
/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting
public func completeOperation() {
if executing {
executing = false
}
if !finished {
finished = true
}
}
override public func start() {
if cancelled {
finished = true
return
}
executing = true
main()
}
override public func main() {
fatalError("subclasses must override `main`")
}
}
extension NSLock {
/// Perform closure within lock.
///
/// An extension to `NSLock` to simplify executing critical code.
///
/// - parameter block: The closure to be performed.
func withCriticalScope<T>(#noescape block: Void -> T) -> T {
lock()
let value = block()
unlock()
return value
}
}
I used this custom class to manage NSOperation. I fiddled a bit with the code from GCDWebServer and came out with this stub. Most of it is self explanatory.
#interface SROperation : NSOperation <NSCopying>
///-------------------------------
/// #name error reporting
///-------------------------------
/**
The error, if any, that occurred in the lifecycle of the request.
*/
#property (nonatomic, strong) NSError *error;
///-----------------------------------------
/// #name the operation properties
///-----------------------------------------
/**
a dictionary.
*/
#property (nonatomic, strong) NSDictionary*aDictionary;
///----------------------------------
/// #name Pausing / Resuming Requests
///----------------------------------
+(instancetype)operationWithDictionary:(NSDictionary*)dict;
/**
Pauses the execution of the request operation.
A paused operation returns `NO` for `-isReady`, `-isExecuting`, and `-isFinished`. As such, it will remain in an `NSOperationQueue` until it is either cancelled or resumed. Pausing a finished, cancelled, or paused operation has no effect.
*/
- (void)pause;
/**
Whether the request operation is currently paused.
#return `YES` if the operation is currently paused, otherwise `NO`.
*/
#property (NS_NONATOMIC_IOSONLY, getter=isPaused, readonly) BOOL paused;
/**
Resumes the execution of the paused request operation.
Pause/Resume behavior varies depending on the underlying implementation for the operation class. In its base implementation, resuming a paused requests restarts the original request. However, since HTTP defines a specification for how to request a specific content range, `AFHTTPRequestOperation` will resume downloading the request from where it left off, instead of restarting the original request.
*/
- (void)resume;
#end
SROperation.m
#import "SROperation.h"
typedef NS_ENUM(NSInteger, SROperationState) {
SROperationPausedState = -1,
SROperationReadyState = 1,
SROperationExecutingState = 2,
SROperationFinishedState = 3,
};
static NSString * const kSROperationLockName = #"your.application.bundle.id.operationlock"
static inline BOOL SRStateTransitionIsValid(SROperationState fromState, SROperationState toState, BOOL isCancelled) {
switch (fromState) {
case SROperationReadyState:
switch (toState) {
case SROperationPausedState:
case SROperationExecutingState:
return YES;
case SROperationFinishedState:
return isCancelled;
default:
return NO;
}
case SROperationExecutingState:
switch (toState) {
case SROperationPausedState:
case SROperationFinishedState:
return YES;
default:
return NO;
}
case SROperationFinishedState:
return NO;
case SROperationPausedState:
return toState == SROperationReadyState;
default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
switch (toState) {
case SROperationPausedState:
case SROperationReadyState:
case SROperationExecutingState:
case SROperationFinishedState:
return YES;
default:
return NO;
}
}
#pragma clang diagnostic pop
}
}
static inline NSString * SRKeyPathFromSROperationState(SROperationState state) {
switch (state) {
case SROperationReadyState:
return #"isReady";
case SROperationExecutingState:
return #"isExecuting";
case SROperationFinishedState:
return #"isFinished";
case SROperationPausedState:
return #"isPaused";
default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
return #"state";
#pragma clang diagnostic pop
}
}
}
#interface SROperation ()
#property (readwrite, nonatomic, assign) SROperationState state;
#property (readwrite, nonatomic, strong) NSRecursiveLock *lock;
#end
#implementation SROperation
+(instancetype)operationWithDictionary:(NSDictionary*)dict {
return [[self alloc] initWithDictionary:dict];
}
-(instancetype)initWithDictionary:(NSDictionary*)aDictionary {
self = [self init];
if (self)
{
self.aDictionary = aDictionary;
self.queuePriority = NSOperationQueuePriorityVeryHigh;
}
return self;
}
-(instancetype)init
{
self = [super init];
if(self) {
_state = SROperationReadyState;
_lock = [[NSRecursiveLock alloc] init];
_lock.name = kSROperationLockName;
}
return self;
}
#pragma mark -
- (void)setState:(SROperationState)state {
if (!SRStateTransitionIsValid(self.state, state, [self isCancelled])) {
return;
}
[self.lock lock];
NSString *oldStateKey = SRKeyPathFromSROperationState(self.state);
NSString *newStateKey = SRKeyPathFromSROperationState(state);
[self willChangeValueForKey:newStateKey];
[self willChangeValueForKey:oldStateKey];
_state = state;
[self didChangeValueForKey:oldStateKey];
[self didChangeValueForKey:newStateKey];
[self.lock unlock];
}
- (void)pause {
if ([self isPaused] || [self isFinished] || [self isCancelled]) {
return;
}
[self.lock lock];
if ([self isExecuting]) {
dispatch_async(dispatch_get_main_queue(), ^{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:SROperationDidFinishNotification object:self userInfo:nil];
});
}
self.state = SROperationPausedState;
[self.lock unlock];
}
-(void)operationDidPause
{
}
- (BOOL)isPaused {
return self.state == SROperationPausedState;
}
- (void)resume {
if (![self isPaused]) {
return;
}
[self.lock lock];
self.state = SROperationReadyState;
[self start];
[self.lock unlock];
}
- (BOOL)isReady {
return self.state == SROperationReadyState && [super isReady];
}
- (BOOL)isExecuting {
return self.state == SROperationExecutingState;
}
- (BOOL)isFinished {
return self.state == SROperationFinishedState;
}
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self cancelConnection];
} else if ([self isReady]) {
self.state = SROperationExecutingState;
[self operationDidStart];
}
[self.lock unlock];
}
- (void)operationDidStart {
[self.lock lock];
if (![self isCancelled]) {
// YOUR ACTUAL OPERATION CODE
// finish
self.state = SROperationFinishedState;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SROperationDidFinishNotification object:nil];
});
}
[self.lock unlock];
}
- (void)cancel {
[self.lock lock];
if (![self isFinished] && ![self isCancelled]) {
[super cancel];
if ([self isExecuting]) {
}
}
[self.lock unlock];
}
#pragma mark - NSObject
- (NSString *)description {
[self.lock lock];
NSString *description = [NSString stringWithFormat:#"<%#: %p, state: %#, cancelled: %#>", NSStringFromClass([self class]), self, SRKeyPathFromSROperationState(self.state), ([self isCancelled] ? #"YES" : #"NO")];
[self.lock unlock];
return description;
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
SROperation *operation = [(SROperation *)[[self class] allocWithZone:zone] init];
// copy more properties
return operation;
}
}
My goal is to achieve synchronized communication to custom Device i.e. next command can be send only when reply is received. Now I'm doing it in this way
Device class implements DeviceDelegate protocol
//Device.h
#class Device;
#protocol DeviceDelegate <NSObject>
- (void)didReciveReplyWithData:(NSData *)data;
#end
#interface Device : NSObject {}
In DeviceViewController implementation:
#interface DeviceViewController()
{
BOOL waitingForReply = false;
}
#end
#implementation DeviceViewController
- (void)sendCommandWithData:(NSData *)data
{
if ( waitingForReply == false)
{
//send command code
waitingForReply = true;
}
}
- (void)didReciveReplyWithData:(NSData *)data
{
//code
waitingForReply = false;
}
#end
but I wish to do it in more elegant way i.e. by using GCD (semaphores?) with blocks (completionHandler?). Any ideas?
PS. Sorry, but I forgot to mention: all commands sended to device while
waitingForReply = true
should be ignored!!!.
Possibly the best approach here would be to create a queue of commands with NSOperationQueue.
Since, presumably, the communication with the device is asynchronous you will have to subclass NSOperation to encapsulate the communication.
#interface DeviceCommandOperation : NSOperation <DeviceDelegate>
#property (nonatomic, assign) BOOL waitingForReply;
#property (nonatomic, copy) NSData *dataToSend;
#property (nonatomic, copy) NSData *dataReceived;
#end
#implementation DeviceCommandOperation
- (instancetype)initWithData:(NSData *)dataToSend
{
self = [super init];
if (self)
{
_dataToSend = [dataToSend copy];
}
return self;
}
- (void)setWaitingForReply:(BOOL)waitingForReply
{
if (_waitingForReply != waitingForReply)
{
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_waitingForReply = waitingForReply;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
}
- (void)start
{
self.waitingForReply = YES;
// Simulate sending a command and waiting for response.
// You will need to replace this with your actual communication mechanism.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// In reality this call would presumably come from the Device
[self didReceiveReplyWithData:someData];
});
}
- (void)didReceiveReplyWithData:(NSData *)data
{
self.dataReceived = data;
self.waitingForReply = NO;
}
#pragma mark - NSOperation
- (BOOL)isAsynchronous
{
return YES;
}
- (BOOL)isExecuting
{
return _waitingForReply;
}
- (BOOL)isFinished
{
return !_waitingForReply;
}
#end
This operation could then be used from your DeviceViewController (it would probably be better architecturally to have this responsibility elsewhere but that's not the topic of this question).
#interface DeviceViewController ()
#property (nonatomic, strong) NSOperationQueue *operationQueue;
#end
#implementation DeviceViewController
- (NSOperationQueue *)operationQueue
{
if (_operationQueue == nil)
{
_operationQueue = [[NSOperationQueue alloc] init];
}
return _operationQueue;
}
- (void)sendNextCommand
{
NSData *data = // Get data for the next command
[self sendCommandWithData:data];
}
- (void)sendCommandWithData:(NSData *)data
{
NSLog(#"Queueing operation");
DeviceCommandOperation *operation = [[DeviceCommandOperation alloc] initWithData:data];
// The operation's completionBlock gets called on a background queue
[operation setCompletionBlock:^{
NSLog(#"DeviceCommandOperation completed");
// Process operation.dataReceived
[self sendNextCommand];
}];
[self.operationQueue addOperation:operation];
}
#end
This approach will allow you to determine what (if any) command to send next, based on the reply to the previous command.
If you know all of the "commands" you will want to send initially and don't need finer grained control you could create instances of DeviceCommandOperation for each command, set the queue's maxConcurrentOperationCount to 1, and add each DeviceCommandOperation to the queue (in the order you want them to be processed).
I have few different questions about NSOperation and NSOperationQueue and I know guys that yours answers will help me;
I have to load a big amount of images and I have created my own loader based on NSOperation, NSOperationQueue and NSURLConnection (asynchronous loading);
Questions:
If I set maxConcurrentOperationCount (for example 3) for queue (NSOperationQueue), does it mean that only 3 operations performed in the same time even queue has 100 operations?
When I set property maxConcurrentOperationCount for queue sometimes "setCompletionBlock" doesn't work and count (operationCount) only increases; Why?
MyLoader:
- (id)init
{
self = [super init];
if (self) {
_loadingFiles = [NSMutableDictionary new];
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 3;
_downloadQueue.name = #"LOADER QUEUE";
}
return self;
}
- (void)loadFile:(NSString *)fileServerUrl handler:(GetFileDataHandler)handler {
if (fileServerUrl.length == 0) {
return;
}
if ([_loadingFiles objectForKey:fileServerUrl] == nil) {
[_loadingFiles setObject:fileServerUrl forKey:fileServerUrl];
__weak NSMutableDictionary *_loadingFiles_ = _loadingFiles;
MyLoadOperation *operation = [MyLoadOperation new];
[operation fileServerUrl:fileServerUrl handler:^(NSData *fileData) {
[_loadingFiles_ removeObjectForKey:fileServerUrl];
if (fileData != nil) {
handler(fileData);
}
}];
[operation setQueuePriority:NSOperationQueuePriorityLow];
[_downloadQueue addOperation:operation];
__weak NSOperationQueue *_downloadQueue_ = _downloadQueue;
[operation setCompletionBlock:^{
NSLog(#"completion block :%i", _downloadQueue_.operationCount);
}];
}
}
MyOperation:
#interface MyLoadOperation()
#property (nonatomic, assign, getter=isOperationStarted) BOOL operationStarted;
#property(nonatomic, strong)NSString *fileServerUrl;
#property(nonatomic, copy)void (^OnFinishLoading)(NSData *);
#end
#implementation MyLoadOperation
- (id)init
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (void)fileServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler {
#autoreleasepool {
self.fileServerUrl = fileServerUrl;
[self setOnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
}
- (void)main {
#autoreleasepool {
[self stop];
}
}
- (void)start {
[self setOperationStarted:YES];
[self willChangeValueForKey:#"isFinished"];
_finished = NO;
[self didChangeValueForKey:#"isFinished"];
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
_executing = NO;
[self didChangeValueForKey:#"isFinished"];
}
else
{
[self willChangeValueForKey:#"isExecuting"];
_finished = NO;
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (void)cancel {
[self.connection cancel];
if ([self isExecuting])
{
[self stop];
}
[super cancel];
}
#pragma mark -NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self OnFinishLoading]) {
[self OnFinishLoading](_data);
}
if (![self isCancelled]) {
[self stop];
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
;
if (![self isCancelled]) {
[self stop];
}
}
- (void)stop {
#try {
__weak MyLoadOperation *self_ = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self_ completeOperation];
});
}
#catch (NSException *exception) {
NSLog(#"Exception! %#", exception);
[self completeOperation];
}
}
- (void)completeOperation {
if (![self isOperationStarted]) return;
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
You must start the connection in the Operation's start method, and not in fileServerUrl:handler:.
I would remove this method altogether, and only provide an init method with all required parameters where you can completely setup the operation. Then, in method start start the connection.
Additionally, it's not clear why you override main.
Modifying the state variables _executing and _finished could be more concise and more clear (you don't need to set them initially, since the are already initialized to NO). Only set them in the "final" method completeOperation including KVO notifications.
You also do not need a #try/#catch in stop, since function dispatch_async() does not throw Objective-C exceptions.
Your cancel method is not thread safe, and there are also a few other issues. I would suggest the following changes:
#implementation MyOperation {
BOOL _executing;
BOOL _finished;
NSError* _error; // remember the error
id _result; // the "result" of the connection, unless failed
completion_block_t _completionHandler; //(your own completion handler)
id _self; // strong reference to self
}
// Use the "main thread" as the "synchronization queue"
- (void) start
{
// Ensure start will be called only *once*:
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.isCancelled && !_finished && !_executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
_self = self; // keep a strong reference to self in order to make
// the operation "immortal for the duration of the task
// Setup connection:
...
[self.connection start];
}
});
}
- (void) cancel
{
dispatch_async(dispatch_get_main_queue, ^{
[super cancel];
[self.connection cancel];
if (!_finished && !_executing) {
// if the op has been cancelled before we started the connection
// ensure the op will be orderly terminated:
self.error = [[NSError alloc] initWithDomain:#"MyOperation"
code:-1000
userInfo:#{NSLocalizedDescriptionKey: #"cancelled"}];
[self completeOperation];
}
});
}
- (void)completeOperation
{
[self willChangeValueForKey:#"isExecuting"];
self.isExecuting = NO;
[self didChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
self.isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
completion_block_t completionHandler = _completionHandler;
_completionHandler = nil;
id result = self.result;
NSError* error = self.error;
_self = nil;
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
completionHandler(result, error);
});
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self onFinishLoading]) {
[self onFinishLoading](self.result);
}
[self completeOperation];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (self.error == nil) {
self.error = error;
}
[self completeOperation];
}
In answer to your questions:
Yes, a maxConcurrentOperationCount of three means that only three will run at a time. Doing network requests like this is perfect example of when you'd want to use maxConcurrentOperationCount, because failure to do so would result in too many network requests trying to run, most likely resulting in some of the connections failing when using a slower network connection.
The main issue here, though, is that you're calling your operation's fileServerUrl method (which is starting the connection) from MyLoader. You've disconnected the request from the operation's start (defeating the purpose of maxConcurrentCount of 3 and possibly confusing the state of the operation).
The start method should be initiating the connection (i.e. don't start the request until one of those three available concurrent operations is available). Furthermore, since you cannot pass the URL and the handler to the start method, you should move your logic that saves those values to a customized rendition of your init method.
There are other minor edits we might suggest to your operation (main not needed, operationStarted is a little redundant, simplify the _executing/_finished handling, etc.), but the starting of the connection in fileServerUrl rather than being initiated by the start method is the key issue.
Thus:
- (id)initWithServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
// do your saving of `fileServerURL` and `handler` here, e.g.
self.fileServerUrl = fileServerUrl;
self.OnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
}
return self;
}
- (void)startRequest {
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
- (void)start {
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[self setOperationStarted:YES]; // personally, I'd retire this and just reference your `executing` flag, but I'll keep it here for compatibility with the rest of your code
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
[self startRequest];
}
For the first question, the answer is yes, if set 3 as a max number of operations, only 3 can be running togheter.
The second is bit strange problem and I'm not totally sure that this answer will be correct. When you leave operations to an NSOperationQueue, you can't be sure on which thread they will be executed, this lead a huge problem with async connection. When you start an NSURLConnection as usual you receive the delegate callbacks without a problem, that is because the connection is running on a thread with a living run loop. If you start the connection on a secondary thread, callbacks will be called on that thread, but if you don't keep the run loop alive they will be never received.That's where probably my answer isn't correct, GCD should take care of living run loops, because GCD queues runs on living threads. But if not, the problem could be that operations are started on a different thread, the start method is called, but the callbacks are never called. Try to check if the thread is always the main thread.
This question already has answers here:
NSOperationQueue serial FIFO queue
(3 answers)
Closed 9 years ago.
I'm having trouble understanding the way NSOperationQueue works.
Say I have:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount=1;
[queue addOperationWithBlock:^{
[someObject someSelector];
}];
[queue addOperationWithBlock:^{
[someObject anotherSelector];
}];
The second block is being called even before the first block finishes - the opposite of what I want. I tried using – performSelectorOnMainThread:withObject:waitUntilDone: instead, but the second block is still being executed first - presumably because the block thread is not being completed on the main thread, and so it is not blocked with waitUntilDone. I added a break point inside my someSelector block, and it is reached after a break point inside the second block.
I don't quite get it. Help me!!
If there are explicit dependencies between the operations, then use addDependency:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount=1;
NSOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
[someObject someSelector];
}];
NSOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
[someObject anotherSelector];
}];
[operation2 addDependency:operation1];
[queue addOperation:operation1];
[queue addOperation:operation2];
If your operations are doing asynchronous activity, then you should define a custom operation, and only call completeOperation (which will post the isFinished message) when the asynchronous task is done).
// SomeOperation.h
#import <Foundation/Foundation.h>
#interface SomeOperation : NSOperation
#end
and
// SomeOperation.m
#import "SomeOperation.h"
#interface SomeOperation ()
#property (nonatomic, readwrite, getter = isFinished) BOOL finished;
#property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
#end
#implementation SomeOperation
#synthesize finished = _finished;
#synthesize executing = _executing;
#pragma Configure basic operation
- (id)init
{
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (void)start
{
if ([self isCancelled]) {
self.finished = YES;
return;
}
self.executing = YES;
[self main];
}
- (void)completeOperation
{
self.executing = NO;
self.finished = YES;
}
- (void)main
{
// start some asynchronous operation
// when it's done, call `completeOperation`
}
#pragma mark - Standard NSOperation methods
- (BOOL)isConcurrent
{
return YES;
}
- (void)setExecuting:(BOOL)executing
{
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
- (void)setFinished:(BOOL)finished
{
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
#end
Thus, with the following code, it won't start operation2 until the asynchronous task initiated in main in SomeOperation object, operation1, calls its completeOperation method.
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount=1;
NSOperation *operation1 = [[SomeOperation alloc] init];
NSOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
[someObject anotherSelector];
}];
[operation2 addDependency:operation1];
[queue addOperation:operation1];
[queue addOperation:operation2];
I am using ASIHTTPRequest to check if user logged in, and try to return a boolean value when login successfull
Problem: When I request the boolean it always returns 0 then few seconds later ASIHTTPRequest finishes its request and update the boolean value.
What I want: get the boolean value after all requests finishes. I think proper way would be write a boolean function and retrieve the result of asihhtp requests?
in singleton:
#interface CloudConnection : NSObject
{
BOOL isUserLoggedIN;
}
#property BOOL isUserLoggedIN;
+ (CloudConnection *)sharedInstance;
#end
+ (CloudConnection *)sharedInstance
{
static CloudConnection *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[CloudConnection alloc] init];
// Do any other initialisation stuff here
});
return sharedInstance;
}
- (id)init {
if (self = [super init]) {
//send login request
[self sendLoginRequest];
}
return self;
}
-(void) sendLoginRequest{ .....}
- (void)requestFinished:(ASIHTTPRequest *)request
{ else if (request.responseStatusCode == 202) {
//parse json data
NSLog(#"Login Succesfull");
_isUserLoggedIN=YES;
}
}
- (void)requestFailed:(ASIHTTPRequest *)request{}
In a VC:
CloudConnection *sharedInstance=[CloudConnection sharedInstance];
NSLog(#"is logged in init %hhd",sharedInstance.isUserLoggedIN);
[self performSelector:#selector( checkLoginAfterFiveSeconds) withObject:nil afterDelay:5.0];
-(void) checkLoginAfterFiveSeconds
{
CloudConnection *sharedInstance=[CloudConnection sharedInstance];
NSLog(#"is logged in init %hhd",sharedInstance.isUserLoggedIN);
}
NSLOG:
is logged in init 0
Login Succesfull`
is logged in init 1 //after 5 secs
well if you do what you propose, its gonna block the calling thread. and you never want a thread waiting for network traffic especially not the main / ui thread
make it a void function and make it call a completionHandler or ... send a NSNotification once the result can be calculated directly! :)
Yes you are right :)
In your request completion block call this method:
[self loginResult:result];
-(void)loginResult:(BOOL)result
{
if(result == TRUE)
{
NSLog(#"Login successfully now call any method or do what ever you want");
}
else
{
NSLog(#"Login unsuccessfull");
}
}