Сервер Игры на MS Orleans — часть 3: Итоги

Привет, Хабр! Я продолжаю изучать MS Orleans и делать простенькую онлайн игру с консольным клиентом и сервером работающим с Orleans грейнами. На этот раз я расскажу чем все закончилось и какие я для себя выводы сделал. За подробностями добро пожаловать под кат.

Таки да, если вам интересно вообще как игровые сервера для динамических игр делаются, а не мой эксперимент с MS Orleans то рекомендую глянуть этот репозиторий (UDP) и эти статьи почитать:

  1. habr.com/ru/post/303006
  2. habr.com/ru/post/328118
  3. habr.com/ru/company/pixonic/blog/499642
  4. habr.com/ru/company/pixonic/blog/420019

Содержание

Исходники

MsOrleansOnlineGame

О игре

Получилась простенькая стрелялка. Зеленые # это противники. Желтый # это ваш персонаж. Красный $ это пуля. Стрельба ведется в том направлении куда вы идете. Направление движения регулируется кнопками W A S D или стрелочками. Для выстрела предназначена клавиша пробела. Подробно описывать код клиента не вижу смысла потому что его нужно заменить на нормальный. Графический.

O акторах (грейнах)

Если кратко: Мое ИМХО что Орлеанс это gRPC на стероидах заточенный под Azure, масштабирование и работу с ин мемори стейтом. С кешем например. Хотя и без стейта как обычный RPC через Stateless Worker Grains умеет он работать. Грейн (Актор) в Орлеанс может выступать в роли точки входа как Controller в Asp.Net. Но в отличии от Контроллера у грейна один единственный инстанс у которого есть свой идентификатор. Грейны хороши тогда когда вам из нескольких потоков или от нескольких пользователей надо одновременно работать с каким-то состоянием. Они обеспечивают потокобезопасную работу с ним.

Например вот актор для корзины товаров. При первом вызове он будет создан и будет висеть в памяти играя роль кеша. При этом к нему могут одновременно делать запросы и на добавление и удаление предметов тысячи пользователей из тысячи разных потоков. Вся работа с его состоянием внутри него будет абсолютно потокобезопасной. При этом конечно было бы полезно сделать актор Shop у которого будет метод List GetBaskets() чтобы получать список всех доступных в системе корзин. При этом Shop тоже будет висеть в памяти как кеш и вся работа с ним будет потокобезопасной.

    public interface IBasket : IGrainWithGuidKey     {         Task Add(string item);         Task Remove(string item);         Task<List<string>> GetItems();     }      public class BasketGrain : Grain, IBasket     {         private readonly ILogger<BasketGrain> _logger;         private readonly IPersistentState<List<string>> _store;          public BasketGrain(             ILogger<BasketGrain> logger,             [PersistentState("basket", "shopState")] IPersistentState<List<string>> store         )         {             _logger = logger;             _store = store;         }           public override Task OnActivateAsync()         {              var shop = GrainFactory.GetGrain<IShop>();            //Добавляем в список корзин нашу если ее еще нет в списке.             await shop.AddBasketIfNotContains(this.GetPrimaryKey())             return base.OnActivateAsync();         }          public override async Task OnDeactivateAsync()         {           //Орлеанс автоматически активирует грейны когда мы их вызываем          // Так же как Asp.Net создает контроллеры.          // В отличии от контроллера грейн висит в памяти пока его кто-то использует.         // Если его долго ник-то не вызывает то Орлеанс убивает грейн.         //Перед тем как это сделать вызывается автоматически этот стандартный метод.      // Тут мы записываем состояние нашего грейна в БД             await _store.WriteStateAsync();             await base.OnDeactivateAsync();         }           public Task Add(string item)         {             _store.State.Add(item);             return Task.CompletedTask;         }          public Task Remove(string item)         {             _store.State.Remove(item);             return Task.CompletedTask;         }          public Task<List<string>> GetItems()         {            //Грейны сериализуют отправляемые и десереализуют принимаемые значения.           // Поэтому лучше из грейна возвращать копию его состояния          // Чтобы во время сериализации не выскочила ошибка ака Коллекшн хаз чейнджед             return Task.FromResult(new List<string>(_store.State));         }     } 

Пример использования в каком нибудь консольном приложении:

         private static async Task DoClientWork(IClusterClient client, Guid baskeId)         {             var basket = client.GetGrain<IBasket>(baskeId);            //как и с gRPC - на самом деле это действие отправит запрос на сервер где и произойдет добавление строки в список             await basket.Add("Apple");         } 

Код игры

Карта на которой сражаются игроки:

   public interface IFrame : IGrainWithIntegerKey     {         Task Update(Frame frame);         Task<Frame> GetState();     }      public class FrameGrain : Grain, IFrame     {         private readonly ILogger<FrameGrain> _logger;         private readonly IPersistentState<Frame> _store;          public FrameGrain(             ILogger<FrameGrain> logger,             [PersistentState("frame", "gameState")] IPersistentState<Frame> store         )         {             _logger = logger;             _store = store;         }          public override Task OnActivateAsync()         {             _logger.LogInformation("ACTIVATED");            //Связь игры и карты 1 к 1 поэтому айди карты и игры одинаковы.             _store.State.GameId = this.GetPrimaryKeyLong();             return base.OnActivateAsync();         }          public override async Task OnDeactivateAsync()         {             _logger.LogInformation("DEACTIVATED");             await _store.WriteStateAsync();             await base.OnDeactivateAsync();         }          public Task Update(Frame frame)         {             _store.State = frame;             return Task.CompletedTask;         }          public Task<Frame> GetState() => Task.FromResult(_store.State.Clone());     } 

Грейн игры который хранит общее состояние текущей игры и 20 раз в секунду отправляет его клиенту по SignalR.

    public interface IGame : IGrainWithIntegerKey     {         Task Update(Player player);         Task Update(Bullet bullet);         Task<List<Player>> GetAlivePlayers();     }      public class GameGrain : Grain, IGame     {         private const byte WIDTH = 100;         private const byte HEIGHT = 50;         private readonly ILogger<GameGrain> _logger;         private readonly IPersistentState<Game> _store;         private readonly IHubContext<GameHub> _hub;         private IDisposable _timer;         public GameGrain(             ILogger<GameGrain> logger,             [PersistentState("game", "gameState")] IPersistentState<Game> store,             IHubContext<GameHub> hub             )         {             _logger = logger;             _store = store;             _hub = hub;         }          public override async Task OnActivateAsync()         {             _store.State.Id = this.GetPrimaryKeyLong();             _store.State.Frame = new Frame(WIDTH, HEIGHT) { GameId = _store.State.Id };             var frame = GrainFactory.GetGrain<IFrame>(_store.State.Id);             await frame.Update(_store.State.Frame.Clone());             _logger.LogWarning("ACTIVATED");             //Тут происходит регистрация таймера который каждые 50 миллисекунд будет дергать метод нашего грейна. Это метод отправляет текущее состояние игры клиенту.             _timer = RegisterTimer(Draw, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(50));             await base.OnActivateAsync();         }          public override async Task OnDeactivateAsync()         {             _logger.LogWarning("DEACTIVATED");             _timer?.Dispose();             _timer = null;             await _store.WriteStateAsync();             await base.OnDeactivateAsync();         }          public async Task Draw(object obj)         {             var state = _store.State;             state.Bullets.RemoveAll(b => !b.IsAlive);             state.Players.RemoveAll(p => !p.IsAlive);             try             {                 await _hub.Clients.All.SendAsync("gameUpdated", state.Clone());             }             catch (Exception e)             {                 _logger.LogError(e, "Error on send s");             }         }          public Task Update(Player player)         {             _store.State.Players.RemoveAll(x => x.Id == player.Id);             _store.State.Players.Add(player);             return Task.CompletedTask;         }         public Task Update(Bullet bullet)         {             _store.State.Bullets.RemoveAll(x => x.Id == bullet.Id);             _store.State.Bullets.Add(bullet);             return Task.CompletedTask;         }          public Task<List<Player>> GetAlivePlayers() =>             Task.FromResult(_store.State.Players.Where(p => p.IsAlive).Select(p => p.Clone()).ToList());     } 

SignalR хаб через который мы общаемся с клиентом. Он выступает в роли прокси между WebGl клиентом и Orleans. Пока что клиент консольный и он дико стремный. Я хочу сделать в будущем веб клиент игры в браузере на Three.js и поэтому нужно подключение по вебсокету SignalR. Сам Orleans клиент только на C# в отличии от gRPC которые доступен на многих языках поэтому для веб клиентом между сервером Orleans и клиентами надо ставить прокси (Gateway asp.net core).

    public class GameHub : Hub     {         private readonly IGrainFactory _client;          public GameHub(IGrainFactory client)         {             _client = client;         }          public async Task GameInput(Input input)         {             var player = _client.GetGrain<IPlayer>(input.PlayerId);             await player.Handle(input);         }     } 

Грейн игрока. Он автоматически по таймеру движется и реагирует на команды пользователя. Если приходит команда стрелять то он создает грейн пули и устанавливает для него направление движения.

    public class PlayerGrain : Grain, IPlayer     {         private readonly ILogger<PlayerGrain> _logger;         private readonly IPersistentState<Player> _store;         private IDisposable _timer;         private readonly Queue<Input> _inputs;         public PlayerGrain(             ILogger<PlayerGrain> logger,             [PersistentState("player", "gameState")] IPersistentState<Player> store         )         {             _logger = logger;             _store = store;             _inputs = new Queue<Input>();         }          public override Task OnActivateAsync()         {             _logger.LogInformation("ACTIVATED");             // State это просто POCO класс с геттерами и сеттерами. Entity Player в нашем случае             _store.State.Id = this.GetPrimaryKey();             _timer = RegisterTimer(Update, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(200));             return base.OnActivateAsync();         }          public override async Task OnDeactivateAsync()         {             _logger.LogInformation("ACTIVATED");             _timer?.Dispose();             _timer = null;             await _store.WriteStateAsync();             await base.OnDeactivateAsync();         }          public async Task Handle(Input input)         {             _store.State.GameId = input.GameId;             _inputs.Enqueue(input);         }          public async Task Update(object obj)         {             if (!_store.State.IsAlive)             {                 await _store.ClearStateAsync();                //Говорим серверу Орлеас что можно удалить этот грейн из оперативной памяти.              // потому что он нам больше не нужен. Это произойдет после выхода из этого метода.                 DeactivateOnIdle();                 return;             }              while (_inputs.Count > 0)             {                 var input = _inputs.Dequeue();                 foreach (var direction in input.Directions.Where(d => d != Direction.None))                 {                     _store.State.Direction = direction;                 }                  foreach (var command in input.Commands.Where(c => c != Command.None))                 {                     if (command == Command.Shoot)                     {                         var bulletId = Guid.NewGuid();                         var bullet = GrainFactory.GetGrain<IBullet>(bulletId);                         // Метод Shot() просто возвращает направление куда смотрит игрок и место где он стоит.                         bullet.Update(_store.State.Shot()).Ignore(); //Ignore() эвейтит таску и игнорирует ошибку если она возникает                     }                 }             }             _store.State.Move();             if (_store.State.GameId.HasValue)             {                 var frame = GrainFactory.GetGrain<IFrame>(_store.State.GameId.Value);                 var fs = await frame.GetState();                 if (fs.Collide(_store.State))                     _store.State.MoveBack();                 GrainFactory.GetGrain<IGame>(_store.State.GameId.Value)                     .Update(_store.State.Clone())                     .Ignore();             }         }          public async Task Die()         {             _store.State.IsAlive = false;             if (_store.State.GameId.HasValue)                 await GrainFactory.GetGrain<IGame>(_store.State.GameId.Value).Update(_store.State.Clone());             await _store.ClearStateAsync();             DeactivateOnIdle();         }     } 

Грейн пули. Она автоматически движется по таймеру и если сталкивается с игроком то приказывает ему умереть. Если сталкивается с препятствием на карте то умирает сама.

  public interface IBullet : IGrainWithGuidKey     {         Task Update(Bullet dto);     }      public class BulletGrain : Grain, IBullet     {         private readonly ILogger<BulletGrain> _logger;         private readonly IPersistentState<Bullet> _store;         private IDisposable _timer;         public BulletGrain(             ILogger<BulletGrain> logger,             [PersistentState("bullet", "gameState")] IPersistentState<Bullet> store         )         {             _logger = logger;             _store = store;         }          public Task Update(Bullet dto)         {             _store.State = dto;             _store.State.Id = this.GetPrimaryKey();             return Task.CompletedTask;         }          public override Task OnActivateAsync()         {             _logger.LogInformation("ACTIVATED");             _timer = this.RegisterTimer(Update, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(50));             return base.OnActivateAsync();         }          public override async Task OnDeactivateAsync()         {             _logger.LogInformation("DEACTIVATED");             _timer?.Dispose();             _timer = null;             await _store.WriteStateAsync();             await base.OnDeactivateAsync();         }          public async Task Update(object obj)         {             if (!_store.State.IsAlive)             {                 await _store.ClearStateAsync();                 DeactivateOnIdle();                 return;             }             _store.State.Move();             if (_store.State.GameId.HasValue)             {                 var frame = GrainFactory.GetGrain<IFrame>(_store.State.GameId.Value);                 var fs = await frame.GetState();                 if (fs.Collide(_store.State))                     _store.State.IsAlive = false;                 if (_store.State.Point.X > fs.Width || _store.State.Point.Y > fs.Height)                     _store.State.IsAlive = false;                 var game = GrainFactory.GetGrain<IGame>(_store.State.GameId.Value);                 var players = await game.GetAlivePlayers();                 foreach (var player in players)                 {                     if (player.Collide(_store.State))                     {                         _store.State.IsAlive = false;                         GrainFactory.GetGrain<IPlayer>(player.Id).Die().Ignore();                         break;                     }                 }                 game.Update(_store.State.Clone()).Ignore();             }         }     } 

ссылка на оригинал статьи https://habr.com/ru/post/499556/

Геймдев, киберпанк и вечная философия: 3 мини-сериала на майские

Впереди много, очень много выходных… И если вы еще думаете чем заняться, то вот еще одна идея — посмотреть классный сериал. Сегодня публикуем мини-подборку 2020 года, которая погрузит вас в технологичный «О дивный новый мир» (ну почти). Все сериалы так или иначе связаны с IT и наукой — ну а как без этого!



Разрабы (Devs)

IMDb 7.8/10
Общее время просмотра: 7 часов, 20 минут

Сан-Франциско, недалекое будущее. Cобытия разворачиваются на базе вымышленного IT-гиганта Amaya, которым управляет загадочный главный исполнительный директор Форрест. После успешной презентации собственного проекта директору разработчик Сергей Павлов получает должность в тайном подразделении, которое занимается квантовыми компьютерами. Он очень рад, что наконец-то узнает, над чем именно работает отдел. Через день Сергей так и не появляется дома: его обугленное тело находят недалеко от главного офиса Amaya…

Мрачный восьмисерийный сериал о Силиконовой долине будущего от режиссера «Из машины» и «Аннигиляции» Алекса Гарленда. Паранойя, шпионаж, кибергонка супердержав, темные прослойки IT — главные хештеги сериала.

Посреди ландшафта бодрых, здравомыслящих ситкомов о нердах и айтишниках «Разрабы» оставляют тревожное впечатление. Сериал местами очень изящно приводит к неуютной мысли — всякое достижение техгаев будет милитаризироваться и украдкой утекать в Пентагоны и MI-6. Сегодняшние хипстерские пет-френдли-офисы с гранолой и комбучей мутируют в мегакорпорации, где не самые верные сотрудники будут заканчивать в пакетах.

Esquire

Devs remains a gloriously handsome watch. A show about people working with computers might sound like a dry proposition, but Garland constantly expands and expands, intercutting the action with stunning, atmospheric shots of San Francisco, reminding us of the wider world outside of the insular universe the characters are inhabiting.

The Guardian


Мистический квест: Пир ворона (Mythic Quest: Raven’s Banquet)

IMDb 7.5/10
Общее время просмотра: 4 часа, 30 минут

Сериал о вымышленной студии разработки видеоигр, которая работает над популярной многопользовательской игрой «Мифический квест».

Глава компании, ее идейный лидер, царь всех игр, король над разработкой, шеф-повар удачных локаций, да и вообще крутой мужик Йен Гримм постоянно придумывает новые дополнения, генерирует бесконечные идеи, а его команде приходится мириться с его эгоцентризмом и по возможности воплощать его задумки в жизнь. Здесь весело обыгрывают то, что происходит в игровой индустрии. А главное, чтобы понимать все эти шутки и отсылки не нужно быть магистром в области видеоигр.

Макс и сериалы

The show is about a bunch of misanthropes working together, with comedy derived from the dark ways their toxic relationships push and pull each other around. You don’t need to know how a studio works to recognize an office hierarchy when you see one, or an egomaniac boss, or a shitty coworker that skates on your hard work.

The Verge


Видоизменённый углерод (Altered Carbon), 2018-2020

IMDb 8.1/10
Общее время просмотра: 18 часов (2 сезона)

Netflix выпустил второй сезон сериала «Видоизменённый углерод» по книгам Ричарда Моргана. Нашумевший проект совмещает в себе стилистику киберпанка, боевик и закрученный детектив.

Действие развивается в мире будущего, где люди научились переносить сознание в электронные стеки и использовать тела только как оболочки. В первом сезоне бывший военный Такеши Ковач проснулся спустя 250 лет после смерти своего тела. Ему дали новую оболочку, и наёмник взялся за расследование убийства мафа — богатого долгожителя, который может клонировать собственное тело

Качественная фантастика с отличным экшеном, любопытными героями, непростой философией и увлекательной новой вселенной с массой возможностей.

Lifehacker

While the first season had other problems, including a crude obsession with human bodies that never developed beyond voyeurism, Kinnaman’s rigid, no-fun lead skewed the self-serious drama toward a profound bore instead of a galaxy-brained lark. The series’ ideas are big, playful, and (like so much great sci-fi) topically applicable when following a consistent allegory, but all that potential remained trapped behind a stoic visage.

Indie Wire


Сериалы хорошие, но в душе май, дача и шашлыки? Для таких мы тоже не остались в стороне. Подготовили для вас специальные фоны для онлайн-трансляций в Zoom — все прелести мая, не выходя из дома.

На шашлыки!

Загородный отдых

Вечеринка

После вечеринки

Сохраняйте и ставьте под настроение: и фоны, и сериалы. Хороших выходных!

ссылка на оригинал статьи https://habr.com/ru/company/yamoney/blog/499802/

Я перехожу на JavaScript

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

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

Мне не хотелось бы писать материал о том, почему я перешёл с Go на JavaScript, перечисляя минусы Go. Я полагаю, что подобные материалы оторваны от жизни и приносят читателям очень мало пользы. Поэтому я написал материал о том, что мне нравится в JavaScript, о том, что подвигло меня на переход.

Система типов и работа с числами

В JavaScript очень долго не было целочисленного типа данных. В языке были только какие-то странные числа двойной точности, которые иногда сносно играли роль целых чисел. Теперь всё уже не так. Теперь в JavaScript есть и целые числа, и массивы целых чисел. Разные значения такого рода можно преобразовывать друг в друга. Всё это делает JS языком с самыми лучшими числовыми типами.

var sb = new ArrayBuffer(4) var intArr = new Int32Array(sb) intArr[0] = 1 << 16 // 65536 intArr[0] // 65536 

Это — именно то, чего я долго ждал: точные вычисления. Очень приятно знать, что выбранный тобой язык поддерживает такие возможности.

Но учитывайте то, что JavaScript на этом не останавливается, так как вышеозначенные возможности вполне обычны во многих других языках. Мы можем использовать Int32Array как Int16Array, и всё будет работать так, как ожидается, без выдачи исключений:

var sb = new ArrayBuffer(4) var intArr = new Int32Array(sb) intArr[0] = 1 << 16 // 65536 intArr[0] // 65536 // Тут я использую тот же буфер var shortArray = new Int16Array(sb) shortArray[0] // 0 shortArray[1] // 1 

Вы можете тут спросить: «Зависит ли результат от порядка следования байтов в системе, в которой выполняется код? А если так — почему я пользуюсь архитектурой, где сначала идёт старший байт?». А я на это отвечу: «Спасибо, что спросили. Хороший вопрос».

Теперь поговорим о ещё одной замечательной особенности этих целых чисел. А именно, переполнения, в отличие от любого другого языка, обрабатываются именно так, как этого можно ожидать:

var shortArr = new Int16Array(1) var c = shortArr[0] = 1 << 15 // бонус: приятное множественное присваивание c == shortArr[0] // false shortArr[0] // -32768 c // 32768 

Полагаю, что к этому моменту я уже продемонстрировал мощь чисел в JavaScript, но, чтобы окончательно прояснить мою мысль, приведу ещё примеры.

Оператор получения остатка от деления (%) можно применять при работе с числами с плавающей точкой. И ведёт себя этот оператор именно так, как он и должен работать:

3.14 % 5 // 3.14 13.14 % 5 // 3.1400000000000006 

Кроме того, массивы целых чисел легко сортировать, что называется, «на месте». При этом программисту даже не надо видеть стандартного кода, выполняющего сортировку. Это выгодно отличает JavaScript от многих языков со строгой статической типизацией:

[-2, -7, 0.0000001, 0.0000000006, 6, 10].sort() // [-2, -7, 10, 1e-7, 6, 6e-10] А синтаксис языка всегда чёток и интуитивно понятен: (1,2,3,4,5,6) === (2,4,6) true 

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

var a = "41" a += 1 // 411, неправильно, несбалансированно, странно. var b = "41" b -=- 1 // 42, а вот использование этой симметричной конструкции приводит к просто замечательному результату 

Тут я обнаружил лишь одну вещь, которую трудно удержать в голове (но мне нужно будет к этому привыкнуть). Дело в том, что при работе с датами нужно учитывать то, что нумерация месяцев начинается с 0, а нумерация всего остального — с 1. Ничего другого, такого, что стоит запомнить, я не нашёл.

Подробности о типах

Как оказалось, самая красота JavaScript — это, бесспорно, его система типов. Поэтому я потрачу тут ещё немного времени на рассказ о том, что мне удалось выяснить в ходе экспериментов с языком или благодаря знатокам JavaScript из Twitter.

Благодаря новой возможности языка, представленной оператором ??, теперь можно писать такой код:

~~!![[]]||__``&&$$++<<((""??''))**00==ಠಠ--//\\ // 1 

Нужна строка banana? Внимательно относитесь к расстановке пробелов:

('b'+'a'++'a'+'a').toLowerCase() // Uncaught SyntaxError: Invalid left-hand side expression in postfix operation ('b' + 'a' + + 'a' + 'a').toLowerCase() // "banana" 

Нравится писать «однострочники»? JavaScript вам в этом поможет:

// Лучший код - это краткий код input ?? obj?.key ? 'yes' : 'no' 

А вот — моё любимое. Я вообще без ума от регулярных выражений. А в JavaScript есть много такого, что никому не даст скучать:

var re = /a/g re.test('ab') // true re.test('ab') // false re.test('ab') // true 

Операторы в JS иногда не отличаются коммутативностью. Помните об этом:

{property: "value"} && {property: "value"} // {property: "value"} Date() && Date() // "Wed Apr 1 2020 00:01:00 GMT+0100 (Central European Standard Time)" Date() && {property: "value"} // {property: "value"} {property: "value"} && Date() // Uncaught SyntaxError: Unexpected token '&&' 

Типы могут меняться в самых неожиданных местах, что помогает программисту не заснуть во время долгих рабочих ночей:

typeof([1][0]) // number for(let i in [1]){console.log(typeof(i))} // string 

Даже значения могут меняться только от того, что мы к ним обращаемся. Это, опять же, помогает продуктивно трудиться:

const x={   i: 1,   toString: function(){     return this.i++;   } }  if(x==1 && x==2 && x==3){   document.write("This will be printed!") } 

Я могу тут приводить и другие примеры, демонстрирующие лёгкость чтения и понимания JavaScript-кода, но я закончу этот раздел следующим примером, который предусматривает работу с DOM:

document.all // HTMLAllCollection(359) [html.inited, head, …] document.all == true // false document.all == false // false 

Углубляемся в кроличью нору

Теперь представляю вам более серьёзный раздел этого материала. Обычно мне нравится лезть вглубь тех инструментов и языков, которыми пользуюсь. Лучший способ с чем-то разобраться — задавать вопросы коллегам и друзьям.

Я недавно начал работать с одним человеком. Он прислал мне этот прекрасный фрагмент JS-кода:

(function({substr}){return substr.call})("") // function call() var x = (function({substr}){return substr.call})("") // undefined x.name // "call" x + "" // "function call() { //     [native code] // }" typeof x // "function" x("this is a string") // TypeError: Function.prototype.call called on incompatible undefined 

На этом он не остановился и прислал мне ещё вот это:

(function(){return this}.call(1)) == (function(){return this}.call(1)) // false 

Последняя капля: конкурентность

Когда я дошёл до этого, я, в общем-то, уже был уверен в том, что мне необходимо перейти на JavaScript.

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

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

Так как я иду в JavaScript из Go — мне было очень легко понять причину, по которой этот код три раза выводит 3:

for (i = 1; i <= 2; ++i) {   setTimeout(function(){     console.log(i);   }, 0); } 

Но я должен признать, что результаты работы следующего фрагмента кода, на первый взгляд, не отличаются тем же уровнем интуитивной понятности:

console.log(0) setTimeout(_ => console.log(1), 0) requestAnimationFrame(_ => console.log(2)) Promise.resolve().then(_ => console.log(3)) console.log(4) // 0 // 4 // 3 // 2 // 1 

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

Полагаю, зная это, ничего не стоит понять механизмы конкурентного исполнения кода в JavaScript. А как насчёт параллелизма?

Вот код, которым я собираюсь тут пользоваться. Благодарю за него codediodeio.

// Этот код предназначен для логирования сведений о прошедшем времени: const start = Date.now(); function log(v){console.log(`${v} \n Elapsed: ${Date.now() - start}ms`);} // Тут запускаем тяжёлую задачу, блокирующую текущий поток function doWork(){   for(let i = 0; i < 1000000000; i++); // обратите внимание на точку с запятой   return 'work done'; } log('before work'); doWork(); log('after work'); // before work //  Elapsed: 0ms // after work //  Elapsed: 624ms 

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

Давайте теперь попробуем воспользоваться промисом:

function doWork(){   return new Promise((resolve, reject) => {     for(let i = 0; i < 1000000000; i++);     resolve('work done');   }) } log('before work'); doWork().then(log); log('after work'); // before work //  Elapsed: 0ms // after work //  Elapsed: 637ms // work done //  Elapsed: 637ms 

Здесь мы сталкиваемся с той же проблемой. Кто-то может тут возразить, сказав, что проблема заключается в том, что код всё ещё выполняется как «макро-задача», или в том, что данная задача блокирует всё тот же поток. Поэтому давайте переработаем код так, чтобы весь тяжёлый цикл выполнялся бы в виде «микро-задачи»:

function doWork(){   // Добавление `resolve` приводит к выполнению этого в задаче другого вида   return Promise.resolve().then(v =>  {     for(let i = 0; i < 1000000000; i++);     return 'work done';   }) } log('before work'); doWork().then(log); requestAnimationFrame(()=>{log('time to next frame')}); log('after work'); // before work //  Elapsed: 0ms // after work //  Elapsed: 1ms // work done //  Elapsed: 631ms // time to next frame //  Elapsed: 630ms 

Если посмотреть на первые строки вывода, то возникнет такое ощущение, что проблему мы решили. Но это не так. Мы просто переместили работу в следующий доступный слот выполнения в главном потоке. Мы, может быть, смогли выполнить какой-то объём нашего синхронного кода быстрее, но асинхронный код всё ещё влияет на время, необходимое на обработку кадра, в результате приложение не реагирует на воздействия пользователя примерно полсекунды.

Это — кое-что такое, что меня прямо-таки поразило: если у нас имеются тяжёлые вычисления, которые нужно выполнить в JavaScript, то они заблокируют главный поток и замедлят интерфейс приложения. Можно найти множество видеоматериалов и учебных руководств о том, как пользоваться конструкцией async/await или промисами для решения этой задачи, но все они будут не в тему. Эти примитивы дают возможность конкурентного, а не параллельного выполнения кода. Единственный вид промисов, которые я смог запустить в настоящем параллельном режиме, это встроенные промисы браузера, вроде fetch. А то, что описано в коде, никогда не будет выполняться параллельно.

Как же выполнять код в параллельном режиме? Надо создать веб-воркер — нечто вроде потока из других языков. Это — отличная новая возможность, которая вводит в JavaScript так необходимое в этом языке состояние гонок параллельное выполнение кода.

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

Так как в JavaScript теперь есть эта замечательная возможность, это значит, что в языке должны быть предусмотрены механизмы, представленные примитивами для оркестрации или синхронизации асинхронных задач. Верно? Верно. Язык даёт нам postMessage и MessageChannel (это похоже на chan), что очень хорошо. Если пользоваться только этими механизмами, в вашем коде никогда не случится «гонка потоков», в этом коде будет очень легко ориентироваться, о нём будет легко рассуждать. Чудно.

Однако если вам нужно что-то более производительное, что-то такое, чему не нужно вызывать события и ожидать планирования коллбэков, если вам нужно, чтобы что-то работало по-настоящему быстро, тогда к вашим услугам SharedArrayBuffer. Речь идёт о фрагментах памяти, которые можно совместно использовать в разных потоках, выполняя над данными из этих фрагментов атомарные операции. Нет ни Mutex, ни select, ни WaitGroup. Если вам нужны эти примитивы — вам понадобится самим их написать (как я). Как вам это понравится? Этот API даже возвращает значения, которые невозможно использовать правильно!

Да, в конце концов, кому понравится лёгкая работа‽ (Это — вопроцательный знак, который пока ещё не относится к числу операторов JavaScript).

Итоги

JavaScript — это зрелый язык, который даёт мне всё, что мне нужно: конкурентность, параллелизм, строго типизированные переменные. Более того, JS оживляет программирование и делает работу веселей, выдавая всякие непредсказуемые и таинственные вещи без каких-либо дополнительных библиотек, что называется, «из коробки». В результате тому, кто пишет на JavaScript, не будет скучно при отладке. Что тут может не понравиться?

Уважаемые читатели! Планируете ли вы переходить на JavaScript с того языка, на котором пишете сейчас?

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/499670/

Эффект кнута и пивная игра: моделирование и обучение управлению поставками

Кнут и игра

В данной статье хотел бы обсудить широко исследованную в логистике проблему эффекта кнута (bullwhip effect), а также представить вниманию преподавателей и специалистов в области управления поставками новую модификацию известной пивной игры (beer game) для обучения логистике. Пивная игра в науке управления запасами (Supply Chain Management) это на самом деле серьезная тема в образовании и практике логистики. Она хорошо описывает неконтролируемый процесс изменчивости заказов и разбухания складских запасов на разных этапах цепей поставок — так называемый эффект кнута. Столкнувшись однажды с трудностями в моделировании эффекта кнута, я решил разработать свою упрощенную версию пивной игры (далее — новая игра). Зная, как много специалистов по логистике на данном сайте, а также учитывая, что комментарии к статьям на Хабр часто бывают интересней самих статей, очень хотелось бы послушать комментарии читателей об актуальности эффекта кнута и пивной игры.

Реальная или надуманная проблема?

Начну с описания эффекта кнута. Существуют тонны научных исследований в логистике, которые рассматривали эффект кнута как важный результат взаимодействия партнеров в цепях поставок, который имеет серьезные управленческие последствия. Эффект кнута — это увеличение изменчивости заказов на начальных этапах цепочки поставок (upstream), что является одним из основных теоретических [1] [2] и экспериментальных результатов пивной игры [3]. Согласно эффекту кнута, колебания спроса со стороны потребителей и заказов ритейлеров на конечных этапах цепочки поставок (downstream) всегда ниже чем у оптовиков и производителей. Эффект, конечно, вреден и приводит к частым изменениям в заказах и производстве. Математически, эффект кнута можно описать как соотношение дисперсий или коэффициентов вариации между этапами (эшелонами) цепи поставок:

BullwhipEffect=VARupstream/VARdownstream

Или (зависит от методики исследователя):

BullwhipEffect=CVupstream/CVdownstream

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

Теоретически, эффект может быть сглажен за счет замещения товаров и переключения клиентов между поставщиками в случае дефицита [4]. Некоторые эмпирические данные подтверждают мнение о том, что эффект кнута может быть ограничен во многих отраслях [5]. Производители и продавцы часто применяют методы сглаживания производства и другие уловки для того, чтобы изменчивость заказов клиентов не была слишком сильной. А интересно: как обстоят дела с эффектом кнута в России и вообще на постсоветском пространстве? Замечали ли читатели (особенно те, кто занимается аналитикой запасов и прогнозированием спроса) такой уж сильный эффект в реальной жизни? Может и в самом деле вопрос эффекта кнута надуман и зря на него столько времени исследователей и студентов логистики потрачено…

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

Это Вам не игрушки…

Моделирование с использованием электронных таблиц широко используется для анализа реальных бизнес-задач. Электронные таблицы также эффективны при подготовке будущих менеджеров. Эффект кнута как известная область в управлении цепочками поставок имеет особенно давнюю традицию использования моделирования в образовании, хорошим примером которого является пивная игра. MIT впервые представил оригинальную пивную игру в начале 1960-х годов, и вскоре она стала популярным инструментом для объяснения динамики цепочки поставок. Игра представляет собой классический пример модели динамики систем (System Dynamics), используемую не только в образовательных целях, но и для принятия решений в реальных бизнес-ситуациях, а также для исследований. Наглядность, воспроизводимость, безопасность, экономичность и доступность серьезных компьютерных игр представляют собой альтернативу обучению на рабочем месте, предоставляя менеджерам полезный инструмент в облегчении принятию решений при проведении экспериментов в безопасной учебной среде.

Игра сыграла важную роль в моделировании для разработки бизнес-стратегий и облегчения принятий решений. Классическая пивная игра представляла собой настольную игру и требовала значительной подготовки перед проведением игры в классе. Преподаватели должны сначала были решать такие проблемы, как сложные инструкции, настройки и ограничения для участников игры. Последующие версии пивной игры пытались облегчить её применение с помощью информационных технологий. Несмотря на значительные улучшения с каждой последующей версией, сложность настройки и реализации, особенно в многопользовательских настройках, во многих случаях препятствовала широкому практическому применению игры в бизнес образовании. Обзор доступных версий пивной игр для симуляции в управлении цепочками поставок показывает отсутствие легкодоступных и бесплатных инструментов для преподавателей в данной области. Я хотел в новой игре под названием Соревнование цепей поставок (Supply Chain Competition Game) адресовать прежде всего именно эту проблему. С точки зрения педагогики, новую игру можно охарактеризовать как инструмент для проблемного обучения (PBL), в котором симулятор сочетается с ролевой игрой. Также есть возможность использования онлайн версии новой игры в Google Sheets. Подход условного форматирования в модели цепочки поставок в виде электронных таблиц позволяет решить две основные проблемы в применении серьезных игр: доступность и простота использования. Данная игра уже пару лет доступна для загрузки по следующей ссылке на общедоступном веб-сайте.

Подробное описание на английском можно скачать здесь.

Краткое описание игры

Вкратце об этапах проведения игры.

Один пользователь, отвечающий за проведение игрового сеанса (далее преподаватель), и минимум четыре пользователя, играющих в игру (далее именуемые игроками) вместе, представляют участников в пивной игре. Новая игра моделирует одну или две цепочки поставок, каждая из которых состоит из четырех этапов: ритейлер ®, оптовый продавец (W), дистрибьютор (D) и завод (F). Реальные сети поставок конечно же сложнее, но классическая цепочка пивной игры подходит для обучения.

image
Рис. 1. Структура цепочки поставок

Каждый игровой сеанс включает в себя в общей сложности 12 периодов.

image
Рис. 2. Форма принятия решения для каждого игрока

Ячейки в формах имеют специальное форматирование, которое делает поля ввода видимым или невидимым для игроков в зависимости от текущего активного периода и последовательности принятия решений, чтобы игроки могли сосредоточиться на том, что является наиболее важным в данный момент. Преподаватель может контролировать рабочий процесс игры через панель управления, где отслеживаются основные параметры и показатели эффективности каждого игрока. Мнгновенно обновляемые графики на каждом листе помогают быстро понять ключевые показатели эффективности для игроков в любой момент. Инструкторы могут выбрать покупательский спрос: детерминированным (включая линейное и нелинейное), либо стохастическим (включая равномерное, нормальное, логнормальное, треугольное, гамма- и экспоненциальное).

Дальнейшая работа

Игра в данном виде еще далека от совершенства — требует дальнейшего улучшения многопользовательской игры по сети таким образом, чтобы исключить необходимость постоянного обновления и сохранения соответствующих листов после каждого действия игроков. Был бы почитать и ответить на комментарии по следующим вопросам:

а) реален ли эффект кнута на практике;
б) насколько может быть полезна пивная игра в обучении логистике и как её можно было бы усовершенствовать.

Ссылки

[1] Lee, H.L., Padmanabhan, V. and Whang, S., 1997.Information distortion in a supply chain: The bullwhip effect. Management science, 43(4), pp.546-558.
[2] Chen, F., Drezner, Z., Ryan, J.K. and Simchi-Levi, D., 2000.Quantifying the bullwhip effect in a simple supply chain: The impact of forecasting, lead times, and information.Management science, 46(3), pp.436-443.
[3] Sterman, J.D., 1989. Modeling managerial behavior: Misperceptions of feedback in a dynamic decision making experiment. Management science, 35(3), pp.321-339.
[4] Sucky, E., 2009. The bullwhip effect in supply chains — an overestimated problem? International Journal of Production Economics, 118(1), pp.311-322.
[5] Cachon, G.P., Randall, T. and Schmidt, G.M., 2007. In search of the bullwhip effect. Manufacturing & Service Operations Management, 9(4), pp.457-479.

ссылка на оригинал статьи https://habr.com/ru/post/499934/

Главная причина, почему все-таки Linux

Недавно на Хабре была опубликована статья Главная причина, почему не Linux, которая наделала много шума в обсуждениях. Данная заметка — это небольшой философский ответ на ту статью, который, как я надеюсь, расставит все точки над i, причем с довольно неожиданной для многих читателей стороны.

Автор исходной статьи так характеризует Linux-системы:

Linux это не система, а ворох разнородных поделок, смотанных изолентой

Почему же так происходит? Потому что

Человеку вообще плевать на приложения. Он пытается достичь своих целей… А в Линуксе потолок проектирования — не достижение целей, а решение задач.… сделаем поддержку пересылки файлов, это универсально и удовлетворит всех. А чтобы выслать селфач — пусть человек ищет софтину для захвата с веб-камеры, потом ретуширует фотку в каком-нибудь графическом редакторе, потом пересылает её с помощью семнадцатой опции в меню «Инструменты». У НАС ЖЕ ЮНИКСВЕЙ!

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

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

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

Итак, имеем следующую классификацию потребителей:

  1. Потребитель, получающий продукт непосредственно, без затрат труда.
  2. Потребитель, получающий продукт в обмен на другой продукт, в производстве которого он участвовал (индивидуально или в составе коллектива).
  3. Потребитель-производитель, получающий именно тот продукт, в производстве которого он участвовал (индивидуально или в составе коллектива).

Нас будет интересовать только коллективное производство, потому что такое благо, как полнофункциональная операционная система, сегодня невозможно создать в одиночку (во всяком случае Windows, macOS и Linux создаются большими коллективами).

К чему это все? Дело в том, что ошибочно ровнять потребителя Windows с потребителем Linux, потому что первый относится к типу 2, а второй — к типу 3. Более того, еще несуразнее относиться к потребителю Linux также, как к потребителю типа 1.

Настоящий, "целевой" потребитель Linux-системы сам же является участником ее производства. Это либо разработчик, которому нужен удобный, полностью подконтрольный и полностью им конфигурируемый инструмент, либо компания, которая использует систему в своем производственном процессе чего-то другого для этих производственных нужд. Этим потребителям становится выгоднее самим участвовать в производстве данного продукта (в том числе и в его настройке, как в одной из стадий производства, доведения продукта до состояния, готового к потреблению), чем покупать необходимые им доработки на стороне. Почему выгоднее? Да потому что себестоимость производства как правило меньше стоимости произведенного продукта, а часто еще готовый информационный продукт продается по цене, выше стоимости его копии.

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

Учитывая это, как можно ставить в вину Linux-сообществу то, что оно скорее создает набор универсальных инструментов, да такой, который еще нужно допиливать (читай — который требует от потребителя участие в производстве), чем удобный потребителю первого или второго типа полностью готовый продукт? Наоборот, попытка пойти на поводу у рыночной культуры чистого потребления и предлагать полностью готовый к потреблению продукт, без участия в его создании, доводке и отладке, подрывает саму производственную основу, на которой выстроен как Linux, так и другие свободные проекты. Отказаться от создания универсальных компонентов в пользу узко специализированных для частных целей — значит обречь свой свободный проект на застойное существование или забвение, потому что компонент, решающий общую во многих случаях задачу, соберет сообщество быстрее и больше, просто потому, что потребность в нем будет у большего числа потребителей-производителей.

И что же делать?

Нас пытаются убедить в том, что

Linux требуется очеловечить.… Это значит — переделать всё, начиная с загрузчика. …[Иначе] Linux останется забавой для людей, которые в детстве не наигрались в конструктор.

Но что мы получим в итоге такого "очеловечивания"? Мы получим систему, подобную Windows, ориентированную на потребителя, не участвующего в производстве, но при этом никак не вписанную в рыночную капиталистическую модель производства и обмена, а значит экономически не жизнеспособную. Оно нам надо?

Нет сомнений в том, что удобство использования — вещь очень важная, но следует иметь ввиду, что в случае с Linux на первом месте должно быть удобство не для пользователей первого или второго типа, а для пользователей третьего типа, участвующих прямо или косвенно в его производстве. Нужно создавать удобные инструменты и проводить соответствующую политику, чтобы пользователи-специалисты — потенциальные контрибьюторы — могли быстрее и проще включиться в сообщество по разработке и вносить свою лепту в общее дело. Нужны развитые средства конфигурирования и сборки, композиции инструментов, чтобы пользователи чувствовали ту реальную мощь, которую может дать им этот подход, и чтобы они не боялись использовать его для повышения своей продуктивности. А ведь за этих пользователей тоже идет борьба и их пытаются определить в категорию номер два, такими средствами, как macOS, например.

Ну а для тех, кто привык к халяве… Облегчение их жизни не должно быть самоцелью 🙂 Пусть поработают, пусть поучаствуют в отладке, пусть пишут сообщения на форумах и трекерах — эта информация сэкономит потом время другим, приучит к соучастию, а не к одностороннему пользованию. Да, Linux требует от потребителя работы. И это прекрасно! Давайте дальше развивать это направление, чтобы больше людей разных специальностей включалось в работу, а не только программисты, да сисадмины. Потому что без пассивного потребителя Linux проживет, а вот без соучастия в разработке — нет.

ссылка на оригинал статьи https://habr.com/ru/post/499936/