Ensuring task execution order in threadpool

后端 未结 17 936
情深已故
情深已故 2020-12-12 11:54

I have been reading about the thread-pool pattern and I can\'t seem to find the usual solution for the following problem.

I sometimes want tasks to be executed serial

相关标签:
17条回答
  • 2020-12-12 12:48

    Thread pools are good for cases where the relative order of the tasks doesn't matter, provided they all get done. In particular, it must be OK for them all to be done in parallel.

    If your tasks must be done in a specific order, then they are not suitable for parallelism, so a thread pool is not appropriate.

    If you want to move these serial tasks off the main thread, then a single background thread with a task queue would be appropriate for those tasks. You can continue to use a thread pool for the remaining tasks which are suitable for parallelism.

    Yes, it means you have to decide where to submit the task depending on whether it is an in-order task or a "may be parallelized" task, but this is not a big deal.

    If you have groups that must be serialized, but which can run in parallel with other tasks then you have multiple choices:

    1. Create a single task for each group, which does the relevant group tasks in order, and post this task to the thread pool.
    2. Have each task in a group explicitly wait for the previous task in the group, and post them to the thread pool. This requires that your thread pool can handle the case where a thread is waiting for a not-yet-scheduled task without deadlocking.
    3. Have a dedicated thread for each group, and post group tasks on the appropriate message queue.
    0 讨论(0)
  • 2020-12-12 12:48

    There is a java framework specifically for this purpose called dexecutor (disclaimer: I am the owner)

    DefaultDependentTasksExecutor<String, String> executor = newTaskExecutor();
    
        executor.addDependency("task1", "task2");
        executor.addDependency("task4", "task6");
        executor.addDependency("task6", "task8");
    
        executor.addIndependent("task3");
        executor.addIndependent("task5");
        executor.addIndependent("task7");
    
        executor.execute(ExecutionBehavior.RETRY_ONCE_TERMINATING);
    

    task1, task3, task5,task7 runs in parallel (Depending upon thread pool size), once task1 finishes, task2 runs, once task2 finishes task4 runs, once task4 finishes task6 runs and finally once task6 finishes task8 runs.

    0 讨论(0)
  • 2020-12-12 12:52

    If I understand the problem correctly, the jdk executors don't have this capability but it's easy to roll your own. You basically need

    • a pool of worker threads, each of which has a dedicated queue
    • some abstraction over those queues to which you offer work (c.f. the ExecutorService)
    • some algorithm that deterministically selects a specific queue for each piece of work
    • each piece of work then gets offers to the right queue and hence gets processed in the right order

    The difference to the jdk executors is that they have 1 queue with n threads but you want n queues and m threads (where n may or may not equal m)

    * edit after reading that each task has a key *

    In a bit more detail

    • write some code that transforms a key into an index (an int) in a given range (0-n where n is the number of threads you want), this could be as simple as key.hashCode() % n or it could be some static mapping of known key values to threads or whatever you want
    • at startup
      • create n queues, put them in an indexed structure (array, list whatever)
      • start n threads, each thread just does a blocking take from the queue
      • when it receives some work, it knows how to execute work specific to that task/event (you can obviously have some mapping of tasks to actions if you have heterogenous events)
    • store this behind some facade that accepts the work items
    • when a task arrives, hand it to the facade
      • the facade finds the right queue for the task based on the key, offers it to that queue

    it's easier enough to add auto restarting worker threads to this scheme, you just then need the worker thread to register with some manager to state "I own this queue" and then some housekeeping around that + detection of errors in the thread (which means it unregisters the ownership of that queue returning the queue to a free pool of queues which is a trigger to start a new thread up)

    0 讨论(0)
  • 2020-12-12 12:52

    You have two different kind of tasks. Mixing them up in a single queue feels rather odd. Instead of having one queue have two. For the sake of simplicity you could even use a ThreadPoolExecutor for both. For the serial tasks just give it a fixed size of 1, for the tasks that can be executed concurrently give it more. I don't see why that would be clumsy at all. Keep it simple and stupid. You have two different tasks so treat them accordingly.

    0 讨论(0)
  • 2020-12-12 13:01

    Something like the following will allow serial and parallel tasks to be queued, where serial tasks will be executed one after the other, and parallel tasks will be executed in any order, but in parallel. This gives you the ability to serialize tasks where necessary, also have parallel tasks, but do this as tasks are received i.e. you do not need to know about the entire sequence up-front, execution order is maintained dynamically.

    internal class TaskQueue
    {
        private readonly object _syncObj = new object();
        private readonly Queue<QTask> _tasks = new Queue<QTask>();
        private int _runningTaskCount;
    
        public void Queue(bool isParallel, Action task)
        {
            lock (_syncObj)
            {
                _tasks.Enqueue(new QTask { IsParallel = isParallel, Task = task });
            }
    
            ProcessTaskQueue();
        }
    
        public int Count
        {
            get{lock (_syncObj){return _tasks.Count;}}
        }
    
        private void ProcessTaskQueue()
        {
            lock (_syncObj)
            {
                if (_runningTaskCount != 0) return;
    
                while (_tasks.Count > 0 && _tasks.Peek().IsParallel)
                {
                    QTask parallelTask = _tasks.Dequeue();
    
                    QueueUserWorkItem(parallelTask);
                }
    
                if (_tasks.Count > 0 && _runningTaskCount == 0)
                {
                    QTask serialTask = _tasks.Dequeue();
    
                    QueueUserWorkItem(serialTask);
                }
            }
        }
    
        private void QueueUserWorkItem(QTask qTask)
        {
            Action completionTask = () =>
            {
                qTask.Task();
    
                OnTaskCompleted();
            };
    
            _runningTaskCount++;
    
            ThreadPool.QueueUserWorkItem(_ => completionTask());
        }
    
        private void OnTaskCompleted()
        {
            lock (_syncObj)
            {
                if (--_runningTaskCount == 0)
                {
                    ProcessTaskQueue();
                }
            }
        }
    
        private class QTask
        {
            public Action Task { get; set; }
            public bool IsParallel { get; set; }
        }
    }
    

    Update

    To handle task groups with serial and parallel task mixes, a GroupedTaskQueue can manage a TaskQueue for each group. Again, you do not need to know about groups up-front, it is all dynamically managed as tasks are received.

    internal class GroupedTaskQueue
    {
        private readonly object _syncObj = new object();
        private readonly Dictionary<string, TaskQueue> _queues = new Dictionary<string, TaskQueue>();
        private readonly string _defaultGroup = Guid.NewGuid().ToString();
    
        public void Queue(bool isParallel, Action task)
        {
            Queue(_defaultGroup, isParallel, task);
        }
    
        public void Queue(string group, bool isParallel, Action task)
        {
            TaskQueue queue;
    
            lock (_syncObj)
            {
                if (!_queues.TryGetValue(group, out queue))
                {
                    queue = new TaskQueue();
    
                    _queues.Add(group, queue);
                }
            }
    
            Action completionTask = () =>
            {
                task();
    
                OnTaskCompleted(group, queue);
            };
    
            queue.Queue(isParallel, completionTask);
        }
    
        private void OnTaskCompleted(string group, TaskQueue queue)
        {
            lock (_syncObj)
            {
                if (queue.Count == 0)
                {
                    _queues.Remove(group);
                }
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题