Кога записите/четенията засягат основната памет?

Когато напиша стойност в поле, какви гаранции получавам относно това кога новата стойност ще бъде запазена в основната памет? Например, как да разбера, че процесорът не запазва новата стойност в личния си кеш, а е актуализирал основната памет?
Друг пример:

int m_foo;

void Read() // executed by thread X (on processor #0)
{
   Console.Write(m_foo);
}

void Write() // executed by thread Y (on processor #1)
{
   m_foo = 1;
}

Има ли възможност след приключване на изпълнението на Write(), друга нишка да изпълни Read(), но всъщност ще види "0" като текуща стойност? (тъй като може би предишното записване в m_foo все още не е било прочистено?).
Какъв вид примитиви (освен брави) са налични, за да се гарантира, че записите са били прочистени?


РЕДАКТИРАНЕ
В примерния код, който използвах, записът и четенето са поставени при различен метод. Thread.MemoryBarrier не засяга ли само пренареждането на инструкции, които съществуват в същия обхват?

Освен това, нека приемем, че те няма да бъдат вградени от JIT, как мога да се уверя, че стойността, записана в m_foo, няма да се съхранява в регистър, а в основната памет? (или когато m_foo се чете, няма да получи стара стойност от кеша на процесора).

Възможно ли е да се постигне това без използване на ключалки или ключовата дума „volatile“? (да речем също, че не използвам примитивни типове, а структури с размер WORD [така че volatile не може да се прилага].)


person Community    schedule 14.11.2009    source източник
comment
Ако имате структури с размер WORD, можете да ги конвертирате в uint и можете да приложите volatile или всяка друга техника, обсъждана в различните отговори.   -  person Abel    schedule 14.11.2009


Отговори (4)


Volatile и Interlocked вече бяха споменати, вие поискахте примитиви, едно допълнение към списъка е да използвате Thread.MemoryBarrier() преди вашите записи или четения. Това гарантира, че няма да се извършва пренареждане на записи и четения в паметта.

Това прави "на ръка" това, което lock, Interlocked и volatile могат да правят автоматично през повечето време. Можете да използвате това като пълна замяна на всяка друга техника, но това е може би най-трудният път за пътуване и така казва MSDN:

"Трудно е да се изграждат правилни многонишкови програми с помощта на MemoryBarrier. За повечето цели операторът за заключване на C#, операторът SyncLock на Visual Basic и методите на класа Monitor предоставят по-лесни и по-малко склонни към грешки начини за синхронизиране на достъпа до паметта . Препоръчваме ви да ги използвате вместо MemoryBarrier. "

Как да използвате MemoryBarrier

Много добър пример са реализациите на VolatileRead и VolatileWrite, които и двете вътрешно използват MemoryBarrier. Основното правило, което трябва да следвате, е: когато четете променлива, поставете бариера на паметта след четенето. Когато пишете стойността, бариерата на паметта трябва да е преди записа.

В случай, че се съмнявате дали това е по-малко ефективно от lock, помислете, че заключването не е нищо повече от "пълно ограждане", тъй като поставя бариера на паметта преди и след кодовия блок (игнорирайки Monitor за момент). Този принцип е добре обяснен в тази отлична окончателна статия за нишки, заключване, летливи и бариери на паметта от Албахари.

От рефлектор:

public static void VolatileWrite(ref byte address, byte value)
{
    MemoryBarrier();
    address = value;
}

public static byte VolatileRead(ref byte address)
{
    byte num = address;
    MemoryBarrier();
    return num;
}
person Abel    schedule 14.11.2009
comment
В примерния код, който използвах, записът и четенето са поставени на различен метод. Thread.MemoryBarrier не засяга ли само пренареждането на инструкции, които съществуват в същия обхват? Освен това, нека приемем, че те няма да бъдат вградени от JIT, как мога да се уверя, че стойността, записана в m_foo, няма да се съхранява в регистър, а в основната памет? (или когато се чете m_foo, той няма да получи стара стойност от кеша на процесора). Възможно ли е да се постигне това без използване на ключалки или ключовата дума „volatile“? - person ; 14.11.2009
comment
Да, така е и да, можете да използвате MemoryBarrier за това. Но дали това е разумно и препоръчително е нещо друго. Ще актуализирам въпроса си с обяснение на този бит. - person Abel; 14.11.2009
comment
Така че практически, ако използвам ограда, след като напиша новата стойност, е гарантирано, че новата стойност ще бъде преместена от регистъра в основната памет? и ако използвам ограда, преди да прочета стойността, тогава това всъщност обезсилва кеша, карайки процесора да получи стойността и от основната памет? - person ; 14.11.2009
comment
да Не знам дали наистина обезсилва кеша на процесора, но е гарантирано, че всички инструкции, записващи на адрес, ще бъдат завършени, когато четете от този адрес (ако четенето е поставено след MemoryBarrier). Ето добро допълнително четиво, което обяснява този принцип в по-общи термини: en.wikipedia.org/wiki/Memory_barrier - person Abel; 14.11.2009

Ако искате да сте сигурни, че е написан навреме и в ред, тогава го маркирайте като volatile или (с повече болка) използвайте Thread.VolatileRead / Thread.VolatileWrite (не е привлекателна опция и лесно може да се пропусне, правейки я безполезна).

volatile int m_foo;

В противен случай практически нямате гаранции за нищо (щом говорите няколко теми).

Може също да искате да разгледате заключването (Monitor) или Interlocked, което ще постигне същия ефект стига да се използва същият подход от всички достъпи (т.е. всички lock или всички Interlocked и т.н.).

person Marc Gravell    schedule 14.11.2009
comment

Разделянето на число на безкрайност е напълно разумно от математическа гледна точка. Това е число, безкрайно близко до нула, без всъщност да достига нула.

От софтуерна гледна точка това е много по-трудно, тъй като безкрайността не е дискретно представима стойност. Предполагам, че вашата разлика в поведението се основава на дизайнерски решения.

Във Firefox вероятно са избрали да върнат стойност нула, тъй като за всички практически цели този резултат ще работи за това, от което се нуждаете.

В IE, от друга страна, изглежда, че разработчиците са взели съзнателното решение да не ви позволят да извършите изчисление, за което са знаели, че не могат да дадат дискретен отговор.

- person Jon Skeet; 14.11.2009
comment
Бележка за volatile: този ред не е гарантиран, когато четене следва запис, което прави volatile по-малко идеален за работата, както може да се очаква. Това изглежда малко известен факт, но все пак е гаранция за бедствие. Между другото, това е обяснено тук: albahari.com/threading/part4.aspx#_Memory_Barriers - person Abel; 14.11.2009
comment
С други думи, ако трябва да четете след писане, трябва да използвате пълна ограда и да използвате Interlocked (използва пълни огради) или традиционно заключване. - person Abel; 14.11.2009
comment
@Abel: Имах предвид само подреждането на записите. - person Jon Skeet; 14.11.2009
comment
@Jon: и не видях текста ти, преди да го изпратя. Просто исках да направя обща забележка относно използването на volatile ;-) - person Abel; 14.11.2009
comment
Не влияе ли „нестабилният“ (и оградите на паметта като цяло) само подреждането на инструкциите, или също така влияе, когато стойностите се изхвърлят от регистрите в основната памет? (или принуждаване на процесора да получи стойността от основната памет вместо от личния си кеш). Възможно ли е да се каже на процесора да прочете стойността от основната памет, без да се използва ключовата дума 'volatile'? (да кажем, че имам структура от 4 байта, вместо примитивен тип. Какво тогава? - person ; 14.11.2009

Докато не използвате никаква синхронизация, нямате гаранция, че нишка, работеща на един процесор, вижда промените, направени от друга нишка, работеща на друг процесор. Това е така, защото стойността може да се кешира в кеш паметта на процесора или в регистър на процесора.

Следователно трябва или да маркирате променливата като непостоянна. Това ще създаде релация „Случва се преди“ между четене и запис.

person Gamlor    schedule 14.11.2009

Това не е проблем с кеша на процесора. Записите обикновено са преминаващи (записите отиват както в кеша, така и в основната памет) и всички четения имат достъп до кеша. Но има много други кешове по пътя (език за програмиране, библиотеки, операционна система, I/O буфери и т.н.). Компилаторът може също така да избере да запази променлива в регистър на процесора и никога да не я записва в основната памет (за това е предназначен операторът volatile, избягвайте съхраняването на стойност в регистъра, когато може да бъде I/O, картографиран в паметта).

Ако имате множество процеси или множество нишки и синхронизирането е проблем, трябва да го направите изрично, има много начини да го направите в зависимост от случая на употреба.

За програма с една нишка не се интересувайте, компилаторът ще направи това, което трябва и чете ще има достъп до написаното.

person kriss    schedule 14.11.2009