Як безпека потоку може бути забезпечена мовою програмування, подібною до того, як забезпечується безпека пам'яті Java та C #?


10

Java та C # забезпечують безпеку пам’яті, перевіряючи межі масиву та відмітки вказівника.

Які механізми можуть бути впроваджені в мову програмування для запобігання можливості гоночних умов та тупиків?


3
Можливо, вас зацікавить, що робить Rust: Безстрашна паралельність з іржею
Вінсент

2
Зробіть все непорушним або зробіть все асинхронізованим з безпечними каналами. Можливо, вас також зацікавлять " Го і Ерланг" .
Тераот

@Theraot "зробіть все асинхронізованим безпечним каналом" - бажайте, щоб ви могли детальніше розробити це.
mrpyo

2
@mrpyo ви не піддаватимете процесам чи потокам, кожен виклик - це обіцянка, все працює одночасно (з плануванням виконання програми, а також створення / об'єднання системних потоків за кадром за потребою), а логіка, яка захищає стан, знаходиться в механізмах яка передає інформацію навколо ... час виконання може автоматично серіалізуватись шляхом планування, і буде стандартна бібліотека із безпечним потоком рішення для більш нюансових поведінок, зокрема виробника / споживача та агрегації.
Theraot

2
До речі, існує ще один можливий підхід: транзакційна пам'ять .
Theraot

Відповіді:


14

Гонки відбуваються, коли у вас відбувається одночасне псевдонім об’єкта і, принаймні, один із псевдонімів мутує.

Отже, щоб запобігти перегонам, потрібно зробити одну або кілька цих умов неправдивими.

Різні підходи стосуються різних аспектів. Функціональне програмування підкреслює незмінність, яка знімає незмінність. Блокування / атома знімають одночасність. Affine типи видаляють псевдонім (Rust видаляє змінний псевдонім). Моделі акторів зазвичай знімають згладження

Ви можете обмежити об'єкти, які можна відчужувати, щоб легше було уникнути вищезазначених умов. Ось тут надходять канали та / або стилі передачі повідомлень. Ви не можете створювати довільну пам’ять, а лише кінець каналу чи черги, який призначений для вільної гонки. Зазвичай, уникаючи одночасності, тобто замків або атомів.

Мінусом цих різних механізмів є те, що вони обмежують програми, які можна писати. Чим більш тупо обмеження, тим менше програм. Таким чином, не створюються чи не змінюються зміни, і їх легко міркувати, але вони дуже обмежують.

Ось чому Руст викликає таку ворушіння. Це інженерна мова (на відміну від академічної), яка підтримує згладжування та змінення, але компілятор перевіряє, чи вони не відбуваються одночасно. Хоча це не ідеал, він дозволяє писати більш великий клас програм безпечно, ніж багато його попередників.


11

Java та C # забезпечують безпеку пам’яті, перевіряючи межі масиву та відмітки вказівника.

Важливо спочатку подумати про те, як це роблять C # і Java. Вони роблять це шляхом перетворення того, що є невизначеною поведінкою в C або C ++, у визначену поведінку: збої програми . Нульові відміни та виключення з індексу масиву ніколи не повинні потрапляти у правильну програму C # або Java; вони не повинні відбуватися в першу чергу, оскільки програма не повинна мати цю помилку.

Але це я думаю, не те, що ви маєте на увазі під своїм питанням! Ми могли б досить легко написати "безпечний тупик" час виконання, який періодично перевіряє, чи немає російських потоків, які взаємно чекають один одного і завершують програму, якщо це станеться, але я не думаю, що це вас би задовольнило.

Які механізми можуть бути впроваджені в мову програмування для запобігання можливості гоночних умов та тупиків?

Наступна проблема, з якою ми стикаємося з вашим питанням, полягає в тому, що "умови перегонів", на відміну від тупикових ситуацій, важко виявити. Пам’ятайте, те, що ми хочемо в безпеці ниток, - це не усунення перегонів . Ми намагаємось зробити програму правильною незалежно від того, хто виграє гонку ! Проблема з умовами перегонів полягає не в тому, що два потоки працюють у невизначеному порядку, і ми не знаємо, хто збирається закінчити першим. Проблема з умовами гонки полягає в тому, що розробники забувають, що деякі замовлення обробки ниток можливі, і не враховують цю можливість.

Тож ваше запитання в основному зводиться до "чи існує спосіб, щоб мова програмування могла забезпечити правильність моєї програми?" і відповідь на це питання на практиці - ні.

Поки що я лише критикував ваше запитання. Дозвольте спробувати переключити передачі тут і вирішити дух вашого питання. Чи є вибір, який могли б зробити мовні дизайнери, що полегшить жахливу ситуацію, з якою ми стикаємося з багатопотоковою програмою?

Ситуація справді жахлива! Виправити багатопотоковий код правильним, особливо у слабких архітектурах моделі пам'яті, дуже і дуже складно. Подумано, чому це важко:

  • Кілька потоків управління в одному процесі важко обґрунтувати. Один потік досить важкий!
  • Абстракції стають надзвичайно протікаючими у багатопотоковому світі. У світі з єдиними потоками ми гарантуємо, що програми поводяться так, ніби вони запускаються в порядку, навіть якщо вони насправді не працюють в порядку. У багатопотоковому світі це вже не так; оптимізації, які були б невидимими для одного потоку, стають видимими, і тепер розробнику потрібно зрозуміти ці можливі оптимізації.
  • Але стає гірше. Специфікація C # говорить про те, що для реалізації НЕ потрібно мати послідовний порядок читання і запису, який може бути узгоджений усіма потоками . Думка про те, що взагалі існують "перегони" і що є явний переможець, насправді не відповідає дійсності! Розглянемо ситуацію, коли в багатьох потоках є два записи та два читання деяких змінних. У розумному світі ми можемо подумати: "ну, ми не можемо знати, хто збирається виграти гонки, але принаймні буде гонка, і хтось переможе". Ми не в тому розумному світі. C # дозволяє безлічі потоків не погоджуватися щодо порядку, в якому відбувається читання і запис; не обов’язково існує послідовний світ, якого спостерігають усі.

Тож існує очевидний спосіб, що мовні дизайнери можуть покращити справи. Відмовитися від продуктивності перемагає сучасний процесор . Зробіть, щоб усі програми, навіть багатопотокові, мали надзвичайно сильну модель пам'яті. Це зробить багатопотокові програми у багато, у багато разів повільнішими, що працює прямо проти причини того, що в першу чергу є багатопотокові програми: для підвищення продуктивності.

Навіть залишаючи осторонь модель пам’яті, є й інші причини, через які багатопотоковість важка:

  • Запобігання тупикових ситуацій вимагає загальнопрограмного аналізу; вам потрібно знати глобальний порядок, в якому можна зняти блокування, і застосувати це замовлення по всій програмі, навіть якщо програма складається з компонентів, написаних у різні часи різними організаціями.
  • Основний інструмент, який ми надаємо вам, щоб приручити багатопоточність - це замок, але замки не можуть бути складені .

Цей останній пункт має додаткове пояснення. Під поняттям "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, не можуть бути використані в одній програмі без створення некоректної програми. Замки роблять так, що ви не можете сказати "кожен окремий метод правильний, тому вся справа правильна".

Отже, що ми можемо зробити як дизайнери мови?

По-перше, не їдь туди. Кілька потоків управління в одній програмі - це погана ідея, а обмін пам’яттю між потоками - погана ідея, тому не перекладайте її в мову чи час виконання в першу чергу.

Це, мабуть, нестандарт.

Давайте звернемо свою увагу тоді на більш фундаментальне питання: чому ми маємо в першу чергу кілька ниток? Є дві основні причини, і вони часто поєднуються в одне і те ж, хоча вони сильно відрізняються. Вони плутаються через те, що обидва стосуються затримки.

  • Ми створюємо теми, помилково, для управління затримкою IO. Потрібно написати великий файл, отримати доступ до віддаленої бази даних, будь-що, створити робочий потік, а не блокувати ваш потік інтерфейсу.

Погана ідея. Замість цього використовуйте однопоточну асинхронію через супроводи. C # робить це прекрасно. Ява, не так добре. Але це головний спосіб, що нинішній урожай мовних дизайнерів допомагає вирішити проблему з ниткою. awaitОператор в C # ( під впливом F # асинхронні робочі процеси і попередній рівень техніки) в даний час включені у всі більш і більше мовами.

  • Ми правильно створюємо теми, щоб наситити простої процесори обчислювально важкою роботою. В основному, ми використовуємо нитки як легкі процеси.

Мовні дизайнери можуть допомогти, створивши мовні функції, які добре працюють з паралелізмом. Подумайте, як LINQ так природно поширюється, наприклад, на PLINQ. Якщо ви є розумною людиною і обмежуєте свої TPL-операції операціями, пов'язаними з процесором, які є паралельними і не діляться пам'яттю, ви можете отримати великі виграші тут.

Що ще ми можемо зробити?

  • Зробіть так, щоб компілятор виявив найбільш помилкові помилки і перетворив їх на попередження чи помилки.

C # не дозволяє чекати у замці, тому що це рецепт тупиків. C # не дозволяє вам зафіксувати тип значення, оскільки це завжди неправильно робити; ви фіксуєте коробку, а не значення. C # попереджає вас, якщо ви псевдонім нестабільний, оскільки псевдонім не нав'язує придбати / випустити семантику. Є набагато більше способів, за допомогою яких компілятор міг би виявити загальні проблеми та запобігти їх.

  • Дизайн "ями якості", де найприродніший спосіб зробити це також найбільш правильний спосіб.

C # та Java допустили величезну помилку дизайну, дозволяючи використовувати будь-який довідковий об'єкт як монітор. Це заохочує всілякі погані практики, які ускладнюють відстеження тупиків і важче запобігти їх статичному застосуванню. І він марнує байти у кожному заголовку об'єкта. Монітори повинні бути похідними від класу моніторів.

  • Величезна кількість часу та зусиль Microsoft Research пішла на спробу додати транзакційну пам'ять програмного забезпечення до мови, що нагадує C #, і вони ніколи не отримували її достатньо ефективною для включення її в основну мову.

STM - прекрасна ідея, і я розігрувався з реалізацією іграшок в Haskell; це дозволяє набагато елегантніше складати правильні рішення з правильних деталей, ніж це рішення, засновані на замках. Однак я не знаю достатньо деталей, щоб сказати, чому це не може бути змушене працювати в масштабі; запитайте Джо Даффі наступного разу, коли ви його побачите.

  • Інша відповідь вже згадувала незмінність. Якщо у вас незмінність поєднується з ефективними процедурами, ви можете будувати такі функції, як модель актора, безпосередньо на своїй мові; подумайте, наприклад, Ерланг.

Було багато досліджень мов на основі обчислення, і я не дуже добре розумію цей простір; спробуйте самостійно прочитати кілька статей і побачити, чи отримаєте ви якусь інформацію.

  • Полегшіть третім сторонам писати хороші аналізатори

Після того, як я працював у Microsoft на Росліні, я працював у Coverity, і одна з речей, яку я зробила, - це отримати передню частину аналізатора за допомогою Roslyn. Маючи точний лексичний, синтаксичний та семантичний аналіз, наданий корпорацією Майкрософт, ми могли б зосередитись на наполегливій роботі написання детекторів, які виявили загальні проблеми багатопотокового прочитання.

  • Підвищити рівень абстракції

Принципова причина, чому ми маємо гонки та тупики, і все це - це те, що ми пишемо програми, які говорять, що робити , і виявляється, що ми всі лайно пишемо імперативні програми; комп'ютер робить те, що ви йому скажете, а ми - ми робимо неправильні речі. Багато сучасних мов програмування дедалі більше стосуються декларативного програмування: скажіть, які результати ви хочете, і дозвольте компілятору розібратися в ефективному, безпечному і правильному способі досягнення цього результату. Знову ж, подумайте про LINQ; ми хочемо, щоб ви сказали from c in customers select c.FirstName, що виражає наміри . Дозвольте компілятору розібратися, як написати код.

  • Використовуйте комп’ютери для вирішення комп'ютерних проблем.

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

Вибачте, що там трохи розійшлося; це величезна і складна тема, і чіткого консенсусу в 20-річному середовищі не склалося, я спостерігав за прогресом у цьому проблемному просторі.


"Отже, ваше запитання в основному зводиться до" чи існує спосіб, щоб мова програмування могла забезпечити правильність моєї програми? ", А відповідь на це питання на практиці - ні". - насправді це цілком можливо - це називається офіційна перевірка, і, хоча це незручно, я впевнений, що це звичайно робиться на критичному програмному забезпеченні, тому я б не вважав це недоцільним. Але ви, як мовний дизайнер, напевно, знаєте це ...
mrpyo

6
@mrpyo: Я добре знаю. Проблем багато. По-перше: я одного разу відвідав офіційну конференцію з верифікації, де дослідницька група MSFT представила новий захоплюючий результат: вони змогли розширити свою техніку для перевірки багатопотокових програм довжиною до двадцяти рядків і перевірити тестування менш ніж за тиждень. Це була цікава презентація, але не приносила мені користі; У мене була програма на 20 мільйонів ліній для аналізу.
Ерік Ліпперт

@mrpyo: По-друге, як я вже згадував, велика проблема із блокуваннями полягає в тому, що програма, що складається з безпечних методів потоків, не обов'язково є безпечною програмою для потоків. Формальна перевірка окремих методів не обов'язково допомагає, і аналіз непрофільних програм важкий для всіх програм.
Ерік Ліпперт

6
@mrpyo: По-третє, велика проблема формального аналізу полягає в тому, що що, в принципі, ми робимо? Ми представляємо специфікацію передумов та постумов, а потім перевіряємо, чи відповідає програма цій специфікації. Чудовий; в теорії, що цілком можливо. На якій мові написана специфікація? Якщо є однозначний, що перевірявся мову специфікації , то давайте просто написати все наші програми на цій мові , і компілювати що . Чому ми цього не зробимо? Тому що виявляється, що правильно писати правильні програми теж мовою специфіки важко!
Ерік Ліпперт

2
Можливий аналіз програми на коректність за допомогою попередніх умов / пост-умов (наприклад, використання контрактів на кодування). Однак такий аналіз здійсненний лише за умови, що умови є композиційними, а замки - ні. Я також зазначу, що написання програми таким чином, що дозволяє аналізувати, вимагає ретельної дисципліни. Наприклад, програми, які не дотримуються принципу заміни Ліскова, як правило, протистоять аналізу.
Брайан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.