← Назад к вопросам

Как сделать массив потокобезопасным?

1.0 Junior🔥 151 комментариев
#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Потокобезопасность массивов в Go

В Go массив (array) и срез (slice) по умолчанию не являются потокобезопасными, так как одновременная запись и чтение из нескольких горутин приводит к состоянию гонки (race condition) и непредсказуемому поведению. Для обеспечения потокобезопасности необходимо применять специальные механизмы синхронизации.

Основные подходы к реализации потокобезопасного массива

1. Использование мьютексов (Mutex)

Наиболее распространённый подход — защита доступа к массиву с помощью sync.RWMutex, который позволяет множественное чтение или эксклюзивную запись.

import "sync"

type ThreadSafeArray struct {
    mu    sync.RWMutex
    items []interface{}
}

func (tsa *ThreadSafeArray) Append(item interface{}) {
    tsa.mu.Lock()
    defer tsa.mu.Unlock()
    tsa.items = append(tsa.items, item)
}

func (tsa *ThreadSafeArray) Get(index int) interface{} {
    tsa.mu.RLock()
    defer tsa.mu.RUnlock()
    if index < 0 || index >= len(tsa.items) {
        return nil
    }
    return tsa.items[index]
}

func (tsa *ThreadSafeArray) Len() int {
    tsa.mu.RLock()
    defer tsa.mu.RUnlock()
    return len(tsa.items)
}

2. Каналы (Channels) для последовательного доступа

Go-идиома — использование каналов для организации последовательного доступа через одну горутину-менеджера (actor model).

type SafeArray struct {
    commands chan command
    items    []interface{}
}

type command struct {
    action string
    item   interface{}
    index  int
    result chan interface{}
}

func NewSafeArray() *SafeArray {
    sa := &SafeArray{
        commands: make(chan command),
        items:    make([]interface{}, 0),
    }
    go sa.run()
    return sa
}

func (sa *SafeArray) run() {
    for cmd := range sa.commands {
        switch cmd.action {
        case "append":
            sa.items = append(sa.items, cmd.item)
            cmd.result <- nil
        case "get":
            if cmd.index >= 0 && cmd.index < len(sa.items) {
                cmd.result <- sa.items[cmd.index]
            } else {
                cmd.result <- nil
            }
        case "len":
            cmd.result <- len(sa.items)
        }
    }
}

3. Атомарные операции для примитивных типов

Для числовых массивов можно использовать sync/atomic с указателями, но это сложно и ограничено.

import "sync/atomic"

type AtomicIntArray struct {
    ptr *[]int64 // Указатель на срез int64
}

func (a *AtomicIntArray) Update(index int, value int64) {
    for {
        oldPtr := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&a.ptr)))
        oldSlice := *(*[]int64)(oldPtr)
        if index < 0 || index >= len(oldSlice) {
            return
        }
        
        newSlice := make([]int64, len(oldSlice))
        copy(newSlice, oldSlice)
        newSlice[index] = value
        
        newPtr := unsafe.Pointer(&newSlice)
        if atomic.CompareAndSwapPointer(
            (*unsafe.Pointer)(unsafe.Pointer(&a.ptr)),
            oldPtr,
            newPtr,
        ) {
            return
        }
    }
}

Ключевые аспекты потокобезопасности

  • Чтение vs Запись: Используйте sync.RWMutex когда чтение происходит часто, а запись редко
  • Иммутабельность: Создание нового массива при изменении — безопасно, но требует копирования
  • Готовые решения: Пакет sync предоставляет sync.Map для concurrent map, но для массивов нет встроенного решения

Рекомендации по выбору подхода

  1. Мьютексы — универсальное решение для большинства случаев
  2. Каналы — идиоматично для Go, подходит когда логика операций сложная
  3. Атомарные операции — только для оптимизации критических участков
  4. Копирование при записи — если изменения редкие, а чтение частое

Пример комплексного решения

type ConcurrentArray struct {
    mu       sync.RWMutex
    data     []interface{}
    capacity int
}

func NewConcurrentArray(capacity int) *ConcurrentArray {
    return &ConcurrentArray{
        data:     make([]interface{}, 0, capacity),
        capacity: capacity,
    }
}

func (ca *ConcurrentArray) Add(item interface{}) bool {
    ca.mu.Lock()
    defer ca.mu.Unlock()
    
    if len(ca.data) >= ca.capacity {
        return false
    }
    
    ca.data = append(ca.data, item)
    return true
}

func (ca *ConcurrentArray) Range(fn func(index int, value interface{}) bool) {
    ca.mu.RLock()
    defer ca.mu.RUnlock()
    
    for i, v := range ca.data {
        if !fn(i, v) {
            break
        }
    }
}

Важно помнить, что потокобезопасность — это не только защита операций доступа, но и обеспечение консистентности данных при сложных составных операциях. Всегда используйте go run -race для обнаружения состояний гонки при тестировании конкурентного кода.

Как сделать массив потокобезопасным? | PrepBro