Python:使用lambda作为线程目标会导致奇怪的行为

时间:2017-02-22 01:12:44

标签: python multithreading lambda

我在Python中遇到了一个奇怪的行为。当我使用lambda作为线程的目标时,行为是不一致的。

第一个例子是这样的:

import time,threading

locker= threading.RLock()

def func(obj):
  while obj['count']>0:
    with locker: print 'thread',obj,id(obj)
    obj['count']-= 1
    time.sleep(0.1)
  with locker: print 'finished',obj,id(obj)

def make_thread1():
  threads= []
  objs= {}
  for i in range(2):
    objs[i]= {}
    objs[i]['id']= i
    objs[i]['count']= (i+2)*2
    t= threading.Thread(name='func'+str(i), target=lambda: func(objs[i]))
    t.start()
    threads.append(t)
  return threads,objs

if __name__=='__main__':
  threads,objs= make_thread1()
  for t in threads:
    t.join()

有两种结果模式。一个是

thread {'count': 4, 'id': 0} 139911658041704
thread {'count': 6, 'id': 1} 139911658041984
thread {'count': 3, 'id': 0} 139911658041704
thread {'count': 5, 'id': 1} 139911658041984
thread {'count': 4, 'id': 1} 139911658041984
thread {'count': 2, 'id': 0} 139911658041704
thread {'count': 3, 'id': 1} 139911658041984
thread {'count': 1, 'id': 0} 139911658041704
thread {'count': 2, 'id': 1} 139911658041984
finished {'count': 0, 'id': 0} 139911658041704
thread {'count': 1, 'id': 1} 139911658041984
finished {'count': 0, 'id': 1} 139911658041984

这是我预期的结果。但是,当多次运行此代码时,有时会产生如下结果:

thread {'count': 6, 'id': 1} 140389870428800
thread {'count': 5, 'id': 1} 140389870428800
thread {'count': 4, 'id': 1} 140389870428800
thread {'count': 3, 'id': 1} 140389870428800
thread {'count': 2, 'id': 1} 140389870428800
thread {'count': 1, 'id': 1} 140389870428800
finished {'count': 0, 'id': 1} 140389870428800
finished {'count': 0, 'id': 1} 140389870428800

创建线程时,lambda:func(objs[0])lambda:func(objs[1])分别被定义为目标函数,但实际上两个目标函数都是lambda:func(objs[1])(但实例不同)。

我无法理解为什么会这样。

嗯,有一种可能性是我在制作lambda函数时使用局部变量i。但是在执行t.start()时应该对它进行评估......?那为什么有两种结果模式?

为了进行更多调查,我修改了没有lambda的代码:

class TObj:
  def __init__(self):
    self.objs= None
  def f(self):
    func(self.objs)

def make_thread2():
  threads= []
  classes= {}
  for i in range(2):
    classes[i]= TObj()
    classes[i].objs= {}
    classes[i].objs['id']= i
    classes[i].objs['count']= (i+2)*2
    t= threading.Thread(name='func'+str(i), target=classes[i].f)
    t.start()
    threads.append(t)
  return threads,classes

if __name__=='__main__':
  threads,classes= make_thread2()
  for t in threads:
    t.join()

这段代码完美无缺:

thread {'count': 4, 'id': 0} 140522771444352
thread {'count': 6, 'id': 1} 140522771445472
thread {'count': 3, 'id': 0} 140522771444352
thread {'count': 5, 'id': 1} 140522771445472
thread {'count': 2, 'id': 0} 140522771444352
thread {'count': 4, 'id': 1} 140522771445472
thread {'count': 1, 'id': 0} 140522771444352
thread {'count': 3, 'id': 1} 140522771445472
finished {'count': 0, 'id': 0} 140522771444352
thread {'count': 2, 'id': 1} 140522771445472
thread {'count': 1, 'id': 1} 140522771445472
finished {'count': 0, 'id': 1} 140522771445472

我想了解第一个代码不一致的原因。

我也想知道如何安全地在循环中创建lambda函数。如果上述问题是由lambda函数中的循环变量i引起的,我们应该避免在循环中生成lambda函数。这会很不方便;我可以编码像后者,但它更长。有什么好主意吗?

1 个答案:

答案 0 :(得分:0)

Deferred evaluation with lambda in Python

中描述了将lambda与循环变量一起使用的想法

我们可以使用默认参数值在lambda中使用循环(局部)变量。在我的例子中,创建一个lambda函数,如:

t= threading.Thread(name='func'+str(i), target=lambda i=i: func(objs[i]))

这与使用类对象的第二种解决方案相同。

关于另一点:当Thread.start评估目标时?这取决于系统。它可能会有所不同如果有延迟,则会在i递增后进行评估。这可能是造成不一致的原因。