可能递归任务的工作池(即每个作业可以排队其他作业)

时间:2015-04-13 22:37:55

标签: concurrency go synchronization worker

我正在编写一个用户可以从多个"工作开始的应用程序" (实际上是URL)。在开头(主例程),我将这些URL添加到队列中,然后启动适用于这些URL的x goroutines。

在特殊情况下,URL指向的资源可能包含更多必须添加到队列的URL。 3名工人正在等待新的工作进来并加工。问题是:一旦每个工人都在等待工作(并且没有人正在生产任何工作),工人应该完全停止工作。所以要么所有这些都有效,要么没有人工作。

我目前的实现看起来像这样,我不认为它很优雅。不幸的是,我无法想出一个不会包含竞争条件的更好方法,而且我不完全确定这个实现是否真的按预期运行:

var queue // from somewhere
const WORKER_COUNT = 3
var done chan struct{}

func work(working chan int) {
  absent := make(chan struct{}, 1)
  // if x>1 jobs in sequence are popped, send to "absent" channel only 1 struct.
  // This implementation also assumes that the select statement will be evaluated "in-order" (channel 2 only if channel 1 yields nothing) - is this actually correct? EDIT: It is, according to the specs.
  one := false
  for {
    select {
    case u, ok := <-queue.Pop():
      if !ok {
        close(absent)
        return
      }
      if !one {
        // I have started working (delta + 1)
        working <- 1
        absent <- struct{}{}
        one = true
      }
      // do work with u (which may lead to queue.Push(urls...))
    case <-absent: // no jobs at the moment. consume absent => wait
      one = false
      working <- -1
    }
  }
}

func Start() {
  working := make(chan int)
  for i := 0; i < WORKER_COUNT; i++ {
    go work(working)
  }
  // the amount of actually working workers...
  sum := 0
  for {
    delta := <-working
    sum += delta
    if sum == 0 {
      queue.Close() // close channel -> kill workers.
      done <- struct{}{}
      return
    }
  }
}

有没有更好的方法来解决这个问题?

1 个答案:

答案 0 :(得分:1)

您可以use a sync.WaitGroup(请参阅docs)来控制工作人员的生命周期,并使用非阻塞发送,以便工作人员在尝试排队更多工作时不会死锁:

package main

import "sync"

const workers = 4

type job struct{}

func (j *job) do(enqueue func(job)) {
    // do the job, calling enqueue() for subtasks as needed
}

func main() {
    jobs, wg := make(chan job), new(sync.WaitGroup)
    var enqueue func(job)

    // workers
    for i := 0; i < workers; i++ {
        go func() {
            for j := range jobs {
                j.do(enqueue)
                wg.Done()
            }
        }()
    }

    // how to queue a job
    enqueue = func(j job) {
        wg.Add(1)
        select {
        case jobs <- j: // another worker took it
        default: // no free worker; do the job now
            j.do(enqueue)
            wg.Done()
        }
    }

    todo := make([]job, 1000)
    for _, j := range todo {
        enqueue(j)
    }
    wg.Wait()
    close(jobs)
}

似乎缓冲jobs通道可以防止死锁添加作业,但它不会:缓冲区可能会填满,然后你就会回到你开始的地方。缓冲很好,在某些情况下效率很高;它只是没有必要或足以防止僵局。

我在this function to kick off a parallel sort中遇到了这种情况,它的递归方式与您的网址提取方式相同。它可能比上面的例子更难阅读,因为有关排序的细节 - 比如将小任务与大任务区别对待 - 会混入其中。