Синхронізація потоків в Visual C # (Sharp)

Якщо кілька потоків поділяють стан (наприклад, змінну), то може виникнути проблема паралельного використання даних Спільне іспользаніе стану незалежними потоками не представляє проблеми, якщо всі поки звертаються до даних тільки для читання Але що б відбулося, якби на многоядерной машині (див рис 133) потік одного ядра читає стан обєкта, а потік іншого ядра модифікує цей же стан Який стан прочитає перший потік, до або після модифікації Чи буде прочитане стан дейсітельним Швидше за все, немає, і тому доступ потоків до стану необхідно синхронізувати

Розглянемо приклад простої колекції NET Наступний вихідний код виполнтся в зухвалій потоці, створює колекцію списків, після чого додає в колекцію два члена:

List&ltint&gt elements = new List&ltint&gt() elementsAdd(10)

elementsAdd(20)

Наступним кроком ми визначаємо вихідний код для потоку, який обрабативт в циклі елементи колекції:

Thread threadl = new Thread)

О => {

ThreadSleep(1000)

foreach (int item in elements) { ConsoleWriteLinef&quotItem (&quot + item + &quot)&quot) ThreadSleep(1000)

. }

})

Цей потік обробляє в циклі елементи колекції, а два методи Thread, sleep () переводять потік в стан сну на 1000 мілісекунд, або 1 секунду Перекладом потоку в стан сну створюється штучна ситуація, коли інший потік давлять в колекцію елемент, в той час як колекція обробляється в циклі перші потоком

Вихідний код потоку для додавання елемента в колекцію виглядає так:

Thread thread2 = new Thread)

О => {

ThreadSleep(1500) elements Add (30)

})

Обидва потоки запускаються таким чином:

threadlStart() thread2Start()

Виконання цих потоків згенерує виключення, але не відразу ж після їх ЗАПУ Спочатку викликає потік створює і запускає потоки threadl і thread2 Пок threadl переходить в режим сну на 1 секунду, а потік thread2 – на 1,5 Секу Після виходу з режиму сну потік threadl обробляє один елемент колекції, після чого знову переходить в режим сну на 1 секунду Але перед тим як потік threadl знову вийде з режиму сну, прокидається потік thread2 і добавлт елемент у колекцію Коли потік threadl знову прокидається і намагається обротать елемент колекції в наступній ітерації, генерується виключення invalidOperationException

Виняток invalidOperationException вказує, що при обробці елементів у циклі, додавання нових елементів у колекцію не дозволяється Класам коекціі не дуже до душі ідея обробляти в циклі елементи колекції і в той же самий час додавати в неї нові елементи Не можна сказати, що вони не праві в цьому неприхильність, т к такі дії можуть дати непередбачувані результати

Проблемою даного вихідного коду є використання колекцій в многопочном контексті У прикладі в колекцію додається елемент, в той час як

елементи колекції обробляються в циклі Одним з рішень було б зробити кию стану колекції та обробляти в циклі цю копію, а елементи додавати в оригінал стану колекції Широко рекомендованим підходом до цього рішення є використання класу SystemCollections ObjectModel ReadOnlyCollection, як показано в наступному прикладі:

using SystemCollectionsObjectModel

List&ltint&gt elements = new List&ltint&gt() elementsAdd(10)

elementsAdd(20)

Thread threadl = new Thread)

{) =&gt {

ThreadSleep(lOOO)

foreach (int item in new ReadOxilyCollectiori&ltint&gt(elements)) { ConsoleWriteLine(&quotItem (&quot + item + &quot)&quot) ThreadSleep(1000)

}

})

Thread thread2 = new Thread(

О => {

ThreadSleep(1500) elementsAdd(30)

})

threadlStart() thread2Start()

Доданий код (виділений жирним шрифтом) створює екземпляр типу Sys-tem collectionsReadOniyCoiiection, якому передається список елементів Тип ReadOniyCoiiection надає базовий клас для загальної колекції, доупной тільки для читання Після цього итератор foreach проходиться в циклі по колекції, доступною тільки для читання, але заснованої на первісної коекціі Але виконання і цього коду також викличе те ж саме виняток Принісши цьому є той факт, що клас ReadOniyCoiiection не створює копію колекції, а тільки маскує її Маска забороняє додавання елементів в коекцію, але оскільки інший потік іде навпростець і модифікує першоосновою колекцію, доступна тільки для читання колекція також піддається цієї модифікації

Але скажемо, у нас вийшло перетворити колекцію в доступну тільки для читання Це все одно нічого б не вирішило У такому випадку другий потік сгенеровал б виключення, т к не можна додавати елементи в колекцію, доступну тільки для читання Суть проблеми полягає в тому, що для розробки многопочного коду з розділяються змінними у нас немає легкого рішення, т к ми намагаємося вирішити проблему, як кілька кухарів можуть ефективно працювати в одній кухні, не натикаючись один на одного

Ми маємо справу з класичною проблемою зчитування / записування, в якій ои потоки зацікавлені тільки в читанні даних, в той час як інші хочуть лише модифікувати їх Одним із способів вирішення цієї проблеми є синхронізація читачів і письменників за допомогою виключає блокування (Exclusive lock) з тим, щоб у будь-який час тільки один потік міг мати доступ до даних, не важливо – для зчитування або записування

Використання виключають блокувань

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

Далі наведено приклад коду з використання виключають блокувань:

List&ltint&gt elements = new List&ltint&gt() elementsAdd(10)

elementsAdd(20)

Thread threadl = new Thread(

О => {

ThreadSleep(1000)

lock (elements) {

foreach (int item in elements) { ConsoleWriteLine(&quotItem (&quot + item + &quot)&quot) ThreadSleep(1000)

}

}

})

Thread thread2 = new Thread(

О => {

ThreadSleep(1500)

lock (elements)  {

threadlStart() thread2Start()

}

})

elementsAdd(30)

У виділеному жирним шрифтом коді використовується ключове слово lock, яке представляє блок коду з винятковим доступом У кожному випадку потоку проставляється доступ тільки до одного блоку коду У коді всередині блоку можна веть, що звернення до колекції виконується в двох місцях Застосування ісклающей блокування, коли один потік може звертатися тільки до одного блоку, дозволяє одному потоку записувати в колекцію, а іншому зчитувати з неї

Оператор lock має параметр, який вказує область дії блокування В обох потоках цією областю є elements Дана загальна область сінхрізірует доступ до коду У будь-який момент часу код всередині блоку lock може виконуватися тільки одним потоком Таким чином, ми отримали бажану воожность, коли тільки один потік може мати доступ до коду для читання або запису колекції Виконання програми протікає в такий спосіб:

1 Обидва потоки перебувають у стані очікування

2 Після 1 секунди потік thread l захоплює блокування, тому що вона була вільною

3 Потік threadl виконує свій код

4 Коли потік thread l починає виконання синхронізованого коду, ніякий інший код не може захопити блокування, асоційовану з змінними елементами

5 Коли потік thread2 прокидається після 1,5 секунди, він намагається захопити блокування, але не може цього зробити, тому що вона все ще утримується потоком threadl Тому ПОТІК thread2 Повинен чекати

6 Після закінчення ще півтори секунди, потік threadl звільняє блокування і завершує виконання синхронізувати коду, що дозволяє потоку thread2 додати елемент в колекцію На цей раз ніяких винятків не створюється

Параметр, переданий оператору lock, не обовязково повинен бути ресурсом, над якими виконуються маніпуляції в блоці коду Це може бути примірник будь-якого обєкта, можна навіть використовувати обєкт syncRoot, як показано в слующем коді:

object _syncRoot = new ObjectO

lock( _syncRoot) {

}

Виключають блокування необхідно використовувати як для читання, так і для запису обєктів Не думайте, що виключають блокування необхідні тільки

при модифікації даних, т к код може зчитувати стан, що знаходиться в процесі модифікування Як було продемонстровано, модифікування колекції в той час, коли над нею виконується операція читання, спотворює даие і викликає виняток У наступному фрагменті коду при читанні блокування не задана, що викликає виняток:

List&ltint&gt elements = new List&ltint&gt() elementsAdd(lO)

elementsAdd(20)

Thread threadl = new Thread(

О => {

ThreadSleep(1000)

foreach (int item in elements) { ConsoleWriteLineCItem (&quot + item + &quot)&quot) ThreadSleep(1000)

}

})

Thread thread2 = new Thread(

() =&gt {

ThreadSleep(1500) lock (elements) {

elementsAdd(30)

}

})

threadlStart() thread2Start()

Повернемося до коду, який працював Здається, що там все в порядку Але все в пядке тільки в аспекті працездатності коду В аспекті ж ефективності іолненія, все далеко не в порядку, т к операція читання колекції змушує зісивающій код простоювати без потреби

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

Синхронізація клонуванням

Однією з найефективніших способів синхронізації буде створення клону обкта, щоб дозволити не блокувати копію, над якою виконується читання,

і не змушувати чекати пишучий потік Слідуючи цьому підходу, попередній код можна переписати таким чином:

L i s t &lt i n t &gt    element s    =    ne w   L i s t &lt i n t &gt ( ) elementsAdd(10)

elementsAdd(20)

Threa d   t h r e a d l    =    ne w   Thread (

О => {

ThreadSleep(lOOO) int[] items

lock (elements) {

items = elementsToArray()

}

foreach  (int item in items) { ConsoleWriteLine(&quotItem (&quot + item + &quot)&quot) ThreadSleep(lOOO)

}

})

Thread thread2 = new Thread(

О => {

ThreadSleep(1500) lock (elements) {

elements Add (30)

}

})

threadlStart() thread2Start()

У цьому коді також використовується блокування, але тільки там, де це необхідно Коли елементи колекції обробляються в циклі, блокування застосовується до операції копіювання колекції в масив (метод тоАггауО) Для проходження ж за елементами масиву блокування не застосовується Тому код не повинен очікувати для виконання запису в колекцію, т к вона не заблокована

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

Візьмемо, наприклад, завантаження тексту в текстовий редактор Коли редактор Microsoft Word виконує цю операцію, то він відразу ж виводить на екран першу завантажену сторінку, дозволяючи відразу ж приступити до роботи А в цей час опація завантаження решти сторінок виконується у фоні обробки тексту Цей же

ефект дає і підхід з клонуванням колекції, який стає навіть більш ефективним на машині з багатоядерним процесором

Як загальне правило при роботі з потоками, використовуйте якомога менше блоковок, але застосовуйте їх завжди, коли необхідно Блокуйте якомога мение фрагменти коду Блокування синхронізують доступ до ресурсів, тому тільки один потік може виконувати заблокований фрагмент коду Чим менше часу фрагмент коду заблокований, тим швидше буде весь код

Джерело: Гросс К С # 2008: Пер з англ – СПб: БХВ-Петербург, 2009 – 576 е: ил – (Самовчитель)

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*