36

I have a function in iOS app that uses dispatch_group to group multiple rest request:

static func fetchCommentsAndTheirReplies(articleId: String, failure: ((NSError)->Void)?, success: (comments: [[String: AnyObject]], replies: [[[String: AnyObject]]], userIds: Set<String>)->Void) {
    var retComments = [[String: AnyObject]]()
    var retReplies = [[[String: AnyObject]]]()
    var retUserIds = Set<String>()

    let queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
    Alamofire.request(.GET, API.baseUrl + API.article.listCreateComment, parameters: [API.article.articleId: articleId]).responseJSON {
        response in

        dispatch_async(queue) {

            guard let comments = response.result.value as? [[String: AnyObject]] else {
                failure?(Helper.error())
                return
            }
            print(comments)
            retComments = comments

            let group = dispatch_group_create()

            for (commentIndex, comment) in comments.enumerate() {
                guard let id = comment["_id"] as? String else {continue}

                let relevantUserIds = helperParseRelaventUserIdsFromEntity(comment)
                for userId in relevantUserIds {
                    retUserIds.insert(userId)
                }

                retReplies.append([[String: AnyObject]]())

                dispatch_group_enter(group)
                Alamofire.request(.GET, API.baseUrl + API.article.listCreateReply, parameters: [API.article.commentId: id]).responseJSON {
                    response in

                    dispatch_async(queue) {
                        if let replies = response.result.value as? [[String: AnyObject]] {
                            for (_, reply) in replies.enumerate() {

                                let relevantUserIds = helperParseRelaventUserIdsFromEntity(reply)
                                for userId in relevantUserIds {
                                    retUserIds.insert(userId)
                                }
                            }
                            retReplies[commentIndex] = replies
                        }
                        dispatch_group_leave(group)
                    }

                }
            }

            dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
            success(comments: retComments, replies: retReplies, userIds: retUserIds)

        }

    }
}

As you can see from my code, I fetch all the comments under the same article, then fetch coresponding replies under each comment. After all requests are done, I invoke my success callback. This can be achieved using GCD's dispatch_group.

Now I am migrating the same functionality to android.

public static void fetchCommentsAndTheirReplies(Context context, String articleId, final StringBuffer outErrorMessage, final Runnable failure, final ArrayList<JSONObject> outComments, final ArrayList<ArrayList<JSONObject>> outReplies, final HashSet<String> outUserIds, final Runnable success) {
    final RequestQueue queue = Volley.newRequestQueue(context);
    HashMap<String, String> commentParams = new HashMap<>();
    commentParams.put(API.article.articleId, articleId);
    JsonArrayRequest commentRequest = new JsonArrayRequest(Request.Method.GET, API.baseUrl + API.article.listCreateComment, new JSONObject(commentParams), new Response.Listener<JSONArray>() {
        @Override
        public void onResponse(JSONArray response) {
            try {
                for (int i = 0; i < response.length(); i++) {
                    JSONObject comment = response.getJSONObject(i);
                    outComments.add(comment);

                    outUserIds.addAll(helperParseRelaventUserIdsFromEntity(comment));
                    outReplies.add(new ArrayList<JSONObject>());

                    //TODO: DISPATCH_GROUP?
                    String id = comment.getString("_id");
                    HashMap<String, String> replyParams = new HashMap<>();
                    replyParams.put(API.article.commentId, id);
                    final int finalI = i;
                    JsonArrayRequest replyRequest = new JsonArrayRequest(Request.Method.GET, API.baseUrl + API.article.listCreateReply, new JSONObject(replyParams), new Response.Listener<JSONArray>() {
                        @Override
                        public void onResponse(JSONArray response) {
                            try {
                                for (int j = 0; j < response.length(); j++) {
                                    JSONObject reply = response.getJSONObject(j);
                                    outUserIds.addAll(helperParseRelaventUserIdsFromEntity(reply));
                                    outReplies.get(finalI).add(reply);
                                }
                            } catch (JSONException ex) {}
                        }
                    }, new Response.ErrorListener() {
                        @Override
                        public void onErrorResponse(VolleyError error) {}
                    });
                    queue.add(replyRequest);
                }
                success.run();

            } catch (JSONException ex) {}
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            outErrorMessage.append(error.getMessage());
            failure.run();
        }
    });
    queue.add(commentRequest);
}

Note that I am using success is executed right after I get all the comments, and before getting all the replies.

So how can I group them and delay the response?

I am working on the hairy implementation like

taskCount++;
if (taskCount == totalCount) {
    success.run();
} 

in reply block, but it seems very tedious.

fasteque
  • 4,309
  • 8
  • 38
  • 50
OMGPOP
  • 1,995
  • 8
  • 52
  • 95
  • 2
    Did you have a look at `RxJava`? This is the intended usecase for it. Async operations and operations on the results and intermediates. – David Medenjak Jan 23 '16 at 11:40
  • Why not using asynctasks? http://developer.android.com/reference/android/os/AsyncTask.html – Mikael Jan 27 '16 at 07:59
  • Sorry, but could you please explain, what's going on in your iOS code? Why do you even want to use `dispatch_group` in the first place? Whether you are sending or receiving something, doing things non-sequentially won't help much, as long as only single server is involved. And it certainly won't be of any benefit with mobile connections, which usually throttle number of concurrent network requests to 1. You may be better off just doing all of above in blocking mode, in a single background request. – user1643723 Jan 30 '16 at 06:53

5 Answers5

32

You can simply do it with this class I made to mimic the iOS behavior. Call enter() and leave() the same way you did in iOS with dispatch_group_enter and dispatch_group_leave and call notify() just after the requests you want to group, just like dispatch_group_notify. It also uses runnable the same way iOS uses blocks :

public class DispatchGroup {

    private int count = 0;
    private Runnable runnable;

    public DispatchGroup()
    {
        super();
        count = 0;
    }

    public synchronized void enter(){
        count++;
    }

    public synchronized void leave(){
        count--;
        notifyGroup();
    }

    public void notify(Runnable r) {
        runnable = r;
        notifyGroup();
    }

    private void notifyGroup(){
        if (count <=0 && runnable!=null) {
             runnable.run();
        }
    }
}

Hope it helps ;)

Damien Praca
  • 3,126
  • 21
  • 14
  • I would suggest skipping notifyGroup inside the notify method. This will cause problems if you have for instance nestled Api calls where you call more Api Calls on another's callbacks and you need to wait for them to finish. – Placeable Aug 29 '18 at 13:15
  • 1
    @Placeable skipping is not a good idea. What if no element entered the group? Your execution block will be never called. iOS does behaves like the code above. If no enter() was invoked, then the execution block is called immediately. – Baran Aug 22 '19 at 14:44
22

Here is the Kotlin version of Damien Praca's answer. This will allow you to use Kotlin lambdas like this.

val dispatchGroup = DispatchGroup()
dispatchGroup.enter()
// Some long running task
dispatchGroup.leave()

dispatchGroup.notify {
// Some code to run after all dispatch groups complete
}

class DispatchGroup {
    private var count = 0
    private var runnable: (() -> Unit)? = null

    init {
        count = 0
    }

    @Synchronized
    fun enter() {
        count++
    }

    @Synchronized
    fun leave() {
        count--
        notifyGroup()
    }

    fun notify(r: () -> Unit) {
        runnable = r
        notifyGroup()
    }

    private fun notifyGroup() {
        if (count <= 0 && runnable != null) {
            runnable!!()
        }
    }
}
Rammohan Raja
  • 616
  • 1
  • 6
  • 10
  • 2
    Thanks, I encourage you to add a time-out security to avoid to stay blocked if a leave() condition is not met. – Ben-J Feb 23 '22 at 15:48
3

You may use Threads and Thread.join() with Handlers as an option.

quote from:https://docs.oracle.com/javase/tutorial/essential/concurrency/join.html

The join method allows one thread to wait for the completion of another. If t is a Thread object whose thread is currently executing,

t.join(); causes the current thread to pause execution until t's thread terminates. Overloads of join allow the programmer to specify a waiting period. However, as with sleep, join is dependent on the OS for timing, so you should not assume that join will wait exactly as long as you specify.

Like sleep, join responds to an interrupt by exiting with an InterruptedException.

EDIT: You should also check my event dispatcher gist. You may like it.

Ercan
  • 3,705
  • 1
  • 22
  • 37
3

There is no direct analogue of dispatch_group in plain Java or Android. I can recommend a few rather sophisticated techniques to produce a really clean and elegant solution if you're ready to invest some extra time in it. It's not gonna be one or two lines of code, unfortunately.

  1. Use RxJava with parallelization. RxJava provides a clean way to dispatch multiple tasks, but it works sequentially by default. See this article to make it execute tasks concurrently.

  2. Although this is not exactly the intended usecase, you can try the ForkJoinPool to execute your group of tasks and recieve a single result afterwards.

Dmide
  • 6,422
  • 3
  • 24
  • 31
2

I use java.util.concurrent.CountDownLatch to achieve the goal.
First of all I made a interface for each task.

interface GroupTask {
    void onProcessing(final CountDownLatch latch);
}

Then I create a class to handle grouping tasks.

interface MyDisptchGroupObserver {
    void onAllGroupTaskFinish();
}
class MyDisptchGroup {
    private static final int MSG_ALLTASKCOMPLETED = 300;
    private CountDownLatch latch;
    private MyDisptchGroupObserver observer;

    private MsgHandler msgHandler;
    private class MsgHandler extends Handler {
        MsgHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case MSG_ALLTASKCOMPLETED:
                    observer.onAllGroupTaskFinish();
                    break;
                default:
                    break;
            }
        }
    }

    MyDisptchGroup(List<GroupTask> tasks, MyDisptchGroupObserver obj) {
        latch = new CountDownLatch(tasks.size());
        observer = obj;
        msgHandler = new MsgHandler(getActivity().getMainLooper())

        new Thread( new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    Log.d(TAG, "========= All Tasks Completed =========");
                    msgHandler.sendEmptyMessage(MSG_ALLTASKCOMPLETED);
                } catch() {
                    e.printStackTrace();
                }
            }
        }).start();

        for( GroupTask task : tasks ) {
            task.onProcessing(latch);
        }
    }
}

Of course I have more than one task implementation as the following. The Task1

class Task1 implements GroupTask {
    @Override
    public void onProcessing(final CountDownLatch latch) {
        new Thread( new Runnable() {
            @Override
            public void run() {
                // Just implement my task1 stuff here


                // The end of the Task1 remember to countDown
                latch.countDown();
            }
        }).start();
    }
}

And Task2

class Task2 implements GroupTask {
    @Override
    public void onProcessing(final CountDownLatch latch) {
        new Thread( new Runnable() {
            @Override
            public void run() {
                // Just implement my task2 stuff here


                // The end of the Task2 remember to countDown
                latch.countDown();
            }
        }).start();
    }
}

Now everything are ready to fire.

ArrayList<GroupTask> allTasks = new ArrayList<GroupTask>();
allTasks.add(new Task1());
allTasks.add(new Task2());
new MyDisptchGroup(allTasks, this);
Akabrando
  • 176
  • 1
  • 9