简体   繁体   English

如何同步两个交替线程

[英]How to synchronize two alternating threads

I need to start two threads, controlling which one starts first, then having them alternating their jobs.我需要启动两个线程,控制哪个线程先启动,然后让它们交替工作。

The following code works as expected with do_sleep = True , but it can fail with do_sleep = False .以下代码使用do_sleep = True可以正常工作,但使用do_sleep = False可能会失败。

How can I achieve the same result without using those ugly (and unreliable) sleeps?如果不使用那些丑陋(和不可靠)的睡眠,我怎样才能达到同样的效果?

The reason why it works with do_sleep = True is that:它与do_sleep = True一起使用的原因是:

  • Each worker thread gives time to the other thread to start before trying to acquire the lock and start the next job每个工作线程在尝试获取锁并开始下一个作业之前给另一个线程启动的时间
  • There is a pause between the start of the first and the second worker that allows the first one to acquire the lock before the second is ready在第一个和第二个工作人员的启动之间有一个暂停,允许第一个工作人员在第二个工作人员准备好之前获得锁

With do_sleep = False it can fail because:使用do_sleep = False它可能会失败,因为:

  • At the end of each job, each thread can try to acquire the lock for the next cycle before the other thread, executing two consecutive jobs instead of alternating在每个作业结束时,每个线程可以尝试在另一个线程之前获取下一个周期的锁,执行两个连续的作业而不是交替执行
  • The second thread could acquire the lock before the first one第二个线程可以在第一个线程之前获得锁

Here is the code:这是代码:

import threading
import time
import random

do_sleep = True

def workerA(lock):
    for i in range(5):
        lock.acquire()
        print('Working A - %s' % i)
        time.sleep(random.uniform(0.2, 1))
        lock.release()
        if do_sleep: time.sleep(0.1)

def workerB(lock):
    for i in range(5):
        if do_sleep: time.sleep(0.1)
        lock.acquire()
        print('Working B - %s' % i)
        time.sleep(random.uniform(0.2, 1))
        lock.release()
        if do_sleep: time.sleep(0.1)

lock = threading.Lock()

t1 = threading.Thread(target=workerA, args=(lock, ))
t2 = threading.Thread(target=workerB, args=(lock, ))

t1.start()
if do_sleep: time.sleep(0.1)
t2.start()

t1.join()
t2.join()

print('done')

EDIT Using a Queue as suggested by Mike doesn't help, because the first worker would finish the job without waiting for the second.编辑Mike 建议的使用Queue没有帮助,因为第一个工作人员会在不等待第二个工作人员的情况下完成工作。

This is the wrong output of a version after replacing the Lock with a Queue :这是用Queue替换Lock后版本的错误输出:

Working A - 0
Working A - 1
Working B - 0
Working A - 2
Working B - 1
Working A - 3
Working B - 2
Working A - 4
Working B - 3
Working B - 4
done

This is the wrong output, obtained with do_sleep = False :这是错误的输出,使用do_sleep = False获得:

Working A - 0
Working A - 1
Working A - 2
Working A - 3
Working A - 4
Working B - 0
Working B - 1
Working B - 2
Working B - 3
Working B - 4
done

This is the correct output, obtained with do_sleep = True :这是正确的输出,通过do_sleep = True获得:

Working A - 0
Working B - 0
Working A - 1
Working B - 1
Working A - 2
Working B - 2
Working A - 3
Working B - 3
Working A - 4
Working B - 4
done

Several ways to solve this.解决这个问题的几种方法。 One relatively easy one is to use the lock to control access to a separate shared variable: call this other variable owner , it can either be set to A or B. Thread A can only start a job when owner is set to A, and thread B can only start a job when owner is set to B. Then the pseudo-code is (assume thread A here):一个相对简单的方法是使用锁来控制对一个单独的共享变量的访问:调用这个另一个变量owner ,它可以设置为 A 或 B。只有当owner设置为 A 时,线程 A 才能启动作业,并且线程B 只能在owner设置为 B 的情况下启动作业。那么伪代码是(这里假设线程 A):

while True:
    while True:
        # Loop until I'm the owner
        lock.acquire()
        if owner == A:
            break
        lock.release()

    # Now I'm the owner. And I still hold the lock. Start job.
    <Grab next job (or start job or finish job, whatever is required to remove it from contention)>
    owner = B
    lock.release()
    <Finish job if not already done. Go get next one>

The B thread does the same thing only reversing the if owner and owner = statements. B 线程做同样的事情,只是反转if ownerowner =语句。 And obviously you can parameterize it so that both actually just run the same code.显然,您可以对其进行参数化,以便两者实际上只运行相同的代码。

EDIT编辑

Here is the working version, with the suggested logic inside an object:这是工作版本,在对象中包含建议的逻辑:

import threading
import time

def workerA(lock):
    for i in range(5):
        lock.acquire_for('A')
        print('Start A - %s' % i)
        time.sleep(0.5)
        print('End A - %s' % i)
        lock.release_to('B')

def workerB(lock):
    for i in range(5):
        lock.acquire_for('B')
        print('Start B - %s' % i)
        time.sleep(2)
        print('End B - %s' % i)
        lock.release_to('A')

class LockWithOwner:

    lock = threading.RLock()
    owner = 'A'

    def acquire_for(self, owner):
        n = 0
        while True:
            self.lock.acquire()
            if self.owner == owner:
                break
            n += 1
            self.lock.release()
            time.sleep(0.001)
        print('Waited for {} to be the owner {} times'.format(owner, n))

    def release_to(self, new_owner):
        self.owner = new_owner
        self.lock.release()

lock = LockWithOwner()
lock.owner = 'A'

t1 = threading.Thread(target=workerA, args=(lock, ))
t2 = threading.Thread(target=workerB, args=(lock, ))

t1.start()
t2.start()

t1.join()
t2.join()

print('done')

You can exclude the possibility of the wrong thread acquiring the lock, exclude relying on time.sleep(...) for correctness and shorten your code at the same time using Queue (two queues for both way communication):您可以排除错误线程获取锁的可能性,排除依赖time.sleep(...)的正确性并同时使用队列缩短代码(双向通信的两个队列):

import threading
import time
import random
from Queue import Queue

def work_hard(name, i):
  print('start %s - %s' % (name, i))
  time.sleep(random.uniform(0.2, 1))
  print('end %s - %s' % (name, i))

def worker(name, q_mine, q_his):
  for i in range(5):
    q_mine.get()
    work_hard(name, i)
    q_his.put(1)

qAB = Queue()
qBA = Queue()

t1 = threading.Thread(target=worker, args=('A', qAB, qBA))
t2 = threading.Thread(target=worker, args=('B', qBA, qAB))

t1.start()
qAB.put(1) # notice how you don't need time.sleep(...) even here
t2.start()

t1.join()
t2.join()

print('done')

It works as you specified.它按您指定的方式工作。 Alternatively you can use threading.Condition (a combination of acquire , release , wait and notify / notifyAll ), but that will be more subtle, especially in terms of which thread goes first.或者,您可以使用threading.Conditionacquirereleasewaitnotify / notifyAll的组合),但这会更加微妙,尤其是在哪个线程先行方面。

I have tried Gil Hamilton's answer and it doesn't work for me if I remove all the sleeps.我已经尝试过 Gil Hamilton 的答案,如果我删除所有的睡眠,它对我不起作用。 I think it's because my 'main' thread keeps getting the priority.我认为这是因为我的“主”线程不断获得优先权。 I found out that a better way to synchronize two or more threads is to use conditional object.我发现同步两个或多个线程的更好方法是使用条件对象。

Here is my working alternate lock object with conditional object inside这是我的工作备用锁定对象,里面有条件对象

class AltLock():
    def __init__(self, initial_thread):
        self.allow = initial_thread
        self.cond = threading.Condition()
        
    def acquire_for(self, thread):
        self.cond.acquire()
        while self.allow!=thread:
            print("\tLOCK:", thread, "waiting")
            self.cond.wait()
        print("\tLOCK:", thread, "acquired")
    
    def release_to(self, thread):
        print("\tLOCK: releasing to", thread)
        self.allow=thread
        self.cond.notifyAll()
        self.cond.release()

And this is an example usecase (the sleep statements in the thread are not required):这是一个示例用例(不需要线程中的睡眠语句):

class MyClass():
    def __init__(self):
        self.lock = AltLock("main")
        
    def _start(self):
        print("thread: Started, wait 2 second")
        time.sleep(2)
        print("---")
        self.lock.acquire_for("thread")
        time.sleep(2)
        print("---")
        print("thread: start lock acquired")
        self.lock.release_to("main")
        return 0
    
    def start(self):
        self.lock.acquire_for("main")
        self.thread = threading.Thread(target = self._start, )
        self.thread.start()
        print("main: releasing lock")
        self.lock.release_to("thread")
        self.lock.acquire_for("main")
        print("main: lock acquired")

myclass = MyClass()
myclass.start()
myclass.lock.release_to("main") # house keeping

And this is stdout:这是标准输出:

    LOCK: main acquired
thread: Started, wait 2 second
main: releasing lock
    LOCK: releasing to thread
    LOCK: main waiting         // 'main' thread try to reacquire the lock immediately but get blocked by wait.
---
    LOCK: thread acquired
---
thread: start lock acquired
    LOCK: releasing to main
    LOCK: main acquired
main: lock acquired
    LOCK: releasing to main

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM