python: fifo mutex / Lock [дубликат]

Когда ваши пути include разные

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

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

Возможно ли получить ошибку компоновщика, даже если объявление и определение, похоже, совпадают? Да! Они могут выглядеть одинаково в исходном коде, но это действительно зависит от того, что видит компилятор. По сути, вы можете столкнуться с такой ситуацией:

// header1.h
typedef int Number;
void foo(Number);

// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically

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

Вы можете спросить, как это получается в такой ситуации? Включите пути, конечно! Если при компиляции разделяемой библиотеки путь include приводит к header1.h, и вы в конечном итоге используете header2.h в своей собственной программе, вы оставите царапины на своем заголовке, задаваясь вопросом, что произошло (каламбур).

Пример того, как это может произойти в реальном мире, объясняется ниже.

Дальнейшая разработка с примером

У меня есть два проекта: graphics.lib и main.exe. Оба проекта зависят от common_math.h. Предположим, что библиотека экспортирует следующую функцию:

// graphics.lib    
#include "common_math.h" 

void draw(vec3 p) { ... } // vec3 comes from common_math.h

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

// main.exe
#include "other/common_math.h"
#include "graphics.h"

int main() {
    draw(...);
}

Boom! Вы получаете ошибку компоновщика, и вы понятия не имеете, почему она терпит неудачу. Причина в том, что общая библиотека использует разные версии одного и того же include common_math.h (я сделал это очевидным здесь в этом примере, включив другой путь, но это может быть не всегда так очевидно. Возможно, путь include отличается в настройки компилятора).

Обратите внимание, что в этом примере компоновщик сказал бы вам, что не смог найти draw(), когда на самом деле вы знаете, что он явно экспортируется библиотекой. Вы могли часами царапать себе голову, думая, что пошло не так. Дело в том, что компоновщик видит другую подпись, потому что типы параметров немного отличаются. В этом примере vec3 является другим типом в обоих проектах в отношении компилятора. Это может произойти из-за того, что они происходят из двух немного разных файлов include (возможно, включенные файлы поступают из двух разных версий библиотеки).

Отладка компоновщика

DUMPBIN - ваш друг, если вы используете Visual Studio. Я уверен, что другие компиляторы имеют другие подобные инструменты.

Процесс выглядит следующим образом:

  1. Обратите внимание на странное искаженное имя, указанное в ошибке компоновщика. (например, draw @ graphics @ XYZ).
  2. Выгрузите экспортированные символы из библиотеки в текстовый файл.
  3. Найдите экспортированный символ, представляющий интерес, и обратите внимание, что искаженное имя
  4. Обратите внимание на то, почему искаженные имена оказались разными. Вы могли бы видеть, что типы параметров различны, хотя они выглядят одинаково в исходном коде.
  5. Причина, почему они разные. В приведенном выше примере они различаются из-за разных файлов include.

[1] По проекту я имею в виду набор исходных файлов, которые связаны друг с другом для создания либо библиотеки, либо исполняемого файла .

РЕДАКТИРОВАТЬ 1: Переписать первый раздел, который будет легче понять. Пожалуйста, прокомментируйте ниже, чтобы сообщить мне, нужно ли что-то еще исправлять. Спасибо!

12
задан Alex W 30 October 2013 в 19:39
поделиться

3 ответа

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

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

Тем не менее, я покажу способ реализовать «блокировку FIFO». Это не сложно и нелегко - где-то посередине - и вы не должны его использовать ;-) Боюсь, что только вы можете ответить на ваш вопрос: «Сколько я буду проигрывать во время обработки?» вопрос - мы понятия не имеем, насколько сильно вы используете блокировки, или сколько конфликтов вызывает ваше приложение.

import threading, collections

class QLock:
    def __init__(self):
        self.lock = threading.Lock()
        self.waiters = collections.deque()
        self.count = 0

    def acquire(self):
        self.lock.acquire()
        if self.count:
            new_lock = threading.Lock()
            new_lock.acquire()
            self.waiters.append(new_lock)
            self.lock.release()
            new_lock.acquire()
            self.lock.acquire()
        self.count += 1
        self.lock.release()

    def release(self):
        with self.lock:
            if not self.count:
                raise ValueError("lock not acquired")
            self.count -= 1
            if self.waiters:
                self.waiters.popleft().release()

    def locked(self):
        return self.count > 0

Вот небольшой тестовый драйвер, который может быть изменен очевидным способом использования либо этого QLock, либо threading.Lock:

def work(name):
    qlock.acquire()
    acqorder.append(name)

from time import sleep
if 0:
    qlock = threading.Lock()
else:
    qlock = QLock()
qlock.acquire()
acqorder = []
ts = []
for name in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
    t = threading.Thread(target=work, args=(name,))
    t.start()
    ts.append(t)
    sleep(0.1) # probably enough time for .acquire() to run
for t in ts:
    while not qlock.locked():
        sleep(0)  # yield time slice
    qlock.release()
for t in ts:
    t.join()
assert qlock.locked()
qlock.release()
assert not qlock.locked()
print "".join(acqorder)

На моем боксе сейчас 3 выхода с использованием threading.Lock создали этот вывод:

BACDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSUVWXYZT
ABCEDFGHIJKLMNOPQRSTUVWXYZ

Так что это, конечно, не случайно, но и не является полностью прогнозируемым. Запустив его с помощью QLock, выход всегда должен быть:

ABCDEFGHIJKLMNOPQRSTUVWXYZ
5
ответ дан Tim Peters 25 August 2018 в 14:11
поделиться

Да, вы можете создать очередь FIFO , используя список идентификаторов потоков:

FIFO = [5,79,3,2,78,1,9...]

Вы попытались бы получить блокировку, а если не сможете, тогда нажмите идентификатор попытки воспроизведения (FIFO.insert(0,threadID)) на переднюю часть очереди и каждый раз, когда вы отпустите блокировку, убедитесь, что если поток хочет получить блокировку, он должен иметь идентификатор потока в конце очереди (threadID == FIFO[-1]). Если поток имеет идентификатор потока в конце очереди, то пусть он получает блокировку, а затем выталкивает ее (FIFO.pop()). Повторите при необходимости.

0
ответ дан Alex W 25 August 2018 в 14:11
поделиться

Я наткнулся на этот пост, потому что у меня было подобное требование. Или, по крайней мере, я так и думал.

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

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

Чтобы добраться до этого, я немного поработал, чтобы лучше понять, как блокировки работали под Linux , Я начал с того, что посмотрел на исходный код glibc и спецификации pthreads (Posix Threads), потому что я работал на C ++ в Linux. Я не знаю, использует ли Python pthreads под капотом, но я предполагаю, что это возможно.

Я не нашел какой-либо спецификации в нескольких ссылках на pthreads, относящихся к порядку разблокируется.

Я нашел: блокировки в pthreads в Linux реализованы с использованием функции ядра, называемой futex .

http: // man7 .org / linux / man-pages / man2 / futex.2.html

http://man7.org/linux/man-pages/man7/futex.7. html

Ссылка на ссылки на первую из этих страниц приводит к этому PDF:

https://www.kernel.org/doc /ols/2002/ols2002-pages-479-495.pdf

Это немного объясняет стратегии разблокировки и о том, как futexes работают и реализуются в ядре Linux, и много больше.

И там я нашел то, что хотел. В нем объясняется, что futexes реализованы в ядре таким образом, что разблокировки в основном выполняются в порядке FIFO (для повышения справедливости). Тем не менее, это не гарантируется, и возможно, что один поток может немного перепрыгнуть линию. Они позволяют это не слишком усложнять код и позволяют достичь хорошей производительности, не теряя его из-за крайних мер по обеспечению соблюдения порядка FIFO.

Итак, в основном, у вас есть:

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

Реализация Linux в библиотеке pthreads основана на функции / методе futex, которая реализует мьютексы, и в основном пытается сделать разблокировку мьютексов в стиле FIFO, но не гарантирует, что это будет сделано в этом порядке.

1
ответ дан felipou 25 August 2018 в 14:11
поделиться
Другие вопросы по тегам:

Похожие вопросы: