Отметка — это решение, которое может упростить разработку управляемого событиями симулятора. Он предоставляет опыт разработки моделирования на основе циклов, но при этом может поддерживать производительность моделирования, управляемого событиями.

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

Моделирование, управляемое событиями, обычно более эффективно. Когда компонент простаивает, он не планирует события, избегая ненужных вызовов функции tick. Однако, как мы видели в предыдущем руководстве, реализация моделирования, управляемого событиями, требует много строк кода. Логика компонента легко становится трудной для понимания в моделировании, управляемом событиями.

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

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

Сначала мы определяем специальный тип компонента с именем TickingComponent. Отмечающий компонент должен встраивать структуру TickingCompnent. TickingComponent нужно только реализовать функцию `Tick`. Функция tick принимает текущее время в качестве аргумента и возвращает логическое значение. Возвращаемое значение указывает, выполняется ли компонент в текущем тике или нет.

Вот пример реализации PingAgent как TickingComponent.

type TickingPingAgent struct {
    *akita.TickingComponent
}
func NewTickingPingAgent(
    name string,
    engine akita.Engine,
    freq akita.Freq,
) *TickingPingAgent {
     a := &TickingPingAgent{}
    a.TickingComponent = akita.NewTickingComponent(
        name, engine, freq, a)
    return a
}
func (a *TickingPingAgent) Tick(now akita.VTimeInSec) bool {
    panic(“not implemented”)
}

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

Остановить галочку, когда компонент не продвигается вперед, не влияет на результат моделирования. Две причины могут привести к тому, что компонент перестанет работать. Во-первых, компонент простаивает. У него нет задачи для запуска. В этом случае ставить галочку не нужно. Другая причина в том, что весь исходящий порт перегружен. В этом случае компонент не будет работать дальше, пока соединение не освободится. Поскольку он не будет прогрессировать, ему не нужно продолжать тикать.

Когда компонент получает новое сообщение, он больше не простаивает. Когда порты уведомляют компонент о том, что соединение свободно, соединение больше не перегружено. В обоих случаях нам нужно позволить компоненту продолжать тикать. Помните функцию NotifyRecv и функцию NotifyPortFree? Они предназначены для этих целей. TickingComponent реализует эти две функции, планируя следующий TickEvent. Таким образом, хотя мы и не удаляем все ненужные галочки, мы можем избежать большинства из них. Мы также гарантируем, что не пропустим ни одного тика, который может привести к прогрессу. Таким образом, результат моделирования эквивалентен традиционному планированию на основе циклов.

Далее давайте подробнее рассмотрим, как реализована функция Tick. Это все еще пример TickingPingAgent.

func (a *TickingPingAgent) Tick(now akita.VTimeInSec) bool {
    madeProgress := false
    madeProgress = a.sendRsp(now) || madeProgress
    madeProgress = a.sendPing(now) || madeProgress
    madeProgress = a.countDown() || madeProgress
    madeProgress = a.processInput(now) || madeProgress
    
    return madeProgress
}

В тиковой функции мы делим компонент на несколько более мелких этапов. Вся функция тика возвращает true (прогресс достигнут), если какой-либо из этапов продвинулся вперед.

В этой реализации агент А создает пинг-сообщение на этапе sendPing и отправляет его по сети на 1-м цикле. Предполагая, что соединение является идеальным соединением с нулевой задержкой, агент B, получатель, может получить сообщение на этапе processInput во 2-м цикле. Поскольку мы устанавливаем задержку на 2 цикла, сообщение остается на этапе countDown агента B в течение 3-го и 4-го циклов. На 5-м цикле агент B отправляет ответ агенту A на этапе sendRsp. Наконец, агент А может обработать ответ в цикле 6. Таким образом, сквозная задержка составляет 5 циклов.

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

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

Далее: Перехват и отслеживание