Кіріспе
Бағдарламалық жасақтамадағы бір мезгілділікті басқару механизмі. Компьютерлік ғылымда бағдарламалық транзакциялық жад (STM) – бір мезгілді есептеуде ортақ жадқа қол жеткізуді басқару үшін дерекқорының транзакцияларына ұқсас бір мезгілділікті басқару механизмі. Бұл құлыптауға негізделген синхрондаудың баламасы. STM – аппараттық компонент ретінде емес, бағдарламалық жасақтамада іске асырылатын стратегия. Бұл жағдайда транзакция, кодтың бір бөлігі ортақ жадқа бірнеше оқу және жазу операцияларын орындағанда жүзеге асады. Бұл оқулар мен жазулар логикалық тұрғыдан бір сәтте болады; аралық күйлер басқа (сәтті аяқталған) транзакцияларға көрінбейді. Транзакцияларға аппараттық қолдау көрсету идеясы Том Найттың 1986 жылғы мақаласында туындады. Бұл идеяны Морис Герлихи және Дж. Элиот Б. Мосс танымал етті. 1995 жылы Нир Шавит пен Дэн Туиту бұл идеяны тек бағдарламалық транзакциялық жадқа (STM) қатысты кеңейтті. 2005 жылдан бері STM қарқынды зерттеулердің назарында болып келеді және практикалық іске асыруға қолдау артып келеді.
In computer science, software transactional memory (STM) is a concurrency control mechanism analogous to database transactions for controlling access to shared memory in concurrent computing. It is an alternative to lock based synchronization. STM is a strategy implemented in software, rather than as a hardware component. A transaction in this context occurs when a piece of code executes a series of reads and writes to shared memory. These reads and writes logically occur at a single instant in time; intermediate states are not visible to other (successful) transactions. The idea of providing hardware support for transactions originated in a 1986 paper by Tom Knight. The idea was popularized by Maurice Herlihy and J. Eliot B. Moss. In 1995, Nir Shavit and Dan Touitou extended this idea to software only transactional memory (STM). Since 2005, STM has been the focus of intense research and support for practical implementations is growing.
Өнер көрсету
Көптеген заманауи көп жіпті қолданбаларда қолданылатын құлыптау техникаларынан өзгеше, STM көбінесе өте оптимистік болады: бір жіп ортақ жадқа өзгерістер енгізуді аяқтайды, басқа жіптер не істеп тұрғанына қарамастан, барлық оқу және жазу амалдарын журналға тіркейді. Жазушының басқа ағымдағы операцияларға кері әсер етпеуін қамтамасыз ету жауапкершілігінен гөрі, бұл жауапкершілік оқырманға жүктеледі. Оқырман толық транзакцияны аяқтағаннан кейін, басқа жіптер оның бұрын қол жеткізген жадына бір мезгілде өзгерістер енгізбегенін тексереді. Бұл соңғы операция, онда транзакцияның өзгерістері тексеріледі және тексеру сәтті аяқталса, олар тұрақты болады, міндеттеме деп аталады. Транзакция кез келген уақытта тоқтатылуы мүмкін, бұл оның барлық бұрынғы өзгерістерін кері қайтаруға немесе жоюға әкеледі. Егер транзакция қарама-қайшылықты өзгерістерге байланысты міндеттемеге қол жеткізе алмаса, әдетте тоқтатылып, сәтті аяқталғанша басынан бастап қайта орындалады. Бұл оптимистік тәсілдің артықшылығы – жоғары параллелизм: ешбір жіп ресурстың қолжетімді болуын күтуге қажеті жоқ, және әртүрлі жіптер бір уақытта дерек құрылымының бөлек бөліктерін қауіпсіз түрде өзгерте алады, бұл бөліктер әдетте бір құлыппен қорғалады. Дегенмен, практикада STM жүйелері, процессорлардың аз санында (қолданбаға байланысты 1-ден 4-ке дейін) ұсақ түйірлі құлыптауға негізделген жүйелерге қарағанда өнімділік бойынша кемістіктерге тап болуы мүмкін. Бұл негізінен журналды жүргізуге байланысты қосымша шығындар мен транзакцияларды міндеттемеге жеткізуге жұмсалатын уақытқа байланысты. Тіпті осы жағдайда да өнімділік әдетте екі еседен аспайды. STM-ді жақтаушылар бұл кемшілікті STM-нің ұғымдық артықшылықтарымен толықтай ақталады деп санайды. Теориялық тұрғыдан алғанда, n бір мезгілдегі транзакциялардың ең нашар жағдайдағы кеңістіктік және уақыттық күрделілігі O(n) құрайды. Нақты қажеттіліктер жүзеге асыру ерекшеліктеріне байланысты (шығындарды болдырмау үшін транзакциялардың ертерек сәтсіздікке ұшырауына қол жеткізуге болады), бірақ бағдарламалық транзакциялық жадға қарағанда құлыптауға негізделген алгоритмдердің уақыт жағынан күрделілігі жақсырақ болатын сирек жағдайлар да болады.
Тұжырымдамалық артықшылықтар мен кемшіліктер
Орындау артықшылықтарынан басқа, STM көп тірісті бағдарламалардың түсінілуін жеңілдетеді және объектілер мен модульдер сияқты жоғары деңгейдегі абстракциялармен үйлесімді жұмыс істеу арқылы бағдарламаларды күтіп-ұстауды жеңілдетеді. Құлыптауға негізделген бағдарламалауда тәжірибеде жиі туындайтын бірнеше мәселе бар: Құлыптау кодтың қашық және көрінбейтін бөлімдеріндегі бір-бірімен байланысты операциялар мен жартылай орындалған операциялар туралы ойлауды қажет етеді, бұл өте қиын және қателерге бейім міндет. Құлыптау бағдарламалаушылардан тұйықталудың, тірі қамаудың және прогреске кедерес келтіретін басқа да мәселелердің алдын алу үшін құлыптау саясатын қабылдауды талап етеді. Мұндай саясат көбінесе ресми емес түрде сақталады және қателіктерге ұшырауы мүмкін, ал осы мәселелер туындағанда оларды қайта жасау және түзету өте қиын. Құлыптау басымдық инверсиясына әкелуі мүмкін, яғни жоғары басымдылыққа ие тіріс, қажетті ресурстың эксклюзивті құлпын ұстап тұрған төмен басымдылыққа ие тіріс күтуге мәжбүр болады. Керісінше, жад транзакциясының түсінігі әлдеқайда қарапайым, өйткені әрбір транзакция жеке тіріс есептеу ретінде қарастырылуы мүмкін. Тұйықталу және тірі қамау толығымен алдын алынады немесе сыртқы транзакция менеджерімен басқарылады; бағдарламашы мұндай мәселелер туралы алаңдаудың қажеті жоқ. Басымдық инверсиясы мәселесі әлі де туындауы мүмкін, бірақ жоғары басымдылыққа ие транзакциялар әлі міндеттеме алған жоқ, төмен басымдылыққа ие транзакцияларды тоқтатуы мүмкін. Дегенмен, транзакцияларды қайтадан орындау және тоқтату қажеттілігі олардың мінез-құлқына шектеулер қояды. Транзакция ішінде орындалатын кез келген операция идемпотентті болуы керек, өйткені транзакция қайтадан орындалуы мүмкін. Сонымен қатар, егер операция тоқтатылған жағдайда кері қайтару қажет болатын жанама әсерлерге ие болса, сәйкес кері операция қосылуы тиіс. Бұл көптеген кіріс/шығыс (I/O) операцияларын транзакциялар ішінде орындауды қиындатады немесе мүмкін емес етеді. Мұндай шектеулер әдетте қайталанбайтын операцияларды кезекке қоятын және транзакция сәтті аяқталғаннан кейін оларды орындайтын буферлер құру арқылы еңсеріледі. Haskell-де бұл шектеу деректер типі жүйесімен компиляция кезінде күшіне енгізіледі.
Locking requires thinking about overlapping operations and partial operations in distantly separated and seemingly unrelated sections of code, a task which is very difficult and error prone. Locking requires programmers to adopt a locking policy to prevent deadlock, livelock, and other failures to make progress. Such policies are often informally enforced and fallible, and when these issues arise they are insidiously difficult to reproduce and debug. Locking can lead to priority inversion, a phenomenon where a high priority thread is forced to wait for a low priority thread holding exclusive access to a resource that it needs. In contrast, the concept of a memory transaction is much simpler, because each transaction can be viewed in isolation as a single threaded computation. Deadlock and livelock are either prevented entirely or handled by an external transaction manager; the programmer need hardly worry about it. Priority inversion can still be an issue, but high priority transactions can abort conflicting lower priority transactions that have not already committed. However, the need to retry and abort transactions limits their behavior. Any operation performed within a transaction must be idempotent since a transaction might be retried. Additionally, if an operation has side effects that must be undone if the transaction is aborted, then a corresponding rollback operation must be included. This makes many input/output (I/O) operations difficult or impossible to perform within transactions. Such limits are typically overcome in practice by creating buffers that queue up the irreversible operations and perform them after the transaction succeeds. In Haskell, this limit is enforced at compile time by the data type system.
Композицияланатын операциялар
2005 жылы Тим Харрис, Саймон Марлоу, Саймон Пейтон Джонс және Морис Герлихи Concurrent Haskell-ге негізделген STM жүйесін сипаттады, ол кез келген атомдық операцияларды үлкен атомдық операцияларға біріктіруге мүмкіндік береді, бұл құлыптауға негізделген бағдарламалауда мүмкін емес, пайдалы ұғым. Авторлардың сөзін келтірейік: «Бәлкім, ең негізгі қарсылық [ ] – құлыптауға негізделген бағдарламалар біріктірілмейді: дұрыс фрагменттер біріктірілген кезде қате болуы мүмкін. Мысалы, thread safe insert және delete операциялары бар хэш-кестені қарастырайық. Енді t1 кестесінен бір A элементін өшіріп, оны t2 кестесіне енгізгіміз делік; бірақ аралық күй (екі кестеде де элемент жоқ) басқа жіптерге көрінбеуі керек. Егер хэш-кестені жүзеге асырушы осы қажеттілікті ескермесе, бұл талапты қанағаттандырудың жолы жоқ». [ ] Қысқасы, жеке-жеке дұрыс операцияларды (қосу, өшіру) үлкен дұрыс операцияларға біріктіруге болмайды. —Тим Харрис және т.б., «Біріктірілетін жад транзакциялары», 2-бөлім: Негізгі мәліметтер, 2-бет. STM-мен бұл мәселені шешу оңай: екі операцияны транзакцияға орау біріктірілген операцияны атомдық етеді. Бір ғана қиындық бар: компоненттік әдістердің іске асылу егжей-тегжейін білмейтін шалушыға операция сәтсіз аяқталса, оны қайта орындауға тырысу керек пе, әлде жоқ па, белгісіз. Оған жауап ретінде авторлар қайталап әрекет ету командасын ұсынды, ол сәтсіз транзакция жасаған транзакция журналын пайдаланып, қандай жад жасушаларын оқығанын анықтайды және осы жасушалардың бірі өзгерген кезде транзакцияны автоматты түрде қайталайды, себебі кем дегенде бір мән өзгермегенше транзакция басқаша әрекет етпейді деген логикаға сүйенеді. Авторлар сондай-ақ баламаларды біріктіру механизмін, orElse функциясын ұсынды. Ол бір транзакцияны орындайды және егер ол қайталап әрекет етсе, екінші транзакцияны орындайды. Егер екеуі де қайталап әрекет етсе, тиісті өзгеріс жасалғанша оларды қайтадан сынап көреді. Бұл мүмкіндік, Портативті Операциялық Жүйе Интерфейсі (POSIX) желілік таңдау шақыруы сияқты мүмкіндіктерге ұқсас, шалушыға бірнеше оқиғалардың кез келгеніне бір уақытта күтуге мүмкіндік береді. Ол сонымен қатар бағдарламалау интерфейстерін жеңілдетеді, мысалы, бұғаттау және бұғаттау емес операцияларды алмастырудың қарапайым механизмін ұсынады. Бұл схема Глазго Хаскелл компиляторында жүзеге асырылды.
Perhaps the most fundamental objection [ ] is that lock based programs do not compose: correct fragments may fail when combined. For example, consider a hash table with thread safe insert and delete operations. Now suppose that we want to delete one item A from table t1, and insert it into table t2; but the intermediate state (in which neither table contains the item) must not be visible to other threads. Unless the implementor of the hash table anticipates this need, there is simply no way to satisfy this requirement. [ ] In short, operations that are individually correct (insert, delete) cannot be composed into larger correct operations. —Tim Harris et al., "Composable Memory Transactions", Section 2: Background, pg.2
With STM, this problem is simple to solve: simply wrapping two operations in a transaction makes the combined operation atomic. The only sticking point is that it is unclear to the caller, who is unaware of the implementation details of the component methods, when it should attempt to re execute the transaction if it fails. In response, the authors proposed a retry command which uses the transaction log generated by the failed transaction to determine which memory cells it read, and automatically retries the transaction when one of these cells is modified, based on the logic that the transaction will not behave differently until at least one such value is changed. The authors also proposed a mechanism for composition of alternatives, the orElse function. It runs one transaction and, if that transaction does a retry, runs a second one. If both retry, it tries them both again as soon as a relevant change is made. This facility, comparable to features such as the Portable Operating System Interface (POSIX) networking select call, allows the caller to wait on any one of a number of events simultaneously. It also simplifies programming interfaces, for example by providing a simple mechanism to convert between blocking and nonblocking operations. This scheme has been implemented in the Glasgow Haskell Compiler.