Изучение компонентов Go — таймер cron

Go

1. Введение

Прошло почти три месяца с тех пор, как я перешел на Go, и я нашел условное отражение, которое принадлежит Go при написании бизнес-кода.

Постдекларация и многопараметрический возврат Код в стиле Go не так хромает для написания и даже немного адаптируется~

Наоборот, когда я несколько дней назад писал Java, я понял, почему Java так медленно запускается, как Java может терпеть эти неиспользуемые коды и лежать там уверенно... Подождите, где вы слышали подобные слова? ? ?

«Почему Go публикует заявление, как неловко»

«Почему в Go должно быть определено так много структур, у меня голова кружится»

...

На самом деле нет лучшего языка, есть только самый подходящий.

В предыдущей серии «Изучение языка Go» в основном были представлены некоторые базовые знания о Go и некоторые новые функции по сравнению с Java. Если в последующих действиях появится соответствующий опыт и новые, мы продолжим их обновлять.

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

2 Введение в крон

robfig/cron — это сторонняя библиотека планирования задач с открытым исходным кодом, которую мы обычно называем временными задачами.

Гитхаб:github.com/robfig/cron

Официальная документация:перейдите на doc.org/GitHub.com/…

3 Как использовать крон

1. Создайте новый файл cron-demo.go

package main

import (
	"fmt"
	"github.com/robfig/cron"
	"time"
)

func main() {
	c := cron.New()
	c.AddFunc("*/3 * * * * *", func() {
		fmt.Println("every 3 seconds executing")
	})

	go c.Start()
	defer c.Stop()


	select {
	case <-time.After(time.Second * 10):
		return
	}
}
  • cron.New создает менеджер таймеров
  • c.AddFunc добавляет временную задачу, первый параметр — это выражение времени cron, а второй параметр — функция, запускающая выполнение.
  • go c.Start() запустить новую сопрограмму для запуска задач по времени
  • c.Stop - дождаться стоп-сигнала для завершения задачи.

2. Запустите сборку go под файлом cron-demo.go.

Этот проект использует go mod для управления пакетами, поэтому после выполнения команды go build в файле go.mod будет сгенерирована соответствующая версия зависимости, как показано на рисунке.

3. Запустите cron-demo.go

Видно, что он выполняется каждые 3 секунды, пока процесс не истечет и не завершится через 10 секунд, а задача завершится.

См. код проекта: go-demo project (GitHub.com/D майнер Джек и…)

Кажется, что это планирование задач довольно простое в использовании, так как же оно реализовано?После прочтения исходного кода, он также очень короткий и лаконичный.Структура каталогов выглядит следующим образом.

Давайте посмотрим, как cron реализует планирование задач, ответив на несколько вопросов.

4 Как cron анализирует выражения задач

В приведенном выше примере мы видим, что добавление выражения типа «*/3 * * * * *» может выполняться каждые 3 секунды.

Очевидно, что это выражение является просто удобной для людей формой контрактного выражения.Чтобы действительно выполнить задачу в указанное время, cron должен прочитать и разобрать это c-выражение, преобразовать его в конкретное время, а затем выполнить.

Итак, давайте посмотрим, как это работает.

Введите реализацию функции AddFunc

// AddFunc adds a func to the Cron to be run on the given schedule.
func (c *Cron) AddFunc(spec string, cmd func()) error {
	return c.AddJob(spec, FuncJob(cmd))
}

Это всего лишь оболочка, и в нее нужно ввести функцию AddJob.

// AddJob adds a Job to the Cron to be run on the given schedule.
func (c *Cron) AddJob(spec string, cmd Job) error {
	schedule, err := Parse(spec)
	if err != nil {
		return err
	}
	c.Schedule(schedule, cmd)
	return nil
}

Первая строка функции — разобрать выражение cron, следуем по вайну, конкретную реализацию видим следующим образом

// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
// It accepts crontab specs and features configured by NewParser.
func (p Parser) Parse(spec string) (Schedule, error) {
	if len(spec) == 0 {
		return nil, fmt.Errorf("Empty spec string")
	}
	if spec[0] == '@' && p.options&Descriptor > 0 {
		return parseDescriptor(spec)
	}

	// Figure out how many fields we need
	max := 0
	for _, place := range places {
		if p.options&place > 0 {
			max++
		}
	}
	min := max - p.optionals

	// Split fields on whitespace
	fields := strings.Fields(spec)	// 使用空白符拆分cron表达式

	// Validate number of fields
	if count := len(fields); count < min || count > max {
		if min == max {
			return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)
		}
		return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)
	}

	// Fill in missing fields
	fields = expandFields(fields, p.options)

	var err error
	field := func(field string, r bounds) uint64 {	// 抽象出filed函数,方便下面调用
		if err != nil {
			return 0
		}
		var bits uint64
		bits, err = getField(field, r)
		return bits
	}

	var (
		second     = field(fields[0], seconds)
		minute     = field(fields[1], minutes)
		hour       = field(fields[2], hours)
		dayofmonth = field(fields[3], dom)
		month      = field(fields[4], months)
		dayofweek  = field(fields[5], dow)
	)
	if err != nil {
		return nil, err
	}

	return &SpecSchedule{
		Second: second,
		Minute: minute,
		Hour:   hour,
		Dom:    dayofmonth,
		Month:  month,
		Dow:    dayofweek,
	}, nil
}

Эта функция в основном сопоставляет выражение cron со структурой SpecSchedule с 6 временными измерениями «Секунда, Минута, Час, Дом, Месяц, Доу».

SpecSchedule — это структура, которая реализует метод «Следующий (время.Время) время.Время», а «Следующий (время.Время) время.Время» определяется в интерфейсе расписания.

// The Schedule describes a job's duty cycle.
type Schedule interface {
	// Return the next activation time, later than the given time.
	// Next is invoked initially, and then each time the job is run.
	Next(time.Time) time.Time
}

Следовательно, можно наконец понять, что cron разбирается и преобразуется в следующий раз для выполнения, ожидая выполнения.

5 Как cron выполняет задачи

Мы знаем, что parser.go может преобразовывать выражения, хорошо понятные людям, во время выполнения, понятное cron.

С временными точками, которые должны быть выполнены, как именно cron выполняет эти задачи?

Давайте посмотрим на конкретную реализацию функции Start.

// Start the cron scheduler in its own go-routine, or no-op if already started.
func (c *Cron) Start() {
	if c.running {
		return
	}
	c.running = true
	go c.run()
}

Здесь мы определим, запускать ли задачу, определив, запущено ли рабочее поле Cron.

Очевидно, что running здесь ложно, потому что running устанавливается в false при вызове инициализации c.New.

Итак, вот вновь вошел в соглашение о выполнении временных задач, а затем вы можете увидеть Humps, мы видим реализацию функции запуска.

// Run the scheduler. this is private just due to the need to synchronize
// access to the 'running' state variable.
func (c *Cron) run() {
	// Figure out the next activation times for each entry.
	now := c.now()
	for _, entry := range c.entries {
		entry.Next = entry.Schedule.Next(now)
	}

	for {
		// Determine the next entry to run.
		sort.Sort(byTime(c.entries))

		var timer *time.Timer
		if len(c.entries) == 0 || c.entries[0].Next.IsZero() {	// 如果没有要执行的任务或者第一个任务的待执行时间为空,则睡眠
			// If there are no entries yet, just sleep - it still handles new entries
			// and stop requests.
			timer = time.NewTimer(100000 * time.Hour)
		} else {
			timer = time.NewTimer(c.entries[0].Next.Sub(now))	// 否则新建一个距离现在到下一个要触发执行的Timer
		}

		for {
			select {
			case now = <-timer.C:	// 触发时间到,执行任务
				now = now.In(c.location)
				// Run every entry whose next time was less than now
				for _, e := range c.entries {
					if e.Next.After(now) || e.Next.IsZero() {
						break
					}
					go c.runWithRecovery(e.Job)
					e.Prev = e.Next
					e.Next = e.Schedule.Next(now)
				}

			case newEntry := <-c.add:	// 添加任务
				timer.Stop()
				now = c.now()
				newEntry.Next = newEntry.Schedule.Next(now)
				c.entries = append(c.entries, newEntry)

			case <-c.snapshot:	// 调用c.Entries()返回一个现有任务列表的snapshot
				c.snapshot <- c.entrySnapshot()
				continue

			case <-c.stop:	// 任务结束,退出
				timer.Stop()
				return
			}

			break
		}
	}
}
  • Войдите в эту функцию, сначала пройдите все задачи, а в следующий раз найдите время для выполнения всех задач.
  • Затем войдите во внешний цикл for, отсортируйте каждую задачу по времени выполнения и убедитесь, что ближайшая к текущему времени выполняется первой.
  • Затем судите по списку задач, есть ли задача, если нет, спите, иначе инициализируйте таймер.

Внутренний цикл for является изюминкой Нижеследующее в основном анализирует добавление и выполнение задач в этом цикле for.

Перед этим нужно разобраться с таймером стандартной библиотеки go

Таймер используется для указания функции или выражения, которое должно быть вызвано через определенный интервал времени.

Используя NewTimer, вы можете создать таймер, который может получать значения через

package main

import (
	"fmt"
	"time"
)

func main() {
	timer1 := time.NewTimer(2 * time.Second)

	<-timer1.C
	fmt.Println("Timer 1 expired")

	timer2 := time.NewTimer(time.Second)
	go func() {
		<-timer2.C
		fmt.Println("Timer 2 expired")
	}()

	stop2 := timer2.Stop()
	if stop2 {
		fmt.Println("Timer 2 stopped")
	}
}

Результат выполнения

Timer 1 expired
Timer 2 stopped

timer1 означает, что он истечет через 2 секунды.До этого он находится в состоянии блокировки.Через 2 секунды

timer2 указывает, что он истечет через 1 секунду, но он останавливается Stop в середине, что эквивалентно очистке функции синхронизации.

На этом фоне давайте посмотрим на внутренний цикл for функции run.

c.Добавить полученный канал

case newEntry := <-c.add:	// 添加任务
	timer.Stop()
	now = c.now()
	newEntry.Next = newEntry.Schedule.Next(now)
	c.entries = append(c.entries, newEntry)

Остановите таймер, очистите установленную функцию синхронизации и начните с текущего момента времени, установите следующее время выполнения добавленной задачи и добавьте ее в очередь задач записей.

получен канал timer.C

case now = <-timer.C:	// 触发时间到,执行任务
	now = now.In(c.location)
    // Run every entry whose next time was less than now
    for _, e := range c.entries {
    	if e.Next.After(now) || e.Next.IsZero() {
    		break
    	}
    go c.runWithRecovery(e.Job)
    e.Prev = e.Next
    e.Next = e.Schedule.Next(now)
    }

Когда запланированная задача прибудет, time.C получит значение и запустит новую сопрограмму для выполнения задания, которое действительно необходимо выполнить, а затем обновит список следующих задач для выполнения.

Входим в функцию runWithRecovery, что видно из названия функции, даже если случится паника, ее можно будет восстановить снова, чтобы другие задачи не пострадали.

func (c *Cron) runWithRecovery(j Job) {
	defer func() {
		if r := recover(); r != nil {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.Stack(buf, false)]
			c.logf("cron: panic running job: %v\n%s", r, buf)
		}
	}()
	j.Run()
}

Проследив источник, мы обнаружили, что реальное выполнение Job — это выполнение j.Run(). Войдя в реализацию этой функции Run, мы видим

func (f FuncJob) Run() { f() }

Правильно, задача, которую мы хотим выполнить, была передана из AddFunc до тех пор, пока здесь мы не вызовем функцию Run для выполнения обернутой функции типа FuncJob в форме f().

Здесь может быть расплывчато, например определение замыкания в Go

func () {
    fmt.Println("test")
}()

Если здесь после определения нет «()», функция не будет выполняться, поэтому проще понять, как вышеописанная задача синхронизации выполняется в сочетании с этим.

6 Опыт чтения кода

1. Тайна канала

Через канал восприятие можно облегчить, например, timer.C — это как когда время вышло, и кто-то постучит в дверь, чтобы сказать вам. Нам не нужно проявлять инициативу, чтобы узнать, истек ли срок ее действия.

2. Использование общих библиотек классов

Например, в парсере мы видим «fields := strings.Fields(spec)». В повседневной разработке мы можем гибко использовать эти API, чтобы не создавать собственные колеса.

3. Больше думайте

Когда я раньше занимался Java, я был больше погружен в использование различных инструментов и фреймворков и мало внимания уделял реализации этих инструментов и фреймворков. Например, при переходе от Quartz к Spring Job нам нужно обновить все более и более простой в использовании инструмент для выполнения задач на время, и базовое обновление реализации Spring помогло нам в этом. Такой удобный для бизнеса проект может быстро реализовать разработку бизнес-функции, но он не является дружественным для разработчиков, а дружественный дизайн парализует желание разработчиков изучать лежащие в его основе принципы.