Ілюстрацыя знакамітай «Праблемы філосафаў-сталоў»

Паралельнасць супраць цыклу падзей супраць цыкла падзей + паралельнасць

Перш за ўсё давайце патлумачым тэрміналогію.
Паралельнасць - азначае, што ў вас некалькі чэргаў задач на некалькіх працэсарных ядрах / патоках. Але ён цалкам адрозніваецца ад паралельнага выканання, паралельнае выкананне не ўтрымлівае некалькіх задач Чарга для паралельнага выпадку, для поўнага паралельнага выканання нам спатрэбіцца 1 ядро ​​/ паток працэсара на заданне, якое мы ў большасці выпадкаў не можам вызначыць. Вось чаму для сучаснай распрацоўкі праграмнага забеспячэння паралельнае праграмаванне часам азначае "Паралельнасць", я ведаю, што дзіўна, але яно ёсць у нас на дадзены момант (гэта залежыць ад мадэлі працэсара / тэмы OS).
Петля падзей - азначае бясконцы цыкл з адной ніткай, які стварае па адной задачы, і гэта не толькі стварэнне адной чаргі задач, але таксама і расстаноўка прыярытэтаў, таму што з цыкла падзей у вас ёсць толькі адзін рэсурс для выканання (1 нітка), таму для выканання для некаторых задач вам патрэбныя прыярытэты. У некаторых словах такі падыход да праграмавання называецца бяспечным праграмаваннем тэмы, таму што адначасова можа выконвацца толькі адна задача / функцыя / аперацыя, і калі вы нешта мяняеце, ён будзе ўжо зменены падчас наступнага выканання задачы.

Паралельнае праграмаванне

У сучасных кампутарах / серверах у нас як мінімум 2 ядра працэсара і мін. 4 тэмы працэсара. Але на серверах зараз ср. сервер мае па меншай меры 16 патокаў працэсара. Такім чынам, калі вы пішаце праграмнае забеспячэнне, якое патрабуе пэўнай прадукцыйнасці, вы абавязкова падумайце зрабіць яго такім чынам, што ён будзе выкарыстоўваць усе ядра працэсара, даступныя на серверы.

Гэтая выява адлюстроўвае асноўную мадэль паралельнасці, але гэта не так проста, што адлюстроўваецца :)

Праграмаванне паралельных валют становіцца вельмі складаным з некаторымі агульнымі рэсурсамі, напрыклад, давайце паглядзім на гэты просты просты адначасовы код.

// Няправільнае супадзенне з мовай Go
асноўны пакет
імпарт (
   "fmt"
   "час"
)
var SharedMap = make (карта [string] string)
func changeMap (радок значэння) {
    SharedMap ["тэст"] = значэнне
}
func main () {
    ісці changeMap ("value1")
    ісці changeMap ("value2")
    time.Sleep (час.Мілісекунда * 500)
    fmt.Println (SharedMap ["тэст"])
}
// Гэта будзе надрукаваць "value1" ці "value2", якіх мы дакладна не ведаем!

У гэтым выпадку Go вызваліць 2 адначасовыя заданні, верагодна, у розных ядрах працэсара, і мы не можам прадказаць, які з іх будзе выкананы першым, таму мы не ведаем, што будзе адлюстроўвацца ў канцы.
Чаму? - Усё проста! Мы плануем 2 розныя задачы для розных ядраў працэсара, але яны выкарыстоўваюць адзіную агульную зменную / памяць, так што яны змяняюць гэтую памяць, а ў некаторых выпадках гэта будзе збой / выключэннем праграмы.

Так што для прагназавання выканання праграмнага забеспячэння паралельнасці нам трэба выкарыстоўваць некаторыя функцыі блакавання, такія як Mutex. З яго дапамогай мы можам заблакаваць гэты агульны рэсурс памяці і зрабіць яго даступным толькі для адной задачы.
Гэты стыль праграмавання называецца Блакіроўка, таму што мы фактычна блакуем усе задачы, пакуль бягучая задача не будзе выканана з агульнай памяці.

Большасць распрацоўшчыкаў не любіць адначасовае праграмаванне, таму што паралельнасць не заўсёды азначае прадукцыйнасць. Гэта залежыць ад канкрэтных выпадкаў.

Адзіночная радок падзей

Такі падыход да распрацоўкі праграмнага забеспячэння - нашмат прасцей, чым праграмнае забеспячэнне адначасовага выкарыстання. Таму што прынцып вельмі просты. За адзін раз у вас ёсць толькі адно выкананне задання. І ў гэтым выпадку ў вас няма праблем з агульнымі пераменнымі / памяццю, таму што праграма адначасова прадказальная.

Агульны паток наступны
1. Даданне эмітэра падзей да чаргі падзей, якое будзе выканана ў наступным цыкле цыкла
2. Петля падзей - атрыманне задачы з чаргі падзей і апрацоўка яе на аснове апрацоўнікаў

Давайце пішам той жа прыклад з node.js

хай SharedMap = {};
const changeMap = (value) => {
    return () => {
        SharedMap ["тэст"] = значэнне
    }
}
// 0 Час тайм-аўту азначае, што мы робім новую задачу ў чарзе для наступнага цыклу
setTimeout (changeMap ("value1"), 0);
setTimeout (changeMap ("value2"), 0);
setTimeout (() => {
   console.log (SharedMap ["тэст"])
}, 500);
// У гэтым выпадку Node.js будзе надрукаваць "value2", таму што яна адзінкавая
// Паток і ён мае "толькі адну чаргу"

Як вы можаце сабе ўявіць, у гэтым выпадку спосаб кода больш прадказальны, чым пры адначасовым прыкладзе Go, і гэта таму, што Node.js працуе ў адзіным рэжыме з выкарыстаннем цыкла JavaScript.

У некаторых выпадках цыкл падзей дае вялікую прадукцыйнасць, чым пры паралельнасці, з-за неблокирующего паводзін. Вельмі добры прыклад - сеткавыя прыкладанні, таму што яны выкарыстоўваюць адзіны рэсурс сеткавага злучэння і апрацоўваюць дадзеныя толькі тады, калі яны даступныя з выкарыстаннем Thread Safe Event Loops.

Паралельнасць + цыкл падзей - пул з ніткамі з бяспекай ніткі

Зрабіць прыкладанне толькі адначасова можа быць вельмі складана, таму што памылкі з пашкоджаннем памяці будуць усюды, інакш ваша дадатак пачне блакаваць дзеянні для кожнай задачы. Асабліва, калі вы хочаце атрымаць максімальную прадукцыйнасць, вам трэба аб'яднаць абодва!

Давайце паглядзім на мадэль пула + цыкл падзей ад структуры вэб-сервера Nginx

Асноўная сеткавая апрацоўка і апрацоўка канфігурацыі вырабляецца цыклам Worker Event у адзіны паток для бяспекі, але калі Nginx неабходна прачытаць нейкі файл альбо неабходна апрацаваць загалоўкі / цела HTTP-запыту, якія блакуюць аперацыі, ён адпраўляе гэтую задачу ў свой паток тэмы. для адначасовай апрацоўкі. І калі задача выканана, вынік адпраўляецца назад у цыкл падзей для бяспечнай апрацоўкі выкананага выніку.

Такім чынам, выкарыстоўваючы гэтую структуру, вы атрымліваеце як Thread Security, так і Concurrency, што дазваляе выкарыстоўваць усе ядра працэсара для прадукцыйнасці і захоўваць прынцып, які не блакуе адзін цыкл падзелу.

Выснова

Шмат праграмнага забеспячэння напісана з чыстай паралельнасцю альбо з чыстым радком падзей з адным радком, але спалучэнне як унутры аднаго прыкладання палягчае напісанне прыкладанняў-выканаўцаў, так і выкарыстанне ўсіх даступных рэсурсаў працэсара.