Аннотация:
В первой части статьи рассматриваются основы работы с потоками — запуск, завершение, прерывание, блокировки и базовые сведения о синхронизации.
В статье использован материал из книги Joseph Albahari, Ben Albahari "C# 3.0 in a Nutshell" — http://www.oreilly.com/catalog/9780596527570/
Здравствуйте, Алексей Кирюшкин (перевод), Вы писали:
class ThreadUnsafe
{
static object locker = new object();
static int x;static void Increment()
{
lock (locker)
x++;
}
static void Assign()
{
lock (locker)
x = 123;
}
}
Чо за бред?
Из спецификации языка: 5.5 Atomicity of variable references
Reads and writes of the following data types are atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types.
Здравствуйте, .Den, Вы писали:
D>Чо за бред?
D>Из спецификации языка: 5.5 Atomicity of variable references D>Reads and writes of the following data types are atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types.
Присваиваение и считывание — одна операция. Только вот x++ это не одна операция, а целых три.
_FR>Огорчает нежелание (непонимание ) автора использовать модификатор readonly в объявлении "объекта синхронизации".
Ну так и написали бы почему необходимо использовать...
Здравствуйте, Lloyd, Вы писали:
L>Здравствуйте, Mika Soukhov, Вы писали:
MS>>Присваиваение и считывание — одна операция. Только вот x++ это не одна операция, а целых три.
L>Думаю, .Den неспроста выделил пример с присваиванием.
Если убрать лок у присваивания, то будет возможно ситуация, что между инкрементированием х он станет равным 123. Задача конечно надумана (лично по мне, ну и что, что будет 123), но видимо автор и хотел донести, что или присваивать или инкрементировать.
Сейчас решил прочитать статью (вернее тот участок ), и увидел вот это
Как правило, любое поле, доступное нескольким потокам, должно читаться и записываться с блокировкой. Даже в самом простом случае, операции присваивания одиночному полю, необходима синхронизация. В следующем классе ни приращение, ни присваивание не потокобезопасны:
Выделенное — неправильно. Но на удивление, сам пример корректен.
_FR>>Огорчает нежелание (непонимание ) автора использовать модификатор readonly в объявлении "объекта синхронизации". Dog>Ну так и написали бы почему необходимо использовать...
Не то, что бы необходимо, но, ИМХО, GoodPractice. Значение "объекту синхронизации" присваивается при объявлении:
object syncRoot = new object();
что бы в местах вызова перед испольхованием не беспокоиться о [потокобезопасной] инициализации. Переприсваивать значение данной переменной вредно: не удастся снять существующие локи да и где-то может запоминаться ссылка на значение данной переменной.
А раз переприсваивать значение не нужно и даже опасно, то не надо:
ни рассчитывать на разумность людей, которые будут, возможно, пользоваться вашим кодом;
ни предупреждать, например, в комментариях, что нельзя перезаписывать значение вот этой вот переменной,
когда можно попросту это запретить:
readonlyobject syncRoot = new object();
Теперь, даже если кому-то случайно\по незнанию захочется что-то своё записать в это поле, он узнает, что так делать нельзя.
После знакомства с последними тенденциями (Linq, Nemerle), я процентов 90 полей объявляю как readonly, ибо позволяет избежать множества проблем, самая первая из которых "не null ли в этом поле"? Во-вторых, класс, содержащий только readonly поля автоматически становится потокобезопасным. Есть и много других (большей частью эстетических) плюсов. Общее правило такое: не объявлять поле как НЕ readonly только если его ну никак нельзя сделать readonly.
Да, я знаю, что в Framework Design Guidelines сказано, что этот модификатор рекомендуется использовать только с immutable-типами, но не согласен: модификотором readonly я защищаю не столько "содержимое", "данные" объекта, сколько ссылку на него. В C++ аналогом readonly я бы назвал
T* const field; //константный указатель
, в то время как многим хочется видеть
const T* field; //указатель на константу
Help will always be given at Hogwarts to those who ask for it.
Здравствуйте, Mika Soukhov, Вы писали:
L>>Думаю, .Den неспроста выделил пример с присваиванием.
MS>Если убрать лок у присваивания, то будет возможно ситуация, что между инкрементированием х он станет равным 123. Задача конечно надумана (лично по мне, ну и что, что будет 123), но видимо автор и хотел донести, что или присваивать или инкрементировать.
Здравствуйте, Mika Soukhov, Вы писали:
MS>Как правило, любое поле, доступное нескольким потокам, должно читаться и записываться с блокировкой. Даже в самом простом случае, операции присваивания одиночному полю, необходима синхронизация. В следующем классе ни приращение, ни присваивание не потокобезопасны: MS>[/q]
MS>Выделенное — неправильно. Но на удивление, сам пример корректен.
Здравствуйте, andrey.bond, Вы писали:
AB>Здравствуйте, Mika Soukhov, Вы писали:
MS>>Как правило, любое поле, доступное нескольким потокам, должно читаться и записываться с блокировкой. Даже в самом простом случае, операции присваивания одиночному полю, необходима синхронизация. В следующем классе ни приращение, ни присваивание не потокобезопасны: MS>>[/q]
MS>>Выделенное — неправильно. Но на удивление, сам пример корректен.
AB>Почему?
Потому что, как было сказано выше, считывание и запись — атомарные операции. Блокировки нужно вводить, когда вводится третье действие — операция над данными, которое содержит поле.
Если резюмировать: безопасность инициализации по умолчанию является потоковой — и это может жестко обеспечить readonly. Так, например, в Java тоже делает final.
Может быть неправ (по-крайнем мере в Java это так), но мне кажется в примерах в принципе все корректно. Объект синхронизации является статическим полем, а соответственно инициализируется при первом обращении к нему, то есть как раз когда происходит инициализация класса. Соответственно, потоковая безопасность инициализации и здесь действует.
Здравствуйте, rsn81, Вы писали:
R>Здравствуйте, _FRED_, Вы писали:
R>Если резюмировать: безопасность инициализации по умолчанию является потоковой — и это может жестко обеспечить readonly. Так, например, в Java тоже делает final.
R>Может быть неправ (по-крайнем мере в Java это так), но мне кажется в примерах в принципе все корректно. Объект синхронизации является статическим полем, а соответственно инициализируется при первом обращении к нему, то есть как раз когда происходит инициализация класса. Соответственно, потоковая безопасность инициализации и здесь действует.
Fred целую научную статью написал . Лично я объясняю проще — ридонли нужно помечать все, что онициализирует один раз в конструкторе (статическом или обычном). И не важно какой юз кейс — патоки, шматоки, гуи.
[Филосовствуя] Вообще, нужно было сразу делать поля неизменяемыми. И, если они должны изменятся, нужно явно помечать поле как mutable.
Я имел в виду, что final в Java не просто immutable-модификатор, но еще и играет роль в потоковой безопасности. Подозреваю, что в C# работает с readonly аналогично. Если не прав, пните.
Безопасность инициализации
Новая модель памяти JMM также старается предоставить новые гарантии безопасности инициализации. То есть, если объект соответствующим образом сконструирован (то есть ссылка на объект не опубликована, пока конструирование не завершено), тогда все потоки будут видеть значения его полей final, которые были установлены конструктором, независимо от того, используется ли синхронизация, чтобы передать ссылку из одного потока другому. Более того, переменные, которые могут быть доступны через поле final должным образом сконструированного объекта, например поля объекта, на который ссылается поле final, также гарантированно видимы другим потокам. Это значит, что если поле final содержит ссылку на, скажем, LinkedList, кроме видимости правильного значения ссылки другим потокам, содержимое этого LinkedList во время конструирования будет доступно другим потокам без синхронизации. Результат этого — значительное усиление значения final, то есть поля final могут быть безопасно доступны без синхронизации, и компиляторы могут предположить, что поля final не изменятся, и следовательно смогут оптимизировать множественные выборки.
Final значит final
Механизм, по которому поля final могли менять свое значение под старой моделью памяти был описан в Части 1 — в отсутствии синхронизации другой поток мог сначала видеть значение по умолчанию для поля final, а позже видеть правильное значение.
Под новой моделью памяти есть что-то похожее на отношения по схеме происходит-прежде между записью поля final в конструкторе и начальной загрузкой общедоступной ссылки на этот объект в другом потоке. Когда конструирование завершается, все записи в полях final (и переменных, доступных косвенно через эти поля final) становятся "заблокированными", и любой поток, который содержит ссылку на этот объект, после блокировки будет гарантированно видеть все заблокированные поля для всех заблокированных значений. Записи, которые инициализируют поля final, не будут перераспределяться с операциями, следующими за блокировкой, связанной с конструктором.
По поводу статической инициализации в той же статье:
Вместо блокировки с двойной проверкой используйте идиому Initialize-on-demand Holder Class, которая обеспечивает ленивую инициализацию, потокобезопасность, и работает быстрее и не так запутанно как блокировка с двойной проверкой:
Листинг 2. Идиома Initialize-On-Demand Holder Class
private static class LazySomethingHolder {
public static Something something = new Something();
}
...
public static Something getInstance() {
return LazySomethingHolder.something;
}
Эта идиома основывает свою поточную безопасность на том факте, что операции, которые являются частью инициализации класса, например статические инициализаторы, гарантированно будут видимы всем потокам, которые используют этот класс, а ленивая инициализация является следствием того факта, что внутренний класс не загружается, пока какой-нибудь поток не сошлется на одно из ее полей или методов.
Здравствуйте, Mika Soukhov, Вы писали:
MS>Потому что, как было сказано выше, считывание и запись — атомарные операции. Блокировки нужно вводить, когда вводится третье действие — операция над данными, которое содержит поле.
Хочу спросить: ведь Ваше высказывание справедливо только для однопроцессорных систем? Рихтер в своей книге писал, что для многопроцессорных систем из-за наличия кеширования данных процессорами могут возникнуть проблемы, особенно при записи. Поэтому он рекомендует запись обязательно синхронизировать, а чтение — на усмотрение. Какое Ваше мнение по этому поводу?
Здравствуйте, Mika Soukhov, Вы писали:
MS>Вообще, нужно было сразу делать поля неизменяемыми. И, если они должны изменятся, нужно явно помечать поле как mutable.
Ага, подход Nemerle в этом отношении более привлекателен.
Я решаю эту проблему автоматическим рефакторингом: среда разработки пробегает по коду и все переменные, которые может, делает неизменяемыми.
Атомарность операции и означает, что после ее выполнения данные будут доступны всем потокам, т.е. не будут находиться в регистрах, локальном кэше процессора и т.п.
Здравствуйте, rsn81, Вы писали:
R>Если резюмировать: безопасность инициализации по умолчанию является потоковой — и это может жестко обеспечить readonly. Так, например, в Java тоже делает final.
R>Может быть неправ (по-крайнем мере в Java это так), но мне кажется в примерах в принципе все корректно.
Некоректно, с моей точки зрения, то, что ничто (кроме здравого смысла) не мешает мне в какой-нить хакерской функции переприсвоить значение "объекта синхронизации":
class Test
{
static object syncLock = new object();
static void Proc1() {
lock(syncLock) {
// bla-bla-bla
}//lock
}
static void Proc2() {
lock(syncLock) {
// bla-bla-bla
}//lock
}
static void ProcXXX() {
lock(syncLock) {
syncLock = 8; // Без readonly этому нично не мешает.
// Грабли в этом переприсвоении читателю предлагается найти самому ;о)
}//lock
}
}
Help will always be given at Hogwarts to those who ask for it.
Здравствуйте, tol05, Вы писали:
T>Здравствуйте, Mika Soukhov, Вы писали:
MS>>Потому что, как было сказано выше, считывание и запись — атомарные операции. Блокировки нужно вводить, когда вводится третье действие — операция над данными, которое содержит поле.
T>Хочу спросить: ведь Ваше высказывание справедливо только для однопроцессорных систем? Рихтер в своей книге писал, что для многопроцессорных систем из-за наличия кеширования данных процессорами могут возникнуть проблемы, особенно при записи. Поэтому он рекомендует запись обязательно синхронизировать, а чтение — на усмотрение. Какое Ваше мнение по этому поводу?
Не смешивайте. Проблемы с кэшем решаются через volitale и ему подобные конструкции, а блокировать ли на чтение, это уже другой вопрос, не имеющий отношение к кэшу.
Здравствуйте, tol05, Вы писали:
T>Здравствуйте, Mika Soukhov, Вы писали:
MS>>Потому что, как было сказано выше, считывание и запись — атомарные операции. Блокировки нужно вводить, когда вводится третье действие — операция над данными, которое содержит поле.
T>Хочу спросить: ведь Ваше высказывание справедливо только для однопроцессорных систем? Рихтер в своей книге писал, что для многопроцессорных систем из-за наличия кеширования данных процессорами могут возникнуть проблемы, особенно при записи.
Лечиться write barrier и применяется volitile. Наверное и для многоядерных такое справедливо. У них тоже свой несколько кешей.
T>Поэтому он рекомендует запись обязательно синхронизировать, а чтение — на усмотрение. Какое Ваше мнение по этому поводу?
Обычные сценарии таковы, что:
1) Если поле присваивают несколько раз из внешнего кода, то тип, содержищий данное поле, является простой структурой данных. И ничего там не нужно синхронизировать вообще.
2) Внешний код вообще не может присваивать значение полю, если внутри идет обработка данных, которое содержит это поле. Такое поле присваивается один раз при констуировании объекта (или типа).
Volitile вообще применяется довольно редко. Нужно уж очень специфичную ситуацию, когда без него необойтись.