Обеспечение безопасности структуры с использованием go каналов - PullRequest
3 голосов
/ 14 января 2020

Предположим, у меня есть следующая структура:

package manager

type Manager struct {
    strings []string
}

func (m *Manager) AddString(s string) {
    m.strings = append(m.strings, s)
}

func (m *Manager) RemoveString(s string) {
    for i, str := range m.strings {
        if str == s {
            m.strings = append(m.strings[:i], m.strings[i+1:]...)
        }
    }
}

Этот шаблон не является потокобезопасным, поэтому следующий тест не пройден из-за некоторого состояния гонки (индекс массива вне границ):

func TestManagerConcurrently(t *testing.T) {
    m := &manager.Manager{}
    wg := sync.WaitGroup{}
    for i:=0; i<100; i++ {
        wg.Add(1)
        go func () {
            m.AddString("a")
            m.AddString("b")
            m.AddString("c")
            m.RemoveString("b")
            wg.Done()
        } ()
    }
    wg.Wait()

    fmt.Println(m)
}

Я новичок в Go, и из-за того, что гуглюсь вокруг, я полагаю, что должен использовать каналы (?) Таким образом, один из способов сделать это параллельным будет выглядеть так:

type ManagerA struct {
    Manager
    addStringChan chan string
    removeStringChan chan string
}

func NewManagerA() *ManagerA {
    ma := &ManagerA{
        addStringChan: make(chan string),
        removeStringChan: make(chan string),
    }
    go func () {
        for {
            select {
            case msg := <-ma.addStringChan:
                ma.AddString(msg)
            case msg := <-ma.removeStringChan:
                ma.RemoveString(msg)
            }
        }
    }()
    return ma
}

func (m* ManagerA) AddStringA(s string) {
    m.addStringChan <- s
}
func (m* ManagerA) RemoveStringA(s string) {
    m.removeStringChan <- s
}

Я хотел бы представить API, аналогичный непараллельному примеру, следовательно, AddStringA, RemoveStringA.

Это кажется работать как ожидалось одновременно (хотя я предполагаю, что внутренний goroutine должен также выйти в некоторый момент). Моя проблема с этим заключается в том, что существует множество дополнительных шаблонов:

  • необходимо определить и инициализировать каналы
  • определить внутреннюю последовательность l oop с помощью select
  • сопоставить функции с вызовами канала

Мне кажется, это немного. Есть ли способ упростить это (рефакторинг / синтаксис / библиотека)?

Я думаю, что лучший способ реализовать это - использовать вместо этого Mutex? Но можно ли все еще упростить этот типовой шаблон?

Ответы [ 2 ]

8 голосов
/ 14 января 2020

Использование мьютекса было бы совершенно идиоматически c примерно так:

type Manager struct {
    mu      sync.Mutex
    strings []string
}

func (m *Manager) AddString(s string) {
    m.mu.Lock()
    m.strings = append(m.strings, s)
    m.mu.Unlock()
}

func (m *Manager) RemoveString(s string) {
    m.mu.Lock()
    for i, str := range m.strings {
        if str == s {
            m.strings = append(m.strings[:i], m.strings[i+1:]...)
        }
    }
    m.mu.Unlock()
}

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

5 голосов
/ 14 января 2020

Если вам просто нужно сделать доступ к struct thread-safe, используйте mutex:

type Manager struct {
   sync.Mutex
   data []string
}

func (m *Manager) AddString(s string) {
    m.Lock()
    m.strings = append(m.strings, s)
    m.Unlock()
}
...