Чем отличаются rwmutex от mutex
Перейти к содержимому

Чем отличаются rwmutex от mutex

  • автор:

12.2. Обеспечение потокозащищенности ресурсов

В программе используется несколько потоков и требуется гарантировать невозможность модификации ресурса несколькими потоками одновременно. В целом это называется обеспечением потокозащищенности (thread-safe) ресурсов или сериализацией доступа к ним.

Используйте класс mutex, определенный в boost/thread/mutex.hpp, для синхронизации доступа к потокам. Пример 12.2 показывает, как можно использовать в простых случаях объект mutex для управления параллельным доступом к очереди.

Пример 12.2. Создание потокозащищенного класса

// Простой класс очереди; в реальной программе вместо него следует

void enqueue(const T& x)

// Блокировать мьютекс для этой очереди

// scoped_lock автоматически уничтожается (и, следовательно, мьютекс

// разблокируется) при выходе из области видимости

throw «empty!»; // Это приводит к выходу из текущей области

T tmp = list_.front(); // видимости, поэтому блокировка освобождается

> // Снова при выходе из области видимости мьютекс разблокируется

Обеспечение потокозащищенности классов, функций, блоков программного кода и других объектов является сущностью многопоточного программирования. Если вы проектируете какой-нибудь компонент программного обеспечения с возможностями многопоточной обработки, то можете постараться обеспечить каждый поток своим набором ресурсов, например объектами в стеке и динамической памяти, ресурсами операционной системы и т.д. Однако рано или поздно вам придется обеспечить совместное использование различными потоками каких-либо ресурсов. Это может быть совместная очередь поступающих запросов (как это происходит на многопоточном веб-сервере) или нечто достаточно простое, как поток вывода (например, в файл журнала или даже в cout). Стандартный способ координации безопасного совместного использования ресурсов подразумевает применение мьютекса (mutex), который обеспечивает монопольный доступ к чему-либо.

Остальная часть обсуждения в целом посвящена мьютексам, и в частности методам использования boost::mutex для сериализации доступа к ресурсам. Я использую терминологию подхода «концепция/модель», о котором я говорил кратко во введении настоящей главы. Концепция — это абстрактное (независимое от языка) описание чего-либо, а модель концепции — конкретное ее представление в форме класса С++. Уточнение концепции — это определенная концепция с некоторыми дополнительными возможностями.

Все-таки параллельное программирование представляет собой сложную тему, и в одном рецепте нельзя отразить все применяемые в этой технологии методы. Можно использовать много шаблонов проектирования и разных стратегий, подходящих для различных приложений. Если при проектировании программного обеспечения вы предполагаете, что многопоточная обработка составит значительный объем, или проектируете высокопроизводительные приложения, необходимо прочитать хорошую книгу по шаблонам многопоточной обработки. Многие проблемы, связанные с трудностями отладки многопоточных программ, могут быть успешно преодолены за счет тщательного и продолжительного проектирования.

Концепция мьютекса проста: мьютекс это некий объект, представляющий ресурс; только один поток может его блокировать или разблокировать в данный момент времени. Он является флагом, который используется для координации доступа к ресурсу со стороны нескольких пользователей. В библиотеке Boost Threads моделью концепции мьютекса является класс boost::mutex. В примере 1 2.2 доступ для записи в классе Queue обеспечивается переменной-членом mutex.

mutex_ должен блокироваться какой-нибудь функцией-членом, которая должна изменять состояние очереди обслуживаемых элементов. Сам объект mutex_ ничего не знает о том, что он представляет. Это просто флаг блокировки/разблокировки, используемый всеми пользователями некоторого ресурса.

В примере 12.2, когда какая-нибудь функция-член класса Queue собирается изменить состояние объекта, она сначала должна заблокировать mutex_. Только один поток в конкретный момент времени может его заблокировать, что не позволяет нескольким объектам одновременно модифицировать состояние объекта Queue. Таким образом, мьютекс mutex представляет собой простой сигнальный механизм, но это нечто большее, чем просто bool или int, потому что для mutex необходим сериализованный доступ, который может быть обеспечен только ядром операционной системы. Если вы попытаетесь сделать то же самое с bool, это не сработает, потому что ничто не препятствует одновременной модификации состояния bool несколькими потоками. (В разных операционных системах это осуществляется по-разному, и именно поэтому не просто реализовать переносимую библиотеку потоков.)

Объекты mutex блокируются и разблокируются, используя несколько различных стратегий блокировки, самой простой из которых является блокировка scoped_lock. scoped_lock — это класс, при конструировании объекта которого используется аргумент типа mutex, блокируемый до тех пор, пока не будет уничтожена блокировка lock. Рассмотрим функцию-член enqueue в примере 12.2, которая показывает, как scoped_lock работает совместно с мьютексом mutex_.

void enqueue(const T& x)

Когда lock уничтожается, mutex_ разблокируется. Если lock конструируется для объекта mutex, который уже заблокирован другим потоком, текущий поток переходит в состояние ожидания до тех пор, пока lock не окажется доступен.

Такой подход поначалу может показаться немного странным: а почему бы мьютексу mutex не иметь методы lock и unlock? Применение класса scoped_lock, который обеспечивает блокировку при конструировании и разблокировку при уничтожении, на самом деле более удобно и менее подвержено ошибкам. Когда вы создаете блокировку, используя scoped_lock, мьютекс блокируется на весь период существования объекта scoped_lock, т.е. вам не надо ничего разблокировать в явной форме на каждой ветви вычислений. С другой стороны, если вам приходится явно разблокировать захваченный мьютекс, необходимо гарантировать перехват любых исключений, которые могут быть выброшены в вашей функции (или где-нибудь выше ее в стеке вызовов), и гарантировать разблокировку mutex. При использовании scoped_lock, если выбрасывается исключение или функция возвращает управление, объект scoped_lock автоматически уничтожается и mutex разблокируется.

Использование мьютекса позволяет сделать всю работу, однако хочется немного большего. При таком подходе нет различия между чтением и записью, что существенно, так как неэффективно заставлять потоки ждать в очереди доступа к ресурсу, когда многие из них выполняют только операции чтения, для которых не требуется монопольный доступ. Для этого в библиотеке Boost Threads предусмотрен класс read_write_mutex. Пример 12.3 показывает, как можно реализовать пример 12.2, используя read_write_mutex с функцией-членом front, которая позволяет вызывающей программе получить копию первого элемента очереди без его выталкивания.

Пример 12.3. Использование мьютекса чтения/записи

Mutexes and RWMutex in Golang

This article was originally published here If you have ever used processing using multiple threads, you probably are fully aware about what are race conditions. Just to recap, it happens when two processing threads are accessing the same value in memory, and are changing it. Because of non-deterministic nature of concurrent threads, the value stored will comply for only one specific thread, which leads to a inconsistent behaviour. If you are working on a payment system, or a banking software this can can cost business huge amount of money. To avoid that, there is a mechanism in go for locking the value in the memory, called Mutext . This stands for mutual exclusion, and what it means is that it keeps track of which thread has access to specific value in memory, at specific point in time. Let’s consider the following piece of code:

type Account struct  balance int Name string > func (a *Account) Withdraw(amount int)  a.balance -= amount > func (a *Account) Deposit(amount int)  a.balance += amount > func (a *Account) GetBalance() int  return a.balance > 

Enter fullscreen mode

Exit fullscreen mode

It is a pretty simple structure in Go, that keeps track of accounts balance, nothing so special. However, if we will try to withdraw, or deposit an amount on some specific account from multiple goroutines, it will inevitably lead to a race condition, like this:

var account Account account.Name = "Test account" for i := 0; i  20; i++  wg.Add(1) go account.Deposit(100) > for i := 0; i  10; i++  wg.Add(1) go account.Withdraw(100) > wg.Wait() fmt.Printf("Balance: %d\n", account.GetBalance()) 

Enter fullscreen mode

Exit fullscreen mode

This means, that when running this code several times, the value of balance will be inconsistent, meaning it will have different values. In order to avoid that, we need to add locks for both reading and writing to the values. In our case, the Mutex lock will be added as a field to this structure, which Golang runtime will understand, that this lock is applied for the scope of this specific struct:

type Account struct  balance int Name string lock sync.Mutex > func (a *Account) Withdraw(amount int, wg *sync.WaitGroup)  defer wg.Done() a.lock.Lock() a.balance -= amount a.lock.Unlock() > func (a *Account) Deposit(amount int, wg *sync.WaitGroup)  defer wg.Done() a.lock.Lock() a.balance += amount a.lock.Unlock() > func (a *Account) GetBalance() int  a.lock.Lock() defer a.lock.Unlock() return a.balance > 

Enter fullscreen mode

Exit fullscreen mode

and the client code will now look like this:

 var account Account var wg sync.WaitGroup account.Name = "Test account" for i := 0; i  20; i++  wg.Add(1) go account.Deposit(100, &wg) > for i := 0; i  10; i++  wg.Add(1) go account.Withdraw(100, &wg) > wg.Wait() fmt.Printf("Balance: %d\n", account.GetBalance()) 

Enter fullscreen mode

Exit fullscreen mode

Now, it is more consistent, and the value of the balance will be the same.

Problem solved, why we need RWMutex?

That’s fair enough. But the problem is, that when using Mutex the value from the memory will be locked until the Unlock method will be invoked. This is also valable for the reading phase. In order to make reading accessible for multiple threads, the Mutex can be replaced with RWMutex , and for reading it will be used RLock and RUnlock methods. In this case the code will look something like this:

type Account struct  balance int Name string lock sync.RWMutex > func (a *Account) Withdraw(amount int, wg *sync.WaitGroup)  defer wg.Done() a.lock.Lock() defer a.lock.Unlock() a.balance -= amount > func (a *Account) Deposit(amount int, wg *sync.WaitGroup)  defer wg.Done() a.lock.Lock() defer a.lock.Unlock() a.balance += amount > func (a *Account) GetBalance() int  a.lock.RLock() defer a.lock.RUnlock() return a.balance > 

Enter fullscreen mode

Exit fullscreen mode

Note, that other thing that changed, is the defer unlock for Deposit and Withdraw . This may be useful for other complex operations, that may result into unlocked locks, due to failures, that may occur to the code.

Conclusion

Race conditions very often are hard to detect, and can lead to serious bugs in system functionality. The most straightforward approach would be to use Mutex for locking your critical data. The RWMutex can be used if, and only if there are a limited number of concurrent readers of that data. Starting from a number of concurrent readers, it may become ineffective to use RWMutex .

Примитивы синхронизации в Go

Продолжаем серию статей о проблемах многопоточности, параллелизме, concurrency и других интересных штуках.

  1. Race condition и Data Race
  2. Deadlocks, Livelocks и Starvation
  3. Примитивы синхронизации в Go
  4. Безопасная работа с каналами в Go
  5. Goroutine Leaks

Примитивы Синхронизации в Go

Продолжаем серию статей о проблемах многопоточности, параллелизме, concurrency и других интересных штуках. Race…

Пакет sync содержит примитивы, которые наиболее полезны для низкоуровневой синхронизации доступа к памяти.

WaitGroup

WaitGroup это отличный способ дождаться завершения набора одновременных операций.

Запустим несколько goroutine и дождемся завершения их работы:

var wg sync.WaitGroup

wg.Add(1)
go func() defer wg.Done()

fmt.Println("1st goroutine sleeping. ")
time.Sleep(100 * time.Millisecond)
>()

wg.Add(1)
go func() defer wg.Done()

fmt.Println("2nd goroutine sleeping. ")
time.Sleep(200 * time.Millisecond)
>()

wg.Wait()
fmt.Println("All goroutines complete.")

У нас нет гарантий когда будут запущены наши goroutine. Возможна ситуация когда при вызове Wait еще не будет ни одной запущенной goroutine. По этому важно вызвать Add за пределами процедур, которые они помогают отслеживать.

Пример неопределенного поведения:

var wg sync.WaitGroupgo func() wg.Add(1) 
defer wg.Done()
fmt.Println("1st goroutine sleeping. ")
time.Sleep(1)
>()
wg.Wait()
fmt.Println("All goroutines complete.")

О WaitGroup можно думать как о concurrent-safe счетчике.

Вызовы Add увеличивает счетчик на переданное число, а вызовы Done уменьшают счетчик на единицу. Wait блокируется пока счетчик не станет равным нулю.

Обычно Add вызывают как можно ближе к goroutine. Но иногда удобно используют Add для отслеживания группы goroutine одновременно. Например в таких циклах:

hello := func(wg *sync.WaitGroup, id int) defer wg.Done() 
fmt.Printf("Hello from %v!\n", id)
>

const numGreeters = 5
var wg sync.WaitGroup

wg.Add(numGreeters)
for i := 0; i < numGreeters; i++ go hello(&wg, i+1)
>

wg.Wait()

GermanGorelkin/go-patterns

Design patterns implemented in Golang. Contribute to GermanGorelkin/go-patterns development by creating an account on…

Mutex and RWMutex

Mutex означает mutual exclusion(взаимное исключение) и является способом защиты critical section(критическая секция) вашей программы.

Критическая секция — это область вашей программы, которая требует эксклюзивного доступа к общему ресурсу. При нахождении в критической секции двух (или более) потоков возникает состояние race(гонки). Так же возможны проблемы взаимной блокировки(deadlock).

Mutex обеспечивает безопасный доступ к общим ресурсам.

Простой пример счетчика:

type counter struct count int
>
func (c *counter) Increment() c.count++
>
func (c *counter) Decrement() c.count--
>

Напишем тест, который будет в разных goroutine увеличивать или уменьшать общее значение:

c := new(counter)

var wg sync.WaitGroup
numLoop := 1000

wg.Add(numLoop)
for i := 0; i < numLoop; i++ go func() defer wg.Done()
c.Increment()
>()
>

wg.Add(numLoop)
for i := 0; i < numLoop; i++ go func() defer wg.Done()
c.Decrement()
>()
>

wg.Wait()

expected := 0
assert.Equal(t, expected, c.count)

Результат:

expected: 0
actual: 52

Используем Mutex для синхронизации доступа:

type counter struct sync.Mutex 
count int
>
func (c *counter) Increment() c.Lock()
defer c.Unlock()

c.count++
>
func (c *counter) Decrement() c.Lock()
defer c.Unlock()

c.count--
>

Мы вызываем Unlock в defer . Это очень распространенная идиома при использовании Mutex, чтобы гарантировать, что вызов всегда происходит, даже при панике. Несоблюдение этого требования может привести к deadlock вашей программы. Хотя defer и несет небольшие затраты.

Критическая секция названа так, потому что она отражает узкое место в вашей программе. Вход в критическую секцию и выход из нее обходится довольно дорого, поэтому обычно люди пытаются минимизировать время, проведенное в критических секциях.

Возможно не все процессы будут читать и записывать в общую память. В этом случае вы можете воспользоваться мьютексом другого типа.

RWMutex

RWMutex концептуально то же самое, что и Mutex: он защищает доступ к памяти. Тем не менее, RWMutex дает вам немного больше контроля над памятью. Вы можете запросить блокировку для чтения, и в этом случае вам будет предоставлен доступ, если блокировка не удерживается для записи.

Это означает, что произвольное число читателей может удерживать блокировку читателя, пока ничто другое не удерживает блокировку писателя.

Посмотрим как это работает:

func (c *counter) CountV1() int c.Lock() 
defer c.Unlock()
return c.count
>
func (c *counter) CountV2() int c.RLock()
defer c.RUnlock()

return c.count
>

CountV2 не блокирует count если не было блокировок на запись.

Немного бенчмарков:

func BenchmarkCountV1(b *testing.B) c := new(counter) 
var wg sync.WaitGroup
for i := 0; i < b.N; i++ for j := 0; j < 1000; j++ wg.Add(1)
go func() defer wg.Done()
c.CountV1()
>()
>
wg.Wait()
>
>

func BenchmarkCountV2(b *testing.B) c := new(counter)
var wg sync.WaitGroup
for i := 0; i < b.N; i++ for j := 0; j < 1000; j++ wg.Add(1)
go func() defer wg.Done()
c.CountV2()
>()
>
wg.Wait()
>
>

Результаты:

enchmarkCountV1-8 2132 501896 ns/op
BenchmarkCountV2-8 3358 306254 ns/op

GermanGorelkin/go-patterns

Design patterns implemented in Golang. Contribute to GermanGorelkin/go-patterns development by creating an account on…

Cond

Условная переменная(condition variable) — примитив синхронизации, обеспечивающий блокирование одного или нескольких потоков до момента поступления сигнала от другого потока о выполнении некоторого условия или до истечения максимального промежутка времени ожидания.

Сигнал не несет никакой информации, кроме факта, что произошло какое-то событие. Очень часто мы хотим подождать один из этих сигналов, прежде чем продолжить выполнение. Один из наивных подходов состоит в использовании бесконечного цикла:

for conditionTrue() == false time.Sleep(1 * time.Millisecond)
>

Но это довольно неэффективно, и вам нужно выяснить, как долго спать: слишком долго, и вы искусственно снижаете производительность; слишком мало, и вы отнимаете слишком много процессорного времени. Было бы лучше, если бы у процесса был какой-то способ эффективно спать, пока ему не будет дан сигнал проснуться и проверить его состояние.

Но если у вас низкоуровневая библиотека, где необходим более производительный код, тогда можно использовать тип sync.Cond.

Пример

Предположим у нас есть некоторый общий ресурс в системе. Одна группа процессов может изменять его состояния, а другая группа должна реагировать на эти изменения.

type message struct cond *sync.Cond 
msg string
>

func main() msg := message cond: sync.NewCond(&sync.Mutex<>),
>

// 1
for i := 1; i <= 3; i++ go func(num int) for msg.cond.L.Lock()
msg.cond.Wait()
fmt.Printf("hello, i am worker%d. text:%s\n", num, msg.msg)
msg.cond.L.Unlock()
>
>(i)
>

// 2
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Enter text: ")
for scanner.Scan() msg.cond.L.Lock()
msg.msg = scanner.Text()
msg.cond.L.Unlock()

msg.cond.Broadcast()
>

>

Мы запустили 3 goroutine которые ждут сигнала. Обратите внимание, что вызов Wait не просто блокирует, он приостанавливает текущую процедуру, позволяя другим процедурам запускаться.

При входе Wait вызывается Unlock в Locker переменной Cond, а при выходе из Wait вызывается Lock в Locker переменной Cond. К этому нужно немного привыкнуть.

Во второй части мы читаем ввод из консоли и отправляем сигнал об изменении состояния.

Broadcast отправляет сигнал всем ожидающим goroutine. А метод Signal находит goroutine, которая ждала дольше всего и будет ее.

Mutex RWMutex отличия?

Здравствуйте, никак не могу понять в чем различия Mutex и RWMutex. Понимаю что RW — это read/write, но в чем реальные отличия от обычного Mutex? Заметила например, что если в цикле вызывать mutex.Lock() — загружается полностью ядро процессора, а если вызывать RWMutex.RLock() — такого нет.

  • Вопрос задан более трёх лет назад
  • 8565 просмотров

Комментировать

Решения вопроса 1

pav5000

RWMutex нужен, когда у нас есть объект, который нельзя параллельно писать, но можно параллельно читать. Например, стандартный тип map.
Перед записью в защищаемый мьютексом объект делается .Lock(), а вызовы .Lock() и .RLock() в других горутинах будут ждать, пока вы не отпустите мьютекс через .Unlock().
Перед чтением защищаемого объекта делается .RLock() и только вызовы .Lock() в других горутинах блокируются, вызовы .RLock() спокойно проходят. Когда отпускаете мьютекс через .RUnlock(), ждущие вызовы .Lock() по-очереди могут забирать мьютекс на себя.
Таких образом обеспечивается параллельное чтение объекта несколькими горутинами, что улучшает производительность.

Ответ написан более трёх лет назад

Нравится 10 4 комментария

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *