确保线程池中的任务执行顺序

时间:2011-08-25 14:33:59

标签: multithreading design-patterns concurrency threadpool

我一直在阅读有关线程池模式的内容,我似乎无法找到解决以下问题的常用解决方案。

我有时希望连续执行任务。例如,我从文件中读取文本块,由于某种原因,我需要按顺序处理块。所以基本上我想消除某些任务的并发性

考虑这种情况,其中*的任务需要按照推入的顺序进行处理。其他任务可以按任何顺序处理。

push task1
push task2
push task3   *
push task4   *
push task5
push task6   *
....
and so on

在线程池的上下文中,没有这个约束,单个待处理任务队列工作正常,但显然不是。

我想过让某些线程在特定于线程的队列上运行,而其他线程在“全局”队列上运行。然后,为了以串行方式执行某些任务,我只需将它们推送到单个线程所在的队列中。 听起来有点笨拙。

所以,这个长篇故事中的真正问题是:你将如何解决这个问题? 您如何确保订购这些任务

修改

作为一个更普遍的问题,假设上面的场景变为

push task1
push task2   **
push task3   *
push task4   *
push task5
push task6   *
push task7   **
push task8   *
push task9
....
and so on

我的意思是组内的任务应该按顺序执行,但组本身可以混合使用。例如,您可以拥有3-2-5-4-7

另外需要注意的是,我无法预先访问组中的所有任务(我不能等到所有任务在启动组之前到达)。

感谢您的时间。

17 个答案:

答案 0 :(得分:17)

以下内容将允许串行和并行任务排队,其中串行任务将一个接一个地执行,并行任务将以任何顺序执行,但并行执行。这使您能够在必要时序列化任务,也可以执行并行任务,但是在接收任务时执行此操作,即您无需事先了解整个序列,动态维护执行顺序。

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; }
    }
}

<强>更新

要处理具有串行和并行任务组合的任务组,GroupedTaskQueue可以为每个组管理TaskQueue。同样,您不需要预先知道组,它们都是在接收任务时动态管理的。

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);
            }
        }
    }
}

答案 1 :(得分:14)

线程池适用于任务的相对顺序无关紧要的情况,只要它们全部完成即可。特别是,所有这些都可以并行完成。

如果您的任务必须按特定顺序完成,那么它们不适合并行性,因此线程池不合适。

如果要将这些串行任务移出主线程,则具有任务队列的单个后台线程将适合这些任务。您可以继续使用线程池来完成适合并行化的其余任务。

是的,这意味着你必须决定在哪里提交任务,具体取决于它是有序任务还是“可以并行化”任务,但这不是什么大问题。

如果您的组必须序列化,但可以与其他任务并行运行,那么您有多种选择:

  1. 为每个组创建一个任务,按顺序执行相关的组任务,并将此任务发布到线程池。
  2. 让组中的每个任务显式等待组中的上一个任务,并将它们发布到线程池中。这要求您的线程池可以处理线程正在等待尚未安排的任务而没有死锁的情况。
  3. 为每个组创建一个专用线程,并在适当的消息队列上发布组任务。

答案 2 :(得分:8)

基本上,有许多待定任务。某些任务只能在一个或多个其他待处理任务完成执行时执行。

待处理任务可以在依赖关系图中建模:

  • “任务1 - &gt; task2”表示“任务2只能在任务1完成后执行”。箭头指向执行顺序。
  • 任务的indegree(指向它的任务数)确定任务是否准备好执行。如果indegree为0,则可以执行。
  • 有时任务必须等待多个任务完成,然后<> li>
  • 如果任务不再需要等待其他任务完成(其indegree为零),则可以将其提交给具有工作线程的线程池,或者具有等待工作人员获取的任务的队列线。您知道提交的任务不会导致死锁,因为任务不会等待任何事情。作为优化,您可以使用优先级队列,例如将首先执行依赖图中的更多任务所依赖的任务。这也不会引发死锁,因为线程池中的所有任务都可以执行。然而,它可能引发饥饿。
  • 如果一个任务完成执行,它可以从依赖图中删除,可能会减少其他任务的不确定性,而这些任务又可以提交给工作线程池。

所以(至少)有一个线程用于添加/删除挂起的任务,并且有一个工作线程的线程池。

将任务添加到依赖关系图时,您必须检查:

  • 在依赖关系图中如何连接任务:必须等待完成哪些任务以及哪些任务必须等待它完成?相应地从新任务中绘制连接。
  • 绘制连接后:新连接是否会导致依赖关系图中的任何循环?如果是这样,就会出现僵局。

<强>性能

  • 如果并行执行实际上很少可能,那么这种模式比顺序执行要慢,因为无论如何你都需要额外的管理来完成所有事情。
  • 如果在实践中可以同时执行许多任务,则此模式很快。

<强>假设

正如您可能已经阅读过这些内容,您必须设计任务,以便它们不会干扰其他任务。此外,必须有一种方法来确定任务的优先级。任务优先级应包括每个任务处理的数据。两个任务可能不会同时改变同一个对象;其中一个任务应该优先于另一个任务,或者对象的执行操作必须是线程安全的。

答案 3 :(得分:6)

要执行您想要对线程池执行的操作,您可能必须创建某种调度程序。

类似的东西:

  

TaskQueue - &gt;调度程序 - &gt;队列 - &gt;线程池

调度程序在自己的线程中运行,跟踪作业之间的依赖关系。当一个作业准备好完成时,调度程序只是将它推送到线程池的队列中。

ThreadPool可能必须向调度程序发送信号以指示作业何时完成,以便调度程序可以根据该作业将作业放入队列。

在您的情况下,依赖项可能存储在链接列表中。

假设您有以下依赖项: 3 - &gt; 4 - &gt; 6 - &gt; 8

作业3正在线程池上运行,您仍然不知道作业8存在。

工作3结束。从链表中删除3,将作业4放在队列中的线程池中。

Job 8到了。你把它放在链表的末尾。

唯一必须完全同步的构造是调度程序之前和之后的队列。

答案 4 :(得分:4)

我认为在这种情况下可以有效地使用线程池。我们的想法是为每组依赖任务使用单独的strand对象。使用或不使用strand对象将任务添加到队列中。您使用具有相关任务的相同strand对象。您的计划程序会检查下一个任务是否包含strand以及此strand是否已锁定。如果不是 - 锁定此strand并运行此任务。如果strand已被锁定,请将此任务保留在队列中,直到下一个调度事件为止。任务完成后,解锁其strand

结果你需要单个队列,你不需要任何额外的线程,没有复杂的组等。strand对象可以非常简单,有两种方法lockunlock。< / p>

我经常遇到同样的设计问题,例如用于处理多个同时会话的异步网络服务器。当会话中的任务依赖时,会话是独立的(这将它们映射到您的独立任务和从属任务组)(这会将会话内部任务映射到组内的依赖任务)。使用描述的方法我完全避免会话内的显式同步。每个会话都有自己的strand对象。

而且,我使用这个想法的现有(伟大)实现:Boost Asio library(C ++)。我刚刚使用了他们的术语strand。实现很优雅:在安排它们之前,我我的异步任务包装到相应的strand对象中。

答案 5 :(得分:4)

如果我正确理解了这个问题,那么jdk执行程序就没有这个功能,但很容易自己动手。你基本上需要

  • 一个工作线程池,每个线程都有一个专用队列
  • 对您提供工作的队列进行一些抽象(c.f。ExecutorService
  • 某些确定性地为每项工作选择特定队列的算法
  • 然后,每项工作都会将商品提供给正确的队列,从而按正确的顺序进行处理

与jdk执行程序的区别在于它们有1个队列,包含n个线程,但是你需要n个队列和m个线程(其中n可能等于或不等于m)

*阅读后,编辑每个任务都有一个键*

更详细一点

  • 编写一些代码,将密钥转换为给定范围内的索引(int)(0-n,其中n是您想要的线程数),这可以像key.hashCode() % n一样简单,也可以将已知键值静态映射到线程或任何你想要的
  • 在启动时
    • 创建n个队列,将它们放入索引结构(数组,列出任何内容)
    • 启动n个线程,每个线程只从队列中执行阻塞,
    • 当它接收到一些工作时,它知道如何执行特定于该任务/事件的工作(如果你有异质事件,你显然可以将任务映射到操作)
  • 将其存放在接受工作项的某个外观后面
  • 任务到达时,将其交给立面
    • Facade根据密钥为任务找到正确的队列,将其提供给该队列

将自动重启工作线程添加到此方案更容易,然后您需要工作线程向某个管理器注册以声明“我拥有此队列”,然后围绕该线程进行一些内务处理+检测线程中的错误(这意味着它取消注册该队列的所有权,将队列返回到一个空闲的队列池,这是一个启动新线程的触发器)

答案 6 :(得分:3)

选项1 - 复杂的

由于您有连续作业,因此您可以在链中收集这些作业,并在作业完成后让作业本身重新提交到线程池。假设我们有一份工作清单:

 [Task1, ..., Task6]

就像在你的例子中一样。我们有一个顺序依赖,因此[Task3, Task4, Task6]是一个依赖链。我们现在做一份工作(Erlang伪代码):

 Task4Job = fun() ->
               Task4(), % Exec the Task4 job
               push_job(Task6Job)
            end.
 Task3Job = fun() ->
               Task3(), % Execute the Task3 Job
               push_job(Task4Job)
            end.
 push_job(Task3Job).

也就是说,我们通过将Task3作业包装到作为延续将队列中的下一个作业推送到线程池的作业来更改Node.js作业。与Twisted或Pythons defer框架等系统中也出现了一般延续传递风格的强烈相似之处。

概括,您创建一个系统,您可以在其中定义可以 Task = fun() -> Task3(), Task4(), Task6() % Just build a new job, executing them in the order desired end, push_job(Task). 进一步工作的工作链,并重新提交进一步的工作。

选项2 - 简单的

为什么我们甚至懒得分手?我的意思是,因为它们是顺序相关的,所以在同一个线程上执行所有它们不会比采用该链并在多个线程上传播它更快或更慢。假设“足够”的工作负载,任何线程总是会有工作,所以只需将作业捆绑在一起可能是最简单的:

jobs

如果你拥有一流公民的功能,那么你可以很容易地做这样的事情,这样你就可以随心所欲地用你的语言构建它们,比如说,任何函数式编程语言,Python,Ruby块 - 等等。

我并不特别喜欢构建队列或连续堆栈的想法,就像在“选项1”中那样,我肯定会选择第二种选择。在Erlang中,我们甚至有一个名为jobs的程序,由Erlang Solutions编写并作为开源发布。 {{1}}用于执行和加载调节这些作业执行。如果我要解决这个问题,我可能会将选项2与作业结合起来。

答案 7 :(得分:3)

建议不使用线程池的答案就像硬编码任务依赖/执行顺序的知识一样。相反,我会创建一个CompositeTask来管理两个任务之间的开始/结束依赖关系。通过将依赖关系封装在任务接口后面,可以统一处理所有任务,并将其添加到池中。这会隐藏执行细节并允许更改任务依赖关系,而不会影响您是否使用线程池。

问题没有指定语言 - 我将使用Java,我希望大多数人都能阅读。

class CompositeTask implements Task
{
    Task firstTask;
    Task secondTask;

    public void run() {
         firstTask.run();
         secondTask.run();
    }
}

这将按顺序并在同一线程上执行任务。您可以将许多CompositeTask链接在一起,根据需要创建一系列连续任务。

这里的缺点是,这会在顺序执行的所有任务的持续时间内占用线程。您可能希望在第一个和第二个任务之间执行其他任务。因此,不是直接执行第二个任务,而是让第二个任务执行复合任务计划:

class CompositeTask implements Runnable
{
    Task firstTask;
    Task secondTask;
    ExecutorService executor;

    public void run() {
         firstTask.run();
         executor.submit(secondTask);
    }
}

这确保第二个任务在第一个任务完成之后才会运行,并且还允许池执行其他(可能更紧急的)任务。请注意,第一个和第二个任务可以在不同的线程上执行,因此尽管它们不会同时执行,但任务使用的任何共享数据必须对其他线程可见(例如,通过生成变量volatile。)

这是一种简单但功能强大且灵活的方法,它允许任务本身定义执行约束,而不是通过使用不同的线程池来实现。

答案 8 :(得分:3)

使用两个Active Objects。用两个词来说:活动对象模式由优先级队列和1个或多个工作线程组成,它们可以从队列中获取任务并处理它。

因此,将一个活动对象与一个工作线程一起使用:将成为队列位置的所有任务将按顺序处理。使用第二个活动对象的工作线程数超过1.在这种情况下,工作线程将以任何顺序从队列中获取和处理任务。

运气。

答案 9 :(得分:2)

我认为你在混合概念。如果你想在线程之间分配一些工作,但是如果你开始在线程之间混合依赖关系,那么Threadpool就没问题了,那么这不是一个好主意。

我的建议,只是不要使用线程池来执行这些任务。只需创建一个专用线程并保留一个简单的顺序项队列,这些项目必须由该线程单独处理。然后,当您没有顺序要求时,可以继续将任务推送到线程池,并在有时使用专用线程。

澄清:使用常识,串行任务队列应由一个接一个地处理每个任务的线程执行:)

答案 10 :(得分:2)

就我理解你的情景而言,这是可以实现的。基本上你需要的是做一些聪明的事情来协调主线程中的任务。您需要的Java API是ExecutorCompletionServiceCallable

首先,实现你的可调用任务:

public interface MyAsyncTask extends Callable<MyAsyncTask> {
  // tells if I am a normal or dependent task
  private boolean isDependent;

  public MyAsyncTask call() {
    // do your job here.
    return this;
  }
}

然后在主线程中,使用CompletionService协调依赖任务执行(即等待机制):

ExecutorCompletionService<MyAsyncTask> completionExecutor = new 
  ExecutorCompletionService<MyAsyncTask>(Executors.newFixedThreadPool(5));
Future<MyAsyncTask> dependentFutureTask = null;
for (MyAsyncTask task : tasks) {
  if (task.isNormal()) {
    // if it is a normal task, submit it immediately.
    completionExecutor.submit(task);
  } else {
    if (dependentFutureTask == null) {
      // submit the first dependent task, get a reference 
      // of this dependent task for later use.
      dependentFutureTask = completionExecutor.submit(task);
    } else {
      // wait for last one completed, before submit a new one.
      dependentFutureTask.get();
      dependentFutureTask = completionExecutor.submit(task);
    }
  }
}

通过执行此操作,您使用单个执行程序(线程池大小5)执行正常任务和从属任务,一旦提交就立即执行正常任务,依赖任务逐个执行(等待在主线程中执行通过在提交新的依赖任务之前调用Future上的get(),所以在任何时候,你总是有一些正常的任务和一个在一个线程池中运行的依赖任务(如果存在)。

这只是一个先机,通过使用ExecutorCompletionService,FutureTask和Semaphore,您可以实现更复杂的线程协调场景。

答案 11 :(得分:1)

你有两种不同的任务。将它们混合在一个队列中感觉相当奇怪。而不是让一个队列有两个。为简单起见,您甚至可以使用ThreadPoolExecutor。对于串行任务,只需给它一个固定大小为1,对于可以同时执行的任务给它更多。我不明白为什么那会很笨拙。保持简单和愚蠢。你有两个不同的任务,所以相应地对待它们。

答案 12 :(得分:1)

由于您只需要在启动相关任务之前等待单个任务完成,因此如果您可以在第一个任务中安排相关任务,则可以轻松完成。所以在你的第二个例子中: 在任务2结束时,安排任务7 和 在任务3结束时,安排任务4,依此类推4-> 6和6->。

一开始,只安排任务1,2,5,9 ......其余的应该遵循。

更常见的问题是,在依赖任务启动之前必须等待多个任务。有效地处理这是一项非常重要的工作。

答案 13 :(得分:1)

  

您如何确保订购这些任务?

push task1
push task2
push task346
push task5

回应编辑:

push task1
push task27   **
push task3468   *
push task5
push task9

答案 14 :(得分:0)

有一个专门用于此目的的Java框架,名为dexecutor(免责声明:我是所有者)

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并行运行(根据线程池大小),一旦task1完成,task2运行,一旦task2完成task4运行,一旦task4完成task6运行,最后一次task6完成task8运行。

答案 15 :(得分:0)

有很多答案,显然已经接受了一个答案。但为什么不使用延续?

如果你有一个已知的&#34;序列&#34;条件,然后当你用这个条件排队第一个任务时,按住任务;并为进一步的任务调用Task.ContinueWith()。

public class PoolsTasks
{
    private readonly object syncLock = new object();
    private Task serialTask = Task.CompletedTask;


    private bool isSerialTask(Action task) {
        // However you determine what is serial ...
        return true;
    }

    public void RunMyTask(Action myTask) {
        if (isSerialTask(myTask)) {
            lock (syncLock)
                serialTask = serialTask.ContinueWith(_ => myTask());
        } else
            Task.Run(myTask);
    }
}

答案 16 :(得分:0)

具有有序和无序执行方法的线程池:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class OrderedExecutor {
    private ExecutorService multiThreadExecutor;
    // for single Thread Executor
    private ThreadLocal<ExecutorService> threadLocal = new ThreadLocal<>();

    public OrderedExecutor(int nThreads) {
        this.multiThreadExecutor = Executors.newFixedThreadPool(nThreads);
    }

    public void executeUnordered(Runnable task) {
        multiThreadExecutor.submit(task);
    }

    public void executeOrdered(Runnable task) {
        multiThreadExecutor.submit(() -> {
            ExecutorService singleThreadExecutor = threadLocal.get();
            if (singleThreadExecutor == null) {
                singleThreadExecutor = Executors.newSingleThreadExecutor();
                threadLocal.set(singleThreadExecutor);
            }
            singleThreadExecutor.submit(task);
        });
    }

    public void clearThreadLocal() {
        threadLocal.remove();
    }

}

填充所有队列后,应清除threadLocal。 唯一的缺点是每次方法

都会创建singleThreadExecutor
  

executeOrdered(可运行任务)

在单独的线程中调用