Java 中跨不同任务但路径不同的多线程

时间:2021-07-06 11:30:36

标签: java multithreading

我有一个队列,生产者将在其中提交任务。每个任务都有一个有效负载和一个文件路径。在消费者方面,我有一个 Executor 线程池,它应该从队列中提取任务并将它们分配给线程。以下是一些需要遵循的约束条件:

  1. 我们不能让多个线程处理同一个文件路径
  2. 我们希望为特定文件路径添加的任务按顺序完成

这是问题陈述的示例。让我们假设以下是队列的状态。这里 T1 是线程名称,括号包含文件路径。
head -> [T1(A),T2(B),T3(C),T4(A),T5(B),T6(A),T7(A),T8(C)] <- tail

因此我们可以让三个线程同时处理文件路径 ABC 的任务。但是,以下不应该发生。这里文件路径 A 有两个线程同时处理它。
Pool1-Thread1-T4(A)
Pool1-Thread2-T5(B)
Pool1-Thread3-T6(A)
文件路径的数量可以介于 10k-50k

为了解决这个问题,我想出的方法是有两个映射,即 Map<String, Queue<Tasks>> 用于任务队列和 Map<String, AtomicBoolean> 用于令牌,两者都有针对 file 的键路径。一个正在运行的消费者线程,它将不断迭代 Tasks Map,并且需要在为该路径创建线程之前获取该文件路径的标记(布尔值应为 true)。线程完成后,它会返回令牌(通过将布尔值标记回 true)。

问题

  • 这是该用例的最佳方法还是我们可以做得更好和/或更简单?
  • 这种方法中是否有任何多线程问题可能会在以后咬我?

编辑

  1. 阐明如何提取任务。我将有一个正在运行的消费者线程,它将不断迭代 Tasks Map。如果它找到任何可用的任务和令牌,它就会从执行器生成一个新线程。
  2. 任务本身很小,但传入任务的吞吐量会很高。这里的文件路径代表克隆的 git repo,任务可以是例如添加文件、修改文件、提交和推送到远程。

2 个答案:

答案 0 :(得分:0)

我会避免使用 Executor 线程池并使用以下方法:

  • 创建一个带有 BlockingXXXQueue 的类(选择实现)和一个不断从这个队列中消费的线程。它显然会消耗任务。当它获取一个任务时,它会执行它。这可以保证队列中的任务按顺序执行,并且最多执行该队列中的一个。
  • 现在创建一些将任务映射到队列的逻辑。例如。如果您不想同时执行相同类型的任务,请映射它们 都在同一个队列中。例如,A 将始终进入队列 1,B 将始终进入队列 2,等等......它遵循前面提到的属性,这样没有 2 个 A 类型的任务将并行进行。

一种常见的方法是将所有队列放在一个数组中,然后根据某个值以队列数为模进行分配,这就是调用分区,例如:

queues = new Queue[N];

typeOfTask = task.getType() (to int somehow, for example you can hash or use the char if it is a single letter)

queueIndex = typeTask % queues.length

queues[queueIndex].push(task)
  • 在上面的类中,您可以在将任务传递给线程之前获取任务时执行任何逻辑,就像您提到的那样。

最后一点,在使用文件系统时,我会避免使用多个线程(如果有的话),因为磁盘是...一个。因此,并行处理它没有什么意义,除非您必须写入大量不想缓冲的数据。相反,线程更多地是为了能够使用更多处理器并行工作以提高速度或在执行阻塞操作时。

答案 1 :(得分:0)

我已经尝试实现消费者用例的一部分,似乎问题多于解决方案。请阅读代码中的内嵌注释。

public class Consumer implements Runnable {

    private volatile boolean isStopped;
    private final Map<String, Queue<Task>> tasks;
    private final Map<String, AtomicBoolean> tokens;
    //Preferably FixedThreadService
    private final ExecutorService service;


    public Consumer(Map<String, Queue<Task>> tasks, Map<String, AtomicBoolean> tokens, ExecutorService service) {
        this.tasks = tasks;
        this.tokens = tokens;
        this.service = service;
        isStopped = false;
    }

    public void stopConsumer(){
        isStopped = true;
    }


    @Override
    public void run() {
        while(!isStopped){
            // tasks map should have latest values because producer is going to update this map
            // frequently. Should have to use ConcurrentHashMap.
            Set<String> keys = tasks.keySet();
            for ( String key:tasks.keySet() ) {
                if(!tokens.get(key).get()){
                    continue;
                }
              try {
                  //This does not mean the task is in progress, just submitted for execution.
                  service.execute(tasks.get(key).poll());
                  //Now that we submitted the task for exicution, we can mark the token to false.
                  tokens.get(key).compareAndSet(true, false);
                  // But at what point are we going to mark token to True. If this token is updated
                  // From any other thread than consumer, it will create race condition.
              }catch(RejectedExecutionException e){
                  //What will happen to the polled task. Task is removed from the queue.
                  
                  //Token we can mark to true, but task has already polled from queue.
                  // Shall we put it back to the head of the queue and how ?
                  tokens.get(key).compareAndSet(false, true);
                  //What if the task is not completed smoothly. T1 is not completed successfully,
                  // what is backup plan, because next task will start any way and corrupt the file.

              }
            }

        }
    }
}

如果我们能把 1. 添加文件、2、修改文件、3. 提交和 4. 推送到远程等任务隔离开来,也许我们可以想出更好的方法。但在这种情况下也会有限制。

最好采用单线程方法。

相关问题