Как выполнять потоки чередующимся способом в Python

#python #multithreading #locking

#python #многопоточность #блокировка

Вопрос:

Я почти новичок в потоковой обработке Python. Это мой код, смоделированный на примере кода, найденного на GeeksForFeeks, который объясняет поведение потоков с использованием блокировки. Но результат — для меня — противоречит здравому смыслу.

 import threading 

# global variable x 
x = 0

# creating a lock 
lock = threading.Lock() 

def increment(): 
    global x 
    x  = 1
    print("thread1:", x)
    
def decrement(): 
    global x 
    x -= 1
    print("thread2:", x)
  
def plus(): 
    global lock
    for _ in range(100000): 
        lock.acquire() 
        increment()
        lock.release()

def minus(): 
    global lock
    for _ in range(100000): 
        lock.acquire() 
        decrement()
        lock.release()
  
def main_task(): 
    global x 
    # setting global variable x as 0 
    x = 0

    # creating threads 
    t1 = threading.Thread(target=plus) 
    t2 = threading.Thread(target=minus)
  
    # start threads 
    t1.start() 
    t2.start() 
  
    # wait until threads finish their job 
    t1.join() 
    t2.join() 
  
if __name__ == "__main__": 
        main_task()
  

Я ожидал бы такого результата:

 thread1: 1
thread2: 0
thread1: 1
thread2: 0
thread1: 1
thread2: 0
thread1: 1
...
  

но вместо этого я получаю

 thread1: 1
thread1: 2
thread1: 3
thread1: 4
thread1: 5
thread1: 6
thread1: 7
...
thread1: 151
thread2: 150
thread2: 149
thread2: 148
thread2: 147
thread2: 146
thread2: 145
thread2: 144
thread2: 143
thread2: 142
thread2: 141
...
  

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

Чего мне не хватает?

Ответ №1:

Это происходит по крайней мере по двум причинам:

  1. GIL Это единственная блокировка самого интерпретатора, которая добавляет правило, согласно которому для выполнения любого байт-кода Python требуется получение блокировки интерпретатора.

  2. Более общий: Lock объект не совсем подходит для вас. Да, он синхронизирует доступ к переменной, но после lock освобождения и до момента ее получения другой код может выполнять несколько таких итераций.

Для чередующейся синхронизации лучше использовать Condition объект, который позволяет коду ждать, пока другой код уведомит (по сути, разбудит его), что первый может выполняться:

 # creating a lock
lock = threading.Condition()

# ...

def plus():
    global lock
    with lock:
        for _ in range(100000):
            increment()
            lock.notify()
            lock.wait()
        lock.notify()  # notify to finish companion thread


def minus():
    global lock
    with lock:
        for _ in range(100000):
            decrement()
            lock.notify()
            lock.wait()
        lock.notify()  # notify to finish companion thread
  
 ...
thread1: 1
thread2: 0
thread1: 1
thread2: 0
thread1: 1
thread2: 0
  

Комментарии:

1. Спасибо. Почему Condition объект не позволяет запускать другие вещи один раз после освобождения ресурса, в то время Lock как объект разрешает, пожалуйста?

2. Потому что в случае Condition мы ждем (метод wait() ), чтобы другой поток заработал, и уведомляем (метод notify() ) нас об этом. Это обеспечивает синхронизацию. В случае блокировки сразу после ее освобождения мы пытаемся получить ее снова.

3. В случае, если у нас есть несколько потоков, созданных из потока, обрабатывающего данные очереди, вам все еще нужен Condition объект, пожалуйста?