шаблон дизайна Голанга для отмены процедур в полете - PullRequest
6 голосов
/ 18 мая 2019

Я новичок в Голанге, который пытается понять правильный шаблон дизайна для этой проблемы. Мое текущее решение кажется очень многословным, и я не уверен, каким будет лучший подход.

Я пытаюсь разработать систему, которая:

  1. выполняет N горутин
  2. возвращает результат каждой процедуры, как только он станет доступен
  3. если программа возвращает определенное значение, она должна уничтожить другие программы, которые будут отменены.

Цель : я хочу запустить ряд процедур, но я хочу отменить процедуры, если одна процедура возвращает определенный результат.

Я пытаюсь понять, является ли мой код супер "вонючим" или это предписанный способ действий. Я до сих пор не чувствую себя хорошо, поэтому любая помощь будет признательна.

Вот что я написал:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {

    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)

    fooCheck := make(chan bool)
    barCheck := make(chan bool)

    go foo(ctx, 3000, fooCheck)
    go bar(ctx, 5000, barCheck)

    for fooCheck != nil ||
        barCheck != nil {
        select {
        case res, ok := <-fooCheck:
            if !ok {
                fooCheck = nil
                continue
            }
            if res == false {
                cancel()
            }
            fmt.Printf("result of foocheck: %t\n", res)
        case res, ok := <-barCheck:
            if !ok {
                barCheck = nil
                continue
            }
            fmt.Printf("result of barcheck: %t\n", res)
        }
    }
    fmt.Printf("here we are at the end of the loop, ready to do some more processing...")
}

func foo(ctx context.Context, pretendWorkTime int, in chan<- bool) {
    fmt.Printf("simulate doing foo work and pass ctx down to cancel down the calltree\n")
    time.Sleep(time.Millisecond * time.Duration(pretendWorkTime))

    select {
    case <-ctx.Done():
        fmt.Printf("\n\nWe cancelled this operation!\n\n")
        break
    default:
        fmt.Printf("we have done some foo work!\n")
        in <- false
    }
    close(in)
}

func bar(ctx context.Context, pretendWorkTime int, in chan<- bool) {
    fmt.Printf("simulate doing bar work and pass ctx down to cancel down the calltree\n")
    time.Sleep(time.Millisecond * time.Duration(pretendWorkTime))

    select {
    case <-ctx.Done():
        fmt.Printf("\n\nWe cancelled the bar operation!\n\n")
        break
    default:
        fmt.Printf("we have done some bar work!\n")
        in <- true
    }
    close(in)
}

(играть с кодом здесь: https://play.golang.org/p/HAA-LIxWNt0)

Вывод работает, как и ожидалось, но я боюсь, что я принимаю какое-то решение, которое сорвёт мою ногу позже.

Ответы [ 2 ]

2 голосов
/ 20 мая 2019

Я бы использовал один канал для передачи результатов, поэтому гораздо проще собирать результаты, и он "масштабируется" автоматически по своей природе.Если вам нужно определить источник результата, просто используйте оболочку, которая включает источник.Примерно так:

type Result struct {
    ID     string
    Result bool
}

Чтобы имитировать «настоящую» работу, работники должны использовать цикл, выполняющий свою работу итеративным образом, и в каждой итерации они должны проверять сигнал отмены.Примерно так:

func foo(ctx context.Context, pretendWorkMs int, resch chan<- Result) {
    log.Printf("foo started...")
    for i := 0; i < pretendWorkMs; i++ {
        time.Sleep(time.Millisecond)
        select {
        case <-ctx.Done():
            log.Printf("foo terminated.")
            return
        default:
        }
    }
    log.Printf("foo finished")
    resch <- Result{ID: "foo", Result: false}
}

В нашем примере bar() - это то же самое, просто замените все слова foo на bar.

А теперь выполняйте задания и завершайте остальныерано, если кто-то оправдывает наши ожидания, выглядит так:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

resch := make(chan Result, 2)

log.Println("Kicking off workers...")
go foo(ctx, 3000, resch)
go bar(ctx, 5000, resch)

for i := 0; i < cap(resch); i++ {
    result := <-resch
    log.Printf("Result of %s: %v", result.ID, result.Result)
    if !result.Result {
        cancel()
        break
    }
}
log.Println("Done.")

Запуск этого приложения приведет к выводу (попробуйте на Go Playground ):

2009/11/10 23:00:00 Kicking off workers...
2009/11/10 23:00:00 bar started...
2009/11/10 23:00:00 foo started...
2009/11/10 23:00:03 foo finished
2009/11/10 23:00:03 Result of foo: false
2009/11/10 23:00:03 Done.

Некоторыевещи на заметку.Если мы завершим досрочно из-за неожиданного результата, будет вызвана функция cancel(), и мы вырвемся из цикла.Может быть, остальные работники также завершают свою работу одновременно и отправляют свой результат, что не будет проблемой, поскольку мы использовали буферизованный канал, поэтому их отправка не будет блокироваться, и они будут правильно завершены.Кроме того, если они не завершаются одновременно, они проверяют ctx.Done() в своем цикле, и они завершаются рано, поэтому процедуры хорошо очищаются.

Также обратите внимание, что вывод приведенного выше кода не печатаетbar terminated.Это связано с тем, что функция main() завершается сразу после цикла, а после завершения функции main() она не ожидает завершения других не-1027 * подпрограмм.Подробнее см. Нет вывода из goroutine в Go .Если приложение не завершится немедленно, мы увидим и эту строку.Если мы добавим time.Sleep() в конце main():

log.Println("Done.")
time.Sleep(3 * time.Millisecond)

Вывод будет (попробуйте на Go Playground ):

2009/11/10 23:00:00 Kicking off workers...
2009/11/10 23:00:00 bar started...
2009/11/10 23:00:00 foo started...
2009/11/10 23:00:03 foo finished
2009/11/10 23:00:03 Result of foo: false
2009/11/10 23:00:03 Done.
2009/11/10 23:00:03 bar terminated.

Теперь, если вам нужно подождать, пока все рабочие закончатся «нормально» или «рано», прежде чем двигаться дальше, вы можете достичь этого разными способами.

Одним из способов является использование sync.WaitGroup.Для примера см. Предотвращение завершения функции main () до завершения выполнения программ в Golang .Другим способом было бы, чтобы каждый работник отправлял Result независимо от того, как они заканчиваются, и Result может содержать условие завершения, например, normal или aborted.И процедура main() может продолжать цикл приема до тех пор, пока не получит значения n из resch.Если выбрано это решение, вы должны убедиться, что каждый работник отправляет значение (даже если возникает паника), чтобы не блокировать main() в таких случаях (например, с использованием defer).

0 голосов
/ 20 мая 2019

Я собираюсь поделиться наиболее упрощенной схемой того, о чем вы говорите. Вы можете расширить его для более сложных сценариев.

func doStuff() {
    // This can be a chan of anything.
    msgCh := make(chan string)

    // This is how you tell your go-routine(s) to stop, by closing this chan.
    quitCh := make(chan struct{})
    defer close(quitCh)

    // Start all go routines.
    for whileStart() {
        go func() {
            // Do w/e you need inside of your go-routine.

            // Write back the result.
            select {
            case msgCh <- "my message":
                // If we got here then the chan is open.
            case <-quitCh:
                // If we got here then the quit chan was closed.
            }
        }()
    }

    // Wait for all go routines.
    for whileWait() {
        // Block until a msg comes back.
        msg := <-msgCh
        // If you found what you want.
        if msg == stopMe {
            // It's safe to return because of the defer earlier.
            return
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...