Почему pthread_mutex_lock () зависает, если вызвана pthread_mutex_unlock ()? - PullRequest
0 голосов
/ 30 января 2020

Я использую библиотеку Swift для изменчивых наблюдаемых, которые используют мьютекс для управления чтением / записью наблюдаемого значения, например так:

import Foundation

class MutObservable<T> {

    lazy var m: pthread_mutex_t = {
        var m = pthread_mutex_t()
        pthread_mutex_init(&m, nil)
        return m
    }()

    var value: T {
        get {
            return _value
        }
        set {
            pthread_mutex_lock(&m)
            _value = newValue
            pthread_mutex_unlock(&m)
        }
    }

}

Я зашел в тупик с этим кодом. Вставляя точки останова и проходя через них, я наблюдаю следующее:

  1. pthread_mutex_lock(&m)
  2. pthread_mutex_unlock(&m)
  3. pthread_mutex_lock(&m)
  4. pthread_mutex_unlock(&m)
  5. pthread_mutex_lock(&m) deadlock

Это происходит каждый раз, когда выполняется эта кодовая последовательность, по крайней мере, 30+ раз, когда я ее пробовал. Две последовательности блокировки / разблокировки, затем тупик.

Исходя из моего опыта работы с мьютексами на других языках, (Go) я не ожидал бы, что равные вызовы блокировки / разблокировки вызовут взаимоблокировку, но я понимаю, что это является прямым C мьютексом, поэтому здесь могут быть правила, с которыми я не знаком. Также в миксе могут быть факторы взаимодействия Swift / C.

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

Есть ли причина, по которой pthread_mutex_lock() вызовет взаимоблокировку, если рассматриваемый мьютекс был ранее заблокирован, а затем разблокирован?

Ответы [ 2 ]

2 голосов
/ 30 января 2020

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

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

class MutObservable<T> {
    private var m = pthread_mutex_t()

    var _value:T

    var value: T {
        get {
            return _value
        }
        set {
            pthread_mutex_lock(&m)
            setCount += 1
            _value = newValue
            pthread_mutex_unlock(&m)
        }
    }

    init(v:T) {
        _value = v
        pthread_mutex_init(&m, nil)
    }
}
1 голос
/ 31 января 2020

FWIW, в WWD C 2016 видео Параллельное программирование с GCD они указывают, что, хотя вы могли исторически использовать pthread_mutex_t, они теперь отговаривают нас от его использования. Они показывают, как вы можете использовать традиционные блокировки (рекомендует os_unfair_lock в качестве более производительного решения, но не испытывающие проблем с питанием со старой устаревшей спин-блокировкой), но если вы хотите сделать это, они советуют вам извлечь базовый класс Objective- C с блокировками на основе структуры в виде ivars. Но они предупреждают нас, что мы просто не можем просто безопасно использовать старые блокировки на основе структуры C непосредственно из Swift.

Но в блокировках pthread_mutex_t больше нет необходимости. Лично я нахожу, что простое NSLock является довольно производительным решением, поэтому я лично использую расширение (на основе паттерна Apple, использованного в их примере «расширенных операций»):

extension NSLocking {
    func synchronized<T>(_ closure: () throws -> T) rethrows -> T {
        lock()
        defer { unlock() }
        return try closure()
    }
}

Тогда я могу определить заблокируйте и используйте этот метод:

class Synchronized<T> {
    private var _value: T

    private var lock = NSLock()

    var value: T {
        get { lock.synchronized { _value } }
        set { lock.synchronized { _value = newValue } }
    }

    init(value: T) {
        _value = value
    }
}

Это видео (посвященное GCD) показывает, как вы можете сделать это с очередями GCD. Последовательная очередь - самое простое решение, но вы можете также использовать шаблон чтения-записи в параллельной очереди, при этом читатель использует sync, а писатель использует async с барьером:

class Synchronized<T> {
    private var _value: T

    private var queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".synchronizer", attributes: .concurrent)

    var value: T {
        get { queue.sync { _value } }
        set { queue.async(flags: .barrier) { self._value = newValue } }
    }

    init(value: T) {
        _value = value
    }
}

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


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

Обязательно синхронизируйте все взаимодействия с базовым объектом.


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

let counter = Synchronized(value: 0)

DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
    counter.value += 1
}

Это почти наверняка не вернет 1 000 000. Это потому, что синхронизация на неправильном уровне. См. Swift Совет: Atomi c Переменные для обсуждения того, что не так.

Вы можете исправить это, добавив метод synchronized, чтобы обернуть все, что требует синхронизации (в данном случае, извлечение значения, его приращение и сохранение результата):

class Synchronized<T> {
    private var _value: T

    private var lock = NSLock()

    var value: T {
        get { lock.synchronized { _value } }
        set { lock.synchronized { _value = newValue } }
    }

    func synchronized(block: (inout T) throws -> Void) rethrows {
        try lock.synchronized { try block(&_value) }
    }

    init(value: T) {
        _value = value
    }
}

И затем:

let counter = Synchronized(value: 0)

DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
    counter.synchronized { $0 += 1 }
}

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

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