Java та C # забезпечують безпеку пам’яті, перевіряючи межі масиву та відмітки вказівника.
Які механізми можуть бути впроваджені в мову програмування для запобігання можливості гоночних умов та тупиків?
Java та C # забезпечують безпеку пам’яті, перевіряючи межі масиву та відмітки вказівника.
Які механізми можуть бути впроваджені в мову програмування для запобігання можливості гоночних умов та тупиків?
Відповіді:
Гонки відбуваються, коли у вас відбувається одночасне псевдонім об’єкта і, принаймні, один із псевдонімів мутує.
Отже, щоб запобігти перегонам, потрібно зробити одну або кілька цих умов неправдивими.
Різні підходи стосуються різних аспектів. Функціональне програмування підкреслює незмінність, яка знімає незмінність. Блокування / атома знімають одночасність. Affine типи видаляють псевдонім (Rust видаляє змінний псевдонім). Моделі акторів зазвичай знімають згладження
Ви можете обмежити об'єкти, які можна відчужувати, щоб легше було уникнути вищезазначених умов. Ось тут надходять канали та / або стилі передачі повідомлень. Ви не можете створювати довільну пам’ять, а лише кінець каналу чи черги, який призначений для вільної гонки. Зазвичай, уникаючи одночасності, тобто замків або атомів.
Мінусом цих різних механізмів є те, що вони обмежують програми, які можна писати. Чим більш тупо обмеження, тим менше програм. Таким чином, не створюються чи не змінюються зміни, і їх легко міркувати, але вони дуже обмежують.
Ось чому Руст викликає таку ворушіння. Це інженерна мова (на відміну від академічної), яка підтримує згладжування та змінення, але компілятор перевіряє, чи вони не відбуваються одночасно. Хоча це не ідеал, він дозволяє писати більш великий клас програм безпечно, ніж багато його попередників.
Java та C # забезпечують безпеку пам’яті, перевіряючи межі масиву та відмітки вказівника.
Важливо спочатку подумати про те, як це роблять C # і Java. Вони роблять це шляхом перетворення того, що є невизначеною поведінкою в C або C ++, у визначену поведінку: збої програми . Нульові відміни та виключення з індексу масиву ніколи не повинні потрапляти у правильну програму C # або Java; вони не повинні відбуватися в першу чергу, оскільки програма не повинна мати цю помилку.
Але це я думаю, не те, що ви маєте на увазі під своїм питанням! Ми могли б досить легко написати "безпечний тупик" час виконання, який періодично перевіряє, чи немає російських потоків, які взаємно чекають один одного і завершують програму, якщо це станеться, але я не думаю, що це вас би задовольнило.
Які механізми можуть бути впроваджені в мову програмування для запобігання можливості гоночних умов та тупиків?
Наступна проблема, з якою ми стикаємося з вашим питанням, полягає в тому, що "умови перегонів", на відміну від тупикових ситуацій, важко виявити. Пам’ятайте, те, що ми хочемо в безпеці ниток, - це не усунення перегонів . Ми намагаємось зробити програму правильною незалежно від того, хто виграє гонку ! Проблема з умовами перегонів полягає не в тому, що два потоки працюють у невизначеному порядку, і ми не знаємо, хто збирається закінчити першим. Проблема з умовами гонки полягає в тому, що розробники забувають, що деякі замовлення обробки ниток можливі, і не враховують цю можливість.
Тож ваше запитання в основному зводиться до "чи існує спосіб, щоб мова програмування могла забезпечити правильність моєї програми?" і відповідь на це питання на практиці - ні.
Поки що я лише критикував ваше запитання. Дозвольте спробувати переключити передачі тут і вирішити дух вашого питання. Чи є вибір, який могли б зробити мовні дизайнери, що полегшить жахливу ситуацію, з якою ми стикаємося з багатопотоковою програмою?
Ситуація справді жахлива! Виправити багатопотоковий код правильним, особливо у слабких архітектурах моделі пам'яті, дуже і дуже складно. Подумано, чому це важко:
Тож існує очевидний спосіб, що мовні дизайнери можуть покращити справи. Відмовитися від продуктивності перемагає сучасний процесор . Зробіть, щоб усі програми, навіть багатопотокові, мали надзвичайно сильну модель пам'яті. Це зробить багатопотокові програми у багато, у багато разів повільнішими, що працює прямо проти причини того, що в першу чергу є багатопотокові програми: для підвищення продуктивності.
Навіть залишаючи осторонь модель пам’яті, є й інші причини, через які багатопотоковість важка:
Цей останній пункт має додаткове пояснення. Під поняттям "composable" я маю на увазі наступне:
Припустимо, ми хочемо обчислити int, заданий дублем. Пишемо правильне виконання обчислення:
int F(double x) { correct implementation here }
Припустимо, ми хочемо обчислити рядок із даними int:
string G(int y) { correct implementation here }
Тепер, якщо ми хочемо обчислити рядок із заданим подвійним:
double d = whatever;
string r = G(F(d));
G і F можуть бути складені правильним рішенням більш складної проблеми.
Але замки не мають цієї властивості через тупики. Правильний метод M1, який приймає блокування в порядку L1, L2, і правильний метод M2, який приймає блокування в порядку L2, L1, не можуть бути використані в одній програмі без створення некоректної програми. Замки роблять так, що ви не можете сказати "кожен окремий метод правильний, тому вся справа правильна".
Отже, що ми можемо зробити як дизайнери мови?
По-перше, не їдь туди. Кілька потоків управління в одній програмі - це погана ідея, а обмін пам’яттю між потоками - погана ідея, тому не перекладайте її в мову чи час виконання в першу чергу.
Це, мабуть, нестандарт.
Давайте звернемо свою увагу тоді на більш фундаментальне питання: чому ми маємо в першу чергу кілька ниток? Є дві основні причини, і вони часто поєднуються в одне і те ж, хоча вони сильно відрізняються. Вони плутаються через те, що обидва стосуються затримки.
Погана ідея. Замість цього використовуйте однопоточну асинхронію через супроводи. C # робить це прекрасно. Ява, не так добре. Але це головний спосіб, що нинішній урожай мовних дизайнерів допомагає вирішити проблему з ниткою. await
Оператор в C # ( під впливом F # асинхронні робочі процеси і попередній рівень техніки) в даний час включені у всі більш і більше мовами.
Мовні дизайнери можуть допомогти, створивши мовні функції, які добре працюють з паралелізмом. Подумайте, як LINQ так природно поширюється, наприклад, на PLINQ. Якщо ви є розумною людиною і обмежуєте свої TPL-операції операціями, пов'язаними з процесором, які є паралельними і не діляться пам'яттю, ви можете отримати великі виграші тут.
Що ще ми можемо зробити?
C # не дозволяє чекати у замці, тому що це рецепт тупиків. C # не дозволяє вам зафіксувати тип значення, оскільки це завжди неправильно робити; ви фіксуєте коробку, а не значення. C # попереджає вас, якщо ви псевдонім нестабільний, оскільки псевдонім не нав'язує придбати / випустити семантику. Є набагато більше способів, за допомогою яких компілятор міг би виявити загальні проблеми та запобігти їх.
C # та Java допустили величезну помилку дизайну, дозволяючи використовувати будь-який довідковий об'єкт як монітор. Це заохочує всілякі погані практики, які ускладнюють відстеження тупиків і важче запобігти їх статичному застосуванню. І він марнує байти у кожному заголовку об'єкта. Монітори повинні бути похідними від класу моніторів.
STM - прекрасна ідея, і я розігрувався з реалізацією іграшок в Haskell; це дозволяє набагато елегантніше складати правильні рішення з правильних деталей, ніж це рішення, засновані на замках. Однак я не знаю достатньо деталей, щоб сказати, чому це не може бути змушене працювати в масштабі; запитайте Джо Даффі наступного разу, коли ви його побачите.
Було багато досліджень мов на основі обчислення, і я не дуже добре розумію цей простір; спробуйте самостійно прочитати кілька статей і побачити, чи отримаєте ви якусь інформацію.
Після того, як я працював у Microsoft на Росліні, я працював у Coverity, і одна з речей, яку я зробила, - це отримати передню частину аналізатора за допомогою Roslyn. Маючи точний лексичний, синтаксичний та семантичний аналіз, наданий корпорацією Майкрософт, ми могли б зосередитись на наполегливій роботі написання детекторів, які виявили загальні проблеми багатопотокового прочитання.
Принципова причина, чому ми маємо гонки та тупики, і все це - це те, що ми пишемо програми, які говорять, що робити , і виявляється, що ми всі лайно пишемо імперативні програми; комп'ютер робить те, що ви йому скажете, а ми - ми робимо неправильні речі. Багато сучасних мов програмування дедалі більше стосуються декларативного програмування: скажіть, які результати ви хочете, і дозвольте компілятору розібратися в ефективному, безпечному і правильному способі досягнення цього результату. Знову ж, подумайте про LINQ; ми хочемо, щоб ви сказали from c in customers select c.FirstName
, що виражає наміри . Дозвольте компілятору розібратися, як написати код.
Алгоритми машинного навчання набагато кращі в деяких завданнях, ніж алгоритми, кодовані вручну, хоча, звичайно, є багато компромісів, включаючи правильність, час, необхідний для тренувань, упередження, спричинені поганою підготовкою тощо. Але ймовірно, що дуже багато завдань, які ми зараз кодуємо "від руки", незабаром піддаються машинним рішенням. Якщо люди не пишуть код, вони не пишуть помилок.
Вибачте, що там трохи розійшлося; це величезна і складна тема, і чіткого консенсусу в 20-річному середовищі не склалося, я спостерігав за прогресом у цьому проблемному просторі.