这个Python代码是一种使用多线程的安全方法

时间:2015-05-19 01:32:09

标签: python multithreading python-3.x

我用于图形的应用程序有一个嵌入式Python解释器 - 除了有一些特殊对象外,它与任何其他Python解释器完全相同。

基本上我正在尝试使用Python下载一堆图像并制作其他网络和磁盘I / O.如果我这样做没有多线程,我的应用程序将冻结(即视频退出播放),直到下载完成。

为了解决这个问题,我尝试使用多线程。但是,我无法触及任何主要过程。

我写了这段代码。该程序唯一的部分是评论。 me.store / me.fetch基本上是获取全局变量的一种方式。 op('files')指的是全局表。

这是两件事,"在主要过程中"只能以线程安全的方式触及。我不确定我的代码是否会这样做。

我会推荐任何关于为什么或(为什么不)这个代码是线程安全的输入以及如何以线程安全的方式绕过访问全局变量。

我担心的一件事是许多线程如何多次获取counter。由于它仅在写入文件后更新,否则会导致竞争条件,其中不同的线程以相同的值访问计数器(然后不要正确存储递增的值)。或者,如果磁盘写入失败,计数器会发生什么。

from urllib import request
import threading, queue, os

url = 'http://users.dialogfeed.com/en/snippet/dialogfeed-social-wall-twitter-instagram.json?api_key=ac77f8f99310758c70ee9f7a89529023'

imgs = [
    'http://search.it.online.fr/jpgs/placeholder-hollywood.jpg.jpg',
    'http://www.lpkfusa.com/Images/placeholder.jpg',
    'http://bi1x.caltech.edu/2015/_images/embryogenesis_placeholder.jpg'
]

def get_pic(url):
    # Fetch image data
    data = request.urlopen(url).read()
    # This is the part I am concerned about, what if multiple threads fetch the counter before it is updated below
    # What happens if the file write fails?
    counter = me.fetch('count', 0)

    # Download the file
    with open(str(counter) + '.jpg', 'wb') as outfile:
        outfile.write(data)
        file_name = 'file_' + str(counter)
        path = os.getcwd() + '\\' + str(counter) + '.jpg'
        me.store('count', counter + 1)
        return file_name, path


def get_url(q, results):
    url = q.get_nowait()
    file_name, path = get_pic(url)
    results.append([file_name, path])
    q.task_done()

def fetch():
    # Clear the table
    op('files').clear()
    results = []
    url_q = queue.Queue()
    # Simulate getting a JSON feed
    print(request.urlopen(url).read().decode('utf-8'))

    for img in imgs:
        # Add url to queue and start a thread
        url_q.put(img)
        t = threading.Thread(target=get_url, args=(url_q, results,))
        t.start()

    # Wait for threads to finish before updating table
    url_q.join()
    for cell in results:
        op('files').appendRow(cell)
    return

# Start a thread so that the first http get doesn't block
thread = threading.Thread(target=fetch) 
thread.start()

3 个答案:

答案 0 :(得分:1)

您的代码似乎根本不安全。要点:

  • 附加到results是不安全的 - 两个线程可能会尝试同时附加到列表中。
  • 在另一个线程设置了新的counter值之前,访问和设置counter是不安全的 - 我获取counter的线程。
  • 传递网址队列是多余的 - 只需将新网址传递给每个作业。

另一种方式(concurrent.futures

由于您使用的是python 3,为什么不使用concurrent.futures模块,这使您的任务更容易管理。下面我以一种不需要显式同步的方式写出你的代码 - 所有工作都由期货模块处理。

from urllib import request
import os
import threading

from concurrent.futures import ThreadPoolExecutor
from itertools import count

url = 'http://users.dialogfeed.com/en/snippet/dialogfeed-social-wall-twitter-instagram.json?api_key=ac77f8f99310758c70ee9f7a89529023'

imgs = [
    'http://search.it.online.fr/jpgs/placeholder-hollywood.jpg.jpg',
    'http://www.lpkfusa.com/Images/placeholder.jpg',
    'http://bi1x.caltech.edu/2015/_images/embryogenesis_placeholder.jpg'
]

def get_pic(url, counter):
    # Fetch image data
    data = request.urlopen(url).read()

    # Download the file
    with open(str(counter) + '.jpg', 'wb') as outfile:
        outfile.write(data)
        file_name = 'file_' + str(counter)
        path = os.getcwd() + '\\' + str(counter) + '.jpg'
        return file_name, path

def fetch():
    # Clear the table
    op('files').clear()

    with ThreadPoolExecutor(max_workers=2) as executor:
        count_start = me.fetch('count', 0)
        # reserve these numbers for our tasks
        me.store('count', count_start + len(imgs))
        # separate fetching and storing is usually not thread safe
        # however, if only one thread modifies count (the one running fetch) then 
        # this will be safe (same goes for the files variable)

        for cell in executor.map(get_pic, imgs, count(count_start)):
            op('files').appendRow(cell)


# Start a thread so that the first http get doesn't block
thread = threading.Thread(target=fetch) 
thread.start()

如果多个线程修改了count,那么在修改count时应该使用锁。

例如

lock = threading.Lock()

def fetch():
    ...
    with lock:
        # Do not release the lock between accessing and modifying count.
        # Other threads wanting to modify count, must use the same lock object (not 
        # another instance of Lock).
        count_start = me.fetch('count', 0)
        me.store('count', count_start + len(imgs))    
   # use count_start here

如果一个作业由于某种原因失败,那么唯一的问题就是你会得到一个丢失的文件号。任何引发的异常也会中断执行映射的执行程序,通过在那里重新引发异常 - 然后你可以根据需要做一些事情。

在将文件永久移动到某个地方之前,您可以使用tempfile模块找到临时存储文件的位置来避免使用计数器。

答案 1 :(得分:0)

如果您不熟悉python多线程内容,请记得查看multiprocessingthreading

您的代码似乎没问题,但代码样式不是很容易阅读。您需要运行它以查看它是否符合您的预期。

with将确保您的锁被释放。输入块时将调用acquire()方法,退出块时将调用release()。

如果添加更多线程,请确保它们没有使用队列中的相同地址且没有竞争条件(似乎由Queue.get()完成,但您需要运行它来验证)。记住,每个线程共享相同的进程,因此几乎所有内容都是共享的您不希望两个线程正在处理相同的address

答案 2 :(得分:0)

Lock根本不做任何事情。您只有一个线程可以调用download_job - 这是您分配给my_thread的线程。另一个是主线程,调用offToOn并在到达该函数结束时立即完成。因此没有第二个线程试图获取锁,因此没有第二个线程被阻止。您提到的表格显然是在您明确打开和关闭的文件中。如果操作系统保护此文件免受来自不同程序的同时访问,您可以侥幸逃脱;否则它肯定是不安全的,因为你还没有完成任何线程同步。

线程之间的正确同步要求不同的线程可以访问SAME锁;即,多个线程访问一个锁。另请注意,“thread”不是“process”的同义词。 Python支持两者。如果您真的应该避免访问主进程,则必须使用多处理模块来启动和管理第二个进程。

此代码永远不会退出,因为总是有一个线程在无限循环中运行(在threader中)。

以线程安全的方式访问资源需要以下内容:

a_lock = Lock()
def use_resource():
    with a_lock:
        # do something

在使用它的函数之外创建一次锁。无论从哪个线程,对整个应用程序中资源的每次访问都必须通过调用use_resource或某些等效来获取相同的锁。