глава 28 была посвящена выполнению потоками операций ввода-вывода.
Теперь пришло время обсудить вопросы синхронизации потоков. Синхрони-
зация позволяет предотвратить повреждение общих данных при
одновременном
доступе к этим данным разных потоков. Слово «одновременно» выделено не зря,
ведь синхронизация потоков целиком и полностью базируется на контроле време-
ни. Если доступ к неким данным со стороны двух потоков осуществляется таким
образом, что потоки никак не могут помешать друг другу, синхронизации не тре-
буется. В главе 28 было показано, как разные секции асинхронной функции могут
выполняться разными потоками. Теоретически возможно, что два потока будут
работать с одними и теми же данными. Однако асинхронные функции реализованы
таким образом, что два потока не будут одновременно работать с одними данными,
поэтому при обращении кода к данным, содержащимся в асинхронной функции,
синхронизация потоков не нужна.
Этот случай можно считать идеальным, так как синхронизация потоков влечет
за собой много проблем. Во-первых, программировать код синхронизации крайне
утомительно и при этом легко допустить ошибку. В коде следует выделить все дан-
ные, которые потенциально могут обрабатываться различными потоками в одно
и то же время. Затем все эти данные заключаются в другой код, обеспечивающий
их блокировку и разблокирование. Блокирование гарантирует, что доступ к ресурсу
в каждый момент времени сможет получить только один поток. Однако достаточно
при программировании забыть заблокировать хотя бы один фрагмент кода, и ваши
данные будут повреждены. К тому же нет способа проверить, правильно ли работает
блокирующий код. Остается только запустить приложение, провести многочислен-
ные нагрузочные испытания и надеяться, что все пройдет благополучно. При этом
тестирование желательно осуществлять на машине с максимально возможным ко-
821
Примитивные.конструкции.синхронизации.потоков
личеством процессоров, так как это повышает шансы выявить ситуацию, когда два
и более потока попытаются получить одновременный доступ к ресурсу — а значит,
повысит шансы на выявление проблемы.
Второй проблемой блокирования является снижение производительности.
Установление и снятие блокировки требуют времени, так как для этого вызываются
дополнительные методы, причем процессоры должны координировать совместную
работу, определяя, который из потоков нужно блокировать первым. Подобное
взаимодействие процессоров не может не сказываться на производительности.
К примеру, рассмотрим код, добавляющий узел в начало связанного списка:
// Этот класс используется классом LinkedList
public class Node {
internal Node m_next;
// Остальные члены не показаны
}
public sealed class LinkedList {
private Node m_head;
public void Add(Node newNode) {
// Эти две строки реализуют быстрое присваивание ссылок
newNode.m_next = m_head;
m_head = newNode;
}
}
Метод
Add
просто очень быстро присваивает ссылки. И если мы хотим сделать
вызов этого метода безопасным, дав возможность разным потокам одновременно
вызывать его без риска повредить связанный список, следует добавить к методу
Add
код установления и снятия блокировки:
public sealed class LinkedList {
private SomeKindOfLock m_lock = new SomeKindOfLock();
private Node m_head;
public void Add(Node newNode) {
m_lock.Acquire();
// Эти две строки выполняют быстрое присваивание ссылок
newNode.m_next = m_head;
m_head = newNode;
m_lock.Release();
}
}
Теперь метод
Add
стал безопасным в отношении потоков, но скорость его вы-
полнения серьезно упала. Снижение скорости работы зависит от вида выбранного
механизма блокирования; сравнение производительности различных вариантов
блокирования делается как в этой, так и в следующей главах. Но даже самое бы-
строе блокирование заставляет метод
Add
работать в несколько раз медленнее по
сравнению с его версией без блокирования. И разумеется, вызов метода
Add
в цикле
822
Do'stlaringiz bilan baham: |