Если у каждого счета есть своя собственная блокировка, обе блокировки должны быть введены, прежде чем сумма может быть вычислена.
Напротив, код, который постепенно увеличивает highPriRequests только, должен взять единственную блокировку. Это вызвано тем, что единственный инвариант, используемый кодом обновления, - то, что highPriRequests является точным счетом; lowPriRequests не включен что. В целом, когда код требует инварианта программы, все блокировки, связанные с любой памятью, включенной с инвариантом, должны быть взяты.
Полезная аналогия, которая помогает иллюстрировать этот тезис, показана в рисунке 5. Думайте о памяти компьютера как о монетном автомате с тысячами окон, один для каждого местоположения памяти. Когда запускаете программу, она походит на натяжение ручки монетного автомата. Местоположения памяти начинают вращаться, поскольку другие нити изменяют ценности памяти. Когда нить вводит блокировку, местоположения, связанные с блокировкой, прекращают вращаться, потому что код последовательно следует соглашению, что блокировка взята, прежде чем любое обновление предпринято. Нить может повторить этот процесс, беря больше блокировок и заставив больше памяти заморозиться, пока все местоположения памяти, необходимые нити, не стабилизированы. Нить теперь имеет возможность выполнять работу без вмешательства от других нитей.
Эта аналогия полезна в изменении мышления программиста от веры, что ничто не изменяется, если это явно не изменено на веру, что все изменяется, если блокировки не используются, чтобы предотвратить его. Принятие этого нового мышления является самым важным советом когда создающие мультипереплетенные приложения.
Какая защита блокировки потребностей памяти?
Видели, как использовать блокировки, чтобы защитить инварианты программы, но я не был точен о том, какая память требует такой защиты. Простое (и правильный) ответ был бы то, что вся память должна быть защищена блокировкой, но это было бы излишеством для большинства приложений.
Память может быть сделана безопасной для мультипереплетенного использования одним из нескольких способов. Во-первых, память, к которой только получает доступ единственная нить, безопасна, потому что другие нити незатронуты ею. Это включает большинство местных переменных и всю распределенную по куче память, прежде чем она будет опубликована (сделанный достижимым к другим нитям). Как только память опубликована, однако, она падает из этой категории, и некоторый другой метод должен использоваться.
Во-вторых, память, которая только для чтения после публикации, не требует блокировки, потому что любые инварианты, связанные с ним, должны держаться для остальной части программы (так как стоимость не изменяется).
В-третьих, память, которая активно обновлена от многократных нитей обычно, использует блокировки, чтобы удостовериться, что только у одной нити есть доступ, в то время как инвариант программы сломан.
Наконец, в определенных специализированных случаях, где инвариант программы относительно слаб, возможно выполнить обновления, которые могут быть сделаны без блокировок. Как правило, специализированные инструкции сравнивать-и-обменивать используются. Эти методы лучше всего считаются специальными, легкими внедрениями блокировок.
Вся память, используемая в программе, должна попасть в один из этих четырех случаев. Кроме того, заключительный случай является значительно более тонким и подверженным ошибкам, и таким образом должен только использоваться, когда эксплуатационные требования оправдают дополнительный уход и рискуют включенный (я буду посвящать будущую статью этому случаю). Игнорируя тот случай на данный момент, общее правило должно состоять в том, что вся память программы должна попасть в один из трех интервалов: нить, исключительная, только для чтения, или блокировка, защищена.