จะเรียกใช้ฟังก์ชันแบบขนานได้อย่างไร?


118

ฉันค้นคว้าก่อนและไม่พบคำตอบสำหรับคำถามของฉัน ฉันพยายามเรียกใช้ฟังก์ชันหลายอย่างพร้อมกันใน Python

ฉันมีสิ่งนี้:

files.py

import common #common is a util class that handles all the IO stuff

dir1 = 'C:\folder1'
dir2 = 'C:\folder2'
filename = 'test.txt'
addFiles = [25, 5, 15, 35, 45, 25, 5, 15, 35, 45]

def func1():
   c = common.Common()
   for i in range(len(addFiles)):
       c.createFiles(addFiles[i], filename, dir1)
       c.getFiles(dir1)
       time.sleep(10)
       c.removeFiles(addFiles[i], dir1)
       c.getFiles(dir1)

def func2():
   c = common.Common()
   for i in range(len(addFiles)):
       c.createFiles(addFiles[i], filename, dir2)
       c.getFiles(dir2)
       time.sleep(10)
       c.removeFiles(addFiles[i], dir2)
       c.getFiles(dir2)

ฉันต้องการเรียก func1 และ func2 และให้พวกเขาทำงานในเวลาเดียวกัน ฟังก์ชันไม่โต้ตอบซึ่งกันและกันหรือบนวัตถุเดียวกัน ตอนนี้ฉันต้องรอให้ func1 เสร็จสิ้นก่อนที่ func2 จะเริ่ม ฉันจะทำสิ่งต่อไปนี้ได้อย่างไร:

process.py

from files import func1, func2

runBothFunc(func1(), func2())

ฉันต้องการสร้างไดเร็กทอรีทั้งสองให้ใกล้เคียงกันเพราะทุกๆนาทีฉันกำลังนับจำนวนไฟล์ที่สร้างขึ้น ถ้าไม่มีไดเร็กทอรีมันจะทำให้เวลาของฉันหมดไป


1
คุณอาจต้องการออกแบบสิ่งนี้ใหม่ หากคุณนับจำนวนไฟล์ / โฟลเดอร์ทุกๆนาทีแสดงว่าคุณกำลังสร้างเงื่อนไขการแข่งขัน สิ่งที่เกี่ยวกับการให้แต่ละฟังก์ชันอัปเดตตัวนับหรือใช้ไฟล์ล็อกเพื่อให้แน่ใจว่ากระบวนการตามระยะเวลาไม่อัปเดตจำนวนจนกว่าทั้งสองฟังก์ชันจะทำงานเสร็จสิ้น

คำตอบ:


173

คุณสามารถใช้threadingหรือmultiprocessing.

เนื่องจากลักษณะของ CPython , threadingไม่น่าเป็นไปเพื่อให้บรรลุความเท่าเทียมที่แท้จริง ด้วยเหตุmultiprocessingนี้โดยทั่วไปแล้วเป็นการเดิมพันที่ดีกว่า

นี่คือตัวอย่างที่สมบูรณ์:

from multiprocessing import Process

def func1():
  print 'func1: starting'
  for i in xrange(10000000): pass
  print 'func1: finishing'

def func2():
  print 'func2: starting'
  for i in xrange(10000000): pass
  print 'func2: finishing'

if __name__ == '__main__':
  p1 = Process(target=func1)
  p1.start()
  p2 = Process(target=func2)
  p2.start()
  p1.join()
  p2.join()

กลไกของการเริ่มต้น / การเข้าร่วมกระบวนการย่อยสามารถห่อหุ้มเป็นฟังก์ชันตามบรรทัดของคุณrunBothFunc:

def runInParallel(*fns):
  proc = []
  for fn in fns:
    p = Process(target=fn)
    p.start()
    proc.append(p)
  for p in proc:
    p.join()

runInParallel(func1, func2)

4
ฉันใช้รหัสของคุณ แต่ฟังก์ชันยังไม่เริ่มทำงานในเวลาเดียวกัน
lmcadory

4
@Lamar McAdory: โปรดอธิบายว่า "ในเวลาเดียวกัน" หมายถึงอะไรบางทีอาจจะให้ตัวอย่างที่เป็นรูปธรรมของสิ่งที่คุณทำสิ่งที่คุณคาดหวังว่าจะเกิดขึ้นและสิ่งที่เกิดขึ้นจริง
NPE

4
@ Lamar: คุณไม่มีทางรับประกันได้ว่า "เวลาเดียวกันเป๊ะ ๆ " และคิดว่าคุณทำได้ผิดธรรมดา ขึ้นอยู่กับจำนวนซีพียูที่คุณมีภาระของเครื่องระยะเวลาของหลายสิ่งที่เกิดขึ้นในคอมพิวเตอร์ทั้งหมดจะมีผลต่อเวลาที่เธรด / กระบวนการเริ่มต้น นอกจากนี้เนื่องจากกระบวนการต่างๆเริ่มต้นทันทีหลังการสร้างจึงต้องคำนวณค่าใช้จ่ายในการสร้างกระบวนการตามความแตกต่างของเวลาที่คุณเห็น
Martin

1
เป็นไปได้ไหมที่จะได้รับรายการผลลัพธ์ของแต่ละฟังก์ชัน สมมติว่าแต่ละฟังก์ชันส่งคืนค่าที่แตกต่างกันค่าสามารถต่อท้ายบางรายการที่สามารถใช้ในภายหลังได้หรือไม่ อาจจะต่อท้ายผลลัพธ์ในรายการส่วนกลาง?
pelos

1
หากฟังก์ชันของฉันใช้พารามิเตอร์และเมื่อฉันส่งผ่านพารามิเตอร์ในขณะที่เรียกใช้จากกระบวนการแยกกันฟังก์ชันจะไม่ทำงานพร้อมกัน คุณช่วยได้
ไหม

20

สิ่งนี้สามารถทำได้อย่างสง่างามด้วยRayซึ่งเป็นระบบที่ช่วยให้คุณสามารถขนานและแจกจ่ายโค้ด Python ของคุณได้อย่างง่ายดาย

การคู่ขนานตัวอย่างของคุณคุณจะต้องกำหนดฟังก์ชั่นของคุณด้วยมัณฑนากรและจากนั้นเรียกพวกเขาด้วย@ray.remote.remote

import ray

ray.init()

dir1 = 'C:\\folder1'
dir2 = 'C:\\folder2'
filename = 'test.txt'
addFiles = [25, 5, 15, 35, 45, 25, 5, 15, 35, 45]

# Define the functions. 
# You need to pass every global variable used by the function as an argument.
# This is needed because each remote function runs in a different process,
# and thus it does not have access to the global variables defined in 
# the current process.
@ray.remote
def func1(filename, addFiles, dir):
    # func1() code here...

@ray.remote
def func2(filename, addFiles, dir):
    # func2() code here...

# Start two tasks in the background and wait for them to finish.
ray.get([func1.remote(filename, addFiles, dir1), func2.remote(filename, addFiles, dir2)]) 

ray.put()ถ้าคุณผ่านอาร์กิวเมนต์เดียวกันกับฟังก์ชั่นทั้งสองและข้อโต้แย้งที่มีขนาดใหญ่เป็นวิธีที่มีประสิทธิภาพมากขึ้นในการทำเช่นนี้คือการใช้ สิ่งนี้หลีกเลี่ยงอาร์กิวเมนต์ขนาดใหญ่ที่จะทำให้เป็นอนุกรมสองครั้งและสร้างสำเนาหน่วยความจำสองชุด:

largeData_id = ray.put(largeData)

ray.get([func1(largeData_id), func2(largeData_id)])

สำคัญ - หากfunc1()และfunc2()ส่งคืนผลลัพธ์คุณต้องเขียนโค้ดใหม่ดังนี้:

ret_id1 = func1.remote(filename, addFiles, dir1)
ret_id2 = func2.remote(filename, addFiles, dir2)
ret1, ret2 = ray.get([ret_id1, ret_id2])

มีข้อดีหลายประการของการใช้ Ray ผ่านโมดูลหลายกระบวนการ โดยเฉพาะอย่างยิ่งรหัสเดียวกันจะทำงานบนเครื่องเดียวและบนคลัสเตอร์ของเครื่อง สำหรับข้อดีเพิ่มเติมของ Ray ดูโพสต์ที่เกี่ยวข้องนี้


ฉันพบว่านี่เป็นตัวเลือกที่ดีกว่า สิ่งหนึ่งที่ฉันจะเพิ่มเข้าไปโดยเฉพาะอย่างยิ่งถ้าคุณใช้ในนักเทียบท่าก็คือมันขึ้นอยู่กับสถาปัตยกรรม ณ ขณะนี้ไม่สามารถใช้งานกับ alpine linux ได้ (Centos 7 ทำงานให้ฉัน) และคุณควรเรียกใช้ray.shutdown()หลังจากเรียกใช้สิ่งนี้เพราะคุณจะหมดหน่วยความจำอย่างรวดเร็วหากสิ่งที่คุณทำมีความซับซ้อนในทางใดทางหนึ่ง
jimh

18

หากฟังก์ชันของคุณทำงานI / O เป็นหลัก (และทำงานของ CPU น้อยลง) และคุณมี Python 3.2+ คุณสามารถใช้ThreadPoolExecutor :

from concurrent.futures import ThreadPoolExecutor

def run_io_tasks_in_parallel(tasks):
    with ThreadPoolExecutor() as executor:
        running_tasks = [executor.submit(task) for task in tasks]
        for running_task in running_tasks:
            running_task.result()

run_io_tasks_in_parallel([
    lambda: print('IO task 1 running!'),
    lambda: print('IO task 2 running!'),
])

หากฟังก์ชั่นของคุณทำงานCPUเป็นหลัก(และทำงาน I / O น้อยลง) และคุณมี Python 2.6+ คุณสามารถใช้โมดูลมัลติโปรเซสเซอร์ได้ :

from multiprocessing import Process

def run_cpu_tasks_in_parallel(tasks):
    running_tasks = [Process(target=task) for task in tasks]
    for running_task in running_tasks:
        running_task.start()
    for running_task in running_tasks:
        running_task.join()

run_cpu_tasks_in_parallel([
    lambda: print('CPU task 1 running!'),
    lambda: print('CPU task 2 running!'),
])

นี่คือคำตอบที่ดี วิธีการระบุจากผลลัพธ์สำหรับงานที่ถูกผูกไว้ I / O โดยใช้ concurrent.futures ซึ่งงานใดเสร็จสมบูรณ์ โดยทั่วไปแทนที่จะเป็นฟังก์ชัน lamba หากเรามีฟังก์ชันปกติจะระบุผลลัพธ์ที่แมปกับฟังก์ชันที่เรียกได้อย่างไร
Tragaknight

ไม่เป็นไรฉันพบวิธี - แทนที่จะใช้ run_cpu_tasks_in_parallel ([lambda: print ('CPU task 1 running!'), lambda: print ('CPU task 2 running!'),]) ใช้สิ่งนี้ - results = run_io_tasks_in_parallel ([lambda: {'is_something1': func1 ()}, lambda: {'is_something2': func2 ()},])
Tragaknight

หากฟังก์ชันให้เอาต์พุตสำหรับพารามิเตอร์ที่แตกต่างกันวิธีบันทึก แท้จริงแล้วสิ่งที่ควรวางไว้lambda: print('CPU task 1 running!'), lambda: print('CPU task 2 running!'),เพื่อผนวกผลลัพธ์เข้ากับตัวแปรtask1_outputและtask2_output
Sai Kiran

5

หากคุณเป็นผู้ใช้ windows และใช้ python 3 โพสต์นี้จะช่วยคุณในการเขียนโปรแกรมแบบขนานใน python เมื่อคุณเรียกใช้การเขียนโปรแกรมพูลของไลบรารีหลายกระบวนการตามปกติคุณจะได้รับข้อผิดพลาดเกี่ยวกับฟังก์ชันหลักในโปรแกรมของคุณ นี่เป็นเพราะความจริงที่ว่า windows ไม่มีฟังก์ชัน fork () โพสต์ด้านล่างกำลังให้วิธีแก้ปัญหาดังกล่าว

http://python.6.x6.nabble.com/Multiprocessing-Pool-woes-td5047050.html

เนื่องจากฉันใช้ python 3 ฉันจึงเปลี่ยนโปรแกรมเป็นแบบนี้:

from types import FunctionType
import marshal

def _applicable(*args, **kwargs):
  name = kwargs['__pw_name']
  code = marshal.loads(kwargs['__pw_code'])
  gbls = globals() #gbls = marshal.loads(kwargs['__pw_gbls'])
  defs = marshal.loads(kwargs['__pw_defs'])
  clsr = marshal.loads(kwargs['__pw_clsr'])
  fdct = marshal.loads(kwargs['__pw_fdct'])
  func = FunctionType(code, gbls, name, defs, clsr)
  func.fdct = fdct
  del kwargs['__pw_name']
  del kwargs['__pw_code']
  del kwargs['__pw_defs']
  del kwargs['__pw_clsr']
  del kwargs['__pw_fdct']
  return func(*args, **kwargs)

def make_applicable(f, *args, **kwargs):
  if not isinstance(f, FunctionType): raise ValueError('argument must be a function')
  kwargs['__pw_name'] = f.__name__  # edited
  kwargs['__pw_code'] = marshal.dumps(f.__code__)   # edited
  kwargs['__pw_defs'] = marshal.dumps(f.__defaults__)  # edited
  kwargs['__pw_clsr'] = marshal.dumps(f.__closure__)  # edited
  kwargs['__pw_fdct'] = marshal.dumps(f.__dict__)   # edited
  return _applicable, args, kwargs

def _mappable(x):
  x,name,code,defs,clsr,fdct = x
  code = marshal.loads(code)
  gbls = globals() #gbls = marshal.loads(gbls)
  defs = marshal.loads(defs)
  clsr = marshal.loads(clsr)
  fdct = marshal.loads(fdct)
  func = FunctionType(code, gbls, name, defs, clsr)
  func.fdct = fdct
  return func(x)

def make_mappable(f, iterable):
  if not isinstance(f, FunctionType): raise ValueError('argument must be a function')
  name = f.__name__    # edited
  code = marshal.dumps(f.__code__)   # edited
  defs = marshal.dumps(f.__defaults__)  # edited
  clsr = marshal.dumps(f.__closure__)  # edited
  fdct = marshal.dumps(f.__dict__)  # edited
  return _mappable, ((i,name,code,defs,clsr,fdct) for i in iterable)

หลังจากฟังก์ชั่นนี้รหัสปัญหาข้างต้นจะเปลี่ยนไปเล็กน้อยเช่นนี้:

from multiprocessing import Pool
from poolable import make_applicable, make_mappable

def cube(x):
  return x**3

if __name__ == "__main__":
  pool    = Pool(processes=2)
  results = [pool.apply_async(*make_applicable(cube,x)) for x in range(1,7)]
  print([result.get(timeout=10) for result in results])

และฉันได้ผลลัพธ์เป็น:

[1, 8, 27, 64, 125, 216]

ฉันคิดว่าโพสต์นี้อาจเป็นประโยชน์สำหรับผู้ใช้ windows บางคน


5

ดูเหมือนว่าคุณมีฟังก์ชันเดียวที่คุณต้องเรียกใช้พารามิเตอร์ที่ต่างกันสองตัว สิ่งนี้สามารถทำได้อย่างหรูหราโดยใช้การผสมผสานระหว่างconcurrent.futuresและmapกับ Python 3.2+

import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def sleep_secs(seconds):
  time.sleep(seconds)
  print(f'{seconds} has been processed')

secs_list = [2,4, 6, 8, 10, 12]

ตอนนี้หากการดำเนินการของคุณถูกผูกไว้กับ IO คุณสามารถใช้สิ่งต่อไปThreadPoolExecutorนี้:

with ThreadPoolExecutor() as executor:
  results = executor.map(sleep_secs, secs_list)

สังเกตวิธีmapใช้ที่นี่เพื่อmapฟังก์ชันของคุณไปยังรายการอาร์กิวเมนต์

ตอนนี้หากฟังก์ชันของคุณถูกผูกไว้กับ CPU คุณสามารถใช้ ProcessPoolExecutor

with ProcessPoolExecutor() as executor:
  results = executor.map(sleep_secs, secs_list)

หากคุณไม่แน่ใจคุณสามารถลองทั้งสองอย่างและดูว่าอันไหนให้ผลลัพธ์ที่ดีกว่า

สุดท้ายหากคุณต้องการพิมพ์ผลลัพธ์ของคุณคุณสามารถทำได้:

with ThreadPoolExecutor() as executor:
  results = executor.map(sleep_secs, secs_list)
  for result in results:
    print(result)

4

ไม่มีทางรับประกันได้ว่าฟังก์ชันทั้งสองจะทำงานโดยซิงค์กันซึ่งดูเหมือนจะเป็นสิ่งที่คุณต้องการทำ

สิ่งที่ดีที่สุดที่คุณสามารถทำได้คือแยกฟังก์ชันออกเป็นหลายขั้นตอนจากนั้นรอให้ทั้งคู่เสร็จสิ้นที่จุดซิงโครไนซ์ที่สำคัญโดยใช้Process.joinคำตอบของ like @ aix ที่กล่าวถึง

ซึ่งดีกว่าtime.sleep(10)เพราะคุณไม่สามารถรับประกันเวลาที่แน่นอนได้ ด้วยการรออย่างชัดเจนคุณกำลังบอกว่าฟังก์ชั่นจะต้องดำเนินการในขั้นตอนนั้นก่อนที่จะย้ายไปที่ถัดไปแทนที่จะสมมติว่ามันจะเสร็จสิ้นภายใน 10ms ซึ่งไม่ได้รับประกันตามสิ่งที่เกิดขึ้นในเครื่อง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.