Блокировка GO и совместное использование атомарных операций

задняя часть
Блокировка GO и совместное использование атомарных операций

Это первый раз, когда я участвую в Gengwen Challenge.12День, подробности о событии уточняйте:Обновить вызов

Блокировка GO и совместное использование атомарных операций

В прошлый раз, когда мы говорили о сопрограммах, давайте повторим:

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

Если вы заинтересованы в использовании сопрограмм, вы можете прочитать эту статью для краткого введенияСовместное использование параллельных программ с помощью GO с первого взгляда

Сегодня поговорим о блокировке в GO

Что такое замок?

Блокировки — это механизм, используемый для устранения изоляции.

Сопрограмма (поток) блокируется первой при доступе к ресурсу, чтобы предотвратить доступ других сопрограмм.После того, как доступ будет завершен и разблокирован, другие сопрограммы будут заблокированы для доступа.

В нашей жизни мы не должны быть незнакомы, замок такой

Первоначальное значение — поместить его на посуду, которую можно открывать и закрывать и открывать с помощью ключа или пароля Расширенное значение — запирать и закрывать замком.

замки используемые в жизни

Блокировка в основном предназначена для предотвращения проникновения посторонних и предотвращения кражи ваших вещей.

Блокировки в языках программирования

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

Для чего используются замки?

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

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

Затем необходимо знать накладные расходы различных блокировок и сценариев применения.

Что такое блокировки в GO?

  • Мьютекс
  • Блокировка чтения-записи

У нас будет несколько в кодированиигорутинаОдновременно работая с ресурсом (критическим разделом), эта ситуация вызовет состояние гонки (гонка данных)

Приведите пример из жизни

Самый очевидный пример из жизни — все спешат в туалет, а ресурсы ограничены, поэтому пользоваться им можно только поодиночке.

Приведи пример в кодировании

package main

import (
	"fmt"
	"sync"
)

// 全局变量
var num int64
var wg sync.WaitGroup

func add() {
	for i := 0; i < 10000000; i++ {
		num = num + 1
	}
	// 协程退出, 记录 -1
	wg.Done()
}
func main() {
	// 启动2个协程,记录 2
	wg.Add(2)

	go add()
	go add()

	// 等待子协程退出
	wg.Wait()
	fmt.Println(num)
}

Согласно приведенному выше коду, наш вывод должен быть20000000, каждая сопрограмма вычисляет10000000раз, но фактический результат

10378923

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

Когда две сопрограммы goroutine получают доступ к переменной num и изменяют ее, одновременно будет 2 сопрограммы для num+1, и, наконец, num добавляется всего 1 вместо 2.

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

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

Давайте сначала посмотрим на мьютекс

Мьютекс

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

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

Каждому объекту соответствует互斥锁Тег, который используется для обеспечения того, чтобы только одна сопрограмма могла получить доступ к объекту в любой момент времени.

Сценарии применения

Записей больше, чем чтений

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

Давайте используем мьютекс для решения вышеуказанной проблемы.

Мьютекс - решить проблему

Mutex — это широко используемый метод управления доступом к общим ресурсам, который гарантирует, что только одна сопрограмма goroutine может получить доступ к общим ресурсам в одно и то же время.

Go использует следующие 1 очко знаний для решения

  • пакет синхронизацииизТип мьютексареализовать взаимное исключение
package main

import (
   "fmt"
   "sync"
)

// 全局变量
var num int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
   for i := 0; i < 10000000; i++ {
      // 访问资源前  加锁
      lock.Lock()
      num = num + 1
      // 访问资源后  解锁
      lock.Unlock()
   }
   // 协程退出, 记录 -1
   wg.Done()
}
func main() {
   // 启动2个协程,记录 2
   wg.Add(2)

   go add()
   go add()

   // 等待子协程退出
   wg.Wait()
   fmt.Println(num)
}

Выполняя приведенный выше код, мы видим, что вывод соответствует тому, что мы ожидали.

20000000

Использование блокировки мьютекса может гарантировать, что только одна сопрограмма горутины входит в критическую секцию одновременно, а другие горутины ждут блокировки

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

Как узнать, какая сопрограмма пробудится первой?

Однако, когда несколько горутин одновременно ожидают блокировки, как узнать, какая из них активируется первой?

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

Блокировка чтения-записи

Зачем нужен мьютекс, но еще и блокировка чтения-записи?

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

Блокировки взаимного исключения полностью исключают друг друга, независимо от того, читает ли сопрограмма ресурсы критической секции или записывает ресурсы критической секции, она должна получить блокировку, иначе она не сможет работать (это ограничение слишком мертво, не так ли?)

Но в нашем реальном сценарии приложенияБольше читайте и меньше пишите

Если мы читаем ресурс одновременно и не вносим никаких изменений в ресурс, если нам также нужно заблокировать данные для чтения данных, разве это не нужно?

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

Типы блокировок чтения-записи

  • блокировка чтения
  • блокировка записи

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

Но если он хочет получить блокировку записи, он должен ждать

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

Давайте сначала напишем демонстрацию блокировки чтения-записи.

Go использует следующие 1 очко знаний для решения

  • пакет синхронизацииизТип RWMutexдля реализации блокировок чтения-записи
package main

import (
   "fmt"
   "sync"
   "time"
)

var (
   num    int64
   wg     sync.WaitGroup
   //lock   sync.Mutex
   rwlock sync.RWMutex
)

func write() {
   // 加互斥锁
   // lock.Lock()

   // 加写锁
   rwlock.Lock()

   num = num + 1
   // 模拟真实写数据消耗的时间
   time.Sleep(10 * time.Millisecond)

   // 解写锁
   rwlock.Unlock()

   // 解互斥锁
   // lock.Unlock()

   // 退出协程前 记录 -1
   wg.Done()
}

func read() {
   // 加互斥锁
   // lock.Lock()

   // 加读锁
   rwlock.RLock()

   // 模拟真实读取数据消耗的时间
   time.Sleep(time.Millisecond)

   // 解读锁
   rwlock.RUnlock()

   // 解互斥锁
   // lock.Unlock()

   // 退出协程前 记录 -1
   wg.Done()
}

func main() {
   // 用于计算时间 消耗
   start := time.Now()

   // 开5个协程用作 写
   for i := 0; i < 5; i++ {
      wg.Add(1)
      go write()
   }

   // 开500 个协程,用作读
   for i := 0; i < 1000; i++ {
      wg.Add(1)
      go read()
   }

   // 等待子协程退出
   wg.Wait()
   end := time.Now()

   // 打印程序消耗的时间
   fmt.Println(end.Sub(start))
}

Открываем 5 сопрограмм на запись, 1000 сопрограмм на чтение и используем блокировки чтения-записи для блокировки, результат требует времени54.4871msследующим образом

54.4871ms

Если мы изменим приведенный выше код, чтобы добавить мьютекс, результатом после запуска будет1.7750029sследующим образом

1.7750029s

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

Подытожим логику этого небольшого кусочка:

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

Вышеупомянутая спиновая блокировка, давайте кратко объясним,Что такое спин-блокировка

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

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

Разница между спин-блокировками и блокировками мьютексов

  • Мьютекс

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

  • блокировка спина

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

Как выбрать замок?

Для С/С++

  • Если потребление бизнес-операций после блокировки больше, чем потребление переключения контекста после блокировки мьютекса, то выбирается мьютекс.
  • Если потребление бизнес-операций после блокировки меньше, чем потребление переключения контекста после блокировки мьютекса, выберите спин-блокировку.

Для ГО

  • Если частота записи намного больше частоты чтения, то выбираем мьютекс
  • Если частота чтения намного больше частоты записи, то выбираем блокировку чтения-записи

Все мы студенты с высокими требованиями к себе.Так есть ли что-нибудь лучше, чем замок??

Конечно есть, давайте посмотрим на атомарные операции

Что такое атомарная операция

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

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

Характеристики атомарных операций:

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

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

противосновные типы данныхМы также можем использовать атомарные операции, чтобы гарантироватьПараллельная безопасность

Поскольку атомарная операция — это метод, предоставляемый языком Go, онпользовательский режимможно сделать, поэтому производительность лучше, чем операция блокировки

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

Расширяем рассмотренный выше случай

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

var num int64
var l sync.Mutex
var wg sync.WaitGroup

// 普通版加函数
func add() {
	num = num + 1
	wg.Done()
}

// 互斥锁版加函数
func mutexAdd() {
	l.Lock()
	num = num + 1
	l.Unlock()
	wg.Done()
}

// 原子操作版加函数
func atomicAdd() {
	atomic.AddInt64(&num, 1)
	wg.Done()
}

func main() {
	// 目的是 记录程序消耗时间
	start := time.Now()
	for i := 0; i < 20000; i++ {

		wg.Add(1)

		// go add()       // 无锁的  add函数 不是并发安全的
		// go mutexAdd()  // 互斥锁的 add函数 是并发安全的,因为拿不到互斥锁会阻塞,所以加锁性能开销大

		go atomicAdd()    // 原子操作的 add函数 是并发安全,性能优于加锁的
	}

	// 等待子协程 退出
	wg.Wait()

	end := time.Now()
	fmt.Println(num)
	// 打印程序消耗时间
	fmt.Println(end.Sub(start))
}

Мы используем приведенный выше демонстрационный код для имитации 3 случаев,Время программы и результаты расчетаВ сравнении

  • не заблокирован

Функция добавления без блокировки не является безопасной для параллелизма.

19495
11.9474ms
  • добавить мьютекс

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

20000
14.9586ms
  • Используйте атомарные операции

Функция добавления атомарной операции безопасна для параллелизма, и ее производительность лучше, чем у блокировки.

20000
9.9726ms

Суммировать

  • Поделился, что такое замок и для чего он используется
  • Общие блокировки мьютексов, блокировки чтения-записи, их отличия и сценарии применения
  • общие атомарные операции
  • Если вам интересно, вы можете взглянуть на реализацию блокировок, которые также используют атомарные операции.

Добро пожаловать лайк, подписка, избранное

Друзья, ваша поддержка и поощрение мотивируют меня продолжать делиться и улучшать качество.

Хорошо, вот и на этот раз,В следующий раз переключитесь на канал GO и обмен пакетами синхронизации

Технологии открыты, и наш менталитет должен быть открытым. Примите перемены, живите на солнце и двигайтесь вперед.

ямаленький дьяволенок Нежа, добро пожаловать, лайкайте, подписывайтесь и добавляйте в избранное, увидимся в следующий раз~