【转载】Python中使用线程的技巧

经常用 Python 写程序的朋友应该都知道怎么用 threading 模块来启动一个新线程。主要有两种方式:

  1. 直接使用 threading.Thread 类型。这种方法相对简单。比如下面这两行代码演示了如何启动一个新线程,并且当新线程调用 sendData() 函数时传入 'arg1' , arg2' 两个参数:

    sendDataThread=threading.Thread(target=sendData, args=('arg1', 'arg2')) 
    sendDataThread.start()
    
  2. 继承 threading.Thread 类,重载它的 run() 方法。这种方法比较麻烦,它的好处是,很方便把一个长的函数拆分成好几部分,方便多个线程之间的同步。比如下面这个发送数据的线程,它会发送数据的时候做统计,其中 makeConnection() 使用了连接池:

    ?


    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    class SendDataThread(threading.Thread):

        daemon=True

        def __init__(self, counter, dataSource):

            threading.Thread.__init__(self)

            #多个线程共享一个counter

            self.counter, self.dataSource=counter, dataSource

     

        def run(self):

            try:

                self.sendData()

            finally:

                del self.counter, self.dataSource

     

        def sendData(self):

            #此处省略连接,取数据等操作,分别是makeConnection()和makePacket()两个方法

            connection=self.makeConnection()

            data=self.dataSource.makePacket()

            sentBytes=connection.send(data)

            self.counter.increase(sentBytes)

     

        def makeConnection(self):

            "从连接池里面返回一个空闲的连接。"

      两种方法实际上差不多。如果线程做的工作比较简单,只有一个函数就使用第一种。如果线程的工作繁复,可以拆成多个方法,就写成一个类。不过需要注意的是,第二种方法最好把run()方法像上面一样写成try...finally... 的形式,主要是为了避免循环引用。Python的GC使用了简单的引用计数,所以,如果Counter引用了SendDataThread,而SendDataThread也引用了Counter就会发生循环引用,两个对象可能不会被释放。这时,最好在SendDataThread执行完毕时解除对Counter的引用。当然,如果确定不会发生循环引用,可以不用这样做。

第二种方法需要继承自Thread,有时候不太方便。我们可以结合第一种方法,稍微变通一样。比如这样:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import threading, struct

 

class Writer:

    def write(self, fout, string, blockSize):

        for block in self.splitToBlock(string, blockSize):

            header=struct.pack("!i", len(block))

            fout.write(header)

            fout.write(block)

 

    def splitToBlock(self, string, blockSize):

        data=string.encode("utf-8")

        for i in range(0, len(data), blockSize):

            yield data[i:i+blockSize]

 

writer=Writer()

threading.Thread(target=writer.write, args=(fout, string, blockSize)).start()

最后一句推广开来可以弄成一个decorater,让同步执行的方法立即变成异步执行。

?


1

2

3

4

5

6

7

8

import threading, functools

def async(wrapped):

    def wrapper(*args, **kwargs):

        t=threading.Thread(target=wrapped, args=args, kwargs=kwargs)

        t.daemon=True

        t.start()

    functools.update_wrapper(wrapper, wrapped)

    return wrapper

于是write()可以改写成:

?


1

2

3

@async

def write(self, fout, string, blockSize):

    pass #和原来一样

下次直接调用writer.write()就变成异步运行了。

      在Python里面使用线程最不爽的恐怕是不能强制结束线程。相比之下 ,Java语言的线程类支持Thread.interrupt(),当线程阻塞在Lock或者Event的时候仍然可以很方便地让线程退出。在Python里面,阻塞的调用多是直接调用C语言的系统函数,而不是像Java那样重写各种阻塞的IO。好处是Python的应用程序可能会拥有更好的IO性能,但是直接的坏处就是必须由用户自行处理阻塞函数。而且像Java那样重写阻塞IO的函数工作量太大,也给扩展Python解释器和移植工作带来很多的麻烦。

那么,既然不能强制性地结束线程,只好用一些迂回地办法,让线程自己退出。伪代码类似于这样:

?


1

2

3

4

5

6

7

8

9

10

11

class DoSomethingThread(threading.Thread):

    daemon=True

    def run(self):

        while True:

            self.doBlockIO()

            if self.exiting:break

 

    def shutdown(self):

        self.exiting=True

        self.interruptBlockIO()

        #self.join()

      有两个关键,一是每次在阻塞函数返回后都判断一下标志位,看是不是应该结束线程了。通常每个循环体内只有一个阻塞函数,所以把判断放在循环语句上面就行了。二是采用某种办法让阻塞函数返回。各种阻塞函数都不尽相同。

      如果阻塞函数支持超时,那就方便了,直接在阻塞函数内传入超时时间,比如0.2秒,连self.interruptBlockIO()这一句都可以省略掉。Python默认创建的socket是永远阻塞的,可以使用socket.settimeout()来设置超时时间——要小心捕获socket.timeout异常。更好的办法是使用select模块,它不仅可以设置超时,还能够在一个线程内同时处理socket的读和写。socket.connect和socket.accept()都支持超时。threading模块的各种锁、信号都支持超时。

      如果阻塞函数不支持超时,那就只好采用一些山寨办法了。假定socket.accept()不支持超时的话,我们可以在shutdown函数里面创建一个新socket连接自己监听的端口,让accept()函数返回。

      如果调用的C函数确实没办法让它从阻塞状态退出,可以考虑使用multiprocessing模块。因为强制结束一个进程不是一个多大不了的事。不过multiprocessing与threading相比,交换数据的效率会低一点。

      细心的朋友可能会发现上面的几段代码都设置了daemon属性。将这个属性设置为True可以保证线程在主线程退出后也会立即退出。一般说来,主线程是负责用户UI的,如果用户关闭了程序,线程继续运行有什么意义呢。这个属性其实挺常用的,不知道为什么默认值是False。

      在使用多线程的时候还要注意import语句不支持多线程,也就是不能有多个线程同时在执行import语句。所以最好不要在模块导入过程中再启动一个新线程。比如模块这样写是不好的:

?


1

2

3

4

5

6

7

8

9

10

#encoding:utf-8

import threading, logging

logging.basicConfig()

logger=logging.getLogger(__name__)

def doSomething():

    "做一项麻烦的工作,并写日志。"

    logger.error("doSomething")

t=threading.Thread(target=doSomething)

t.start()

t.join()

      这个mymodule写得不好。因为import mymodule将会发生死锁,原因是logger.error()的源代码内有一句import multiprocessing。如果主线程和doSomething()都执行到import那里,Python就会锁在import语句那里不会退出。于是t.join()会被阻塞,永远不会返回。事实上,我曾经发现即使去掉t.join()也有可能会出错,不过不知道怎么重现。一个比较好的写法是把线程的启动代码移进函数,不搞并发执行import语句就没关系了。

import mymodule mymodule.doSomething()

      经常使用PyQt的朋友可能会发现PyQt也有一个QThread,而且与threading.Thread相比,还多出了QThread.terminate(),它看起来能省却很多的麻烦。那么,写PyQt程序的时候到底应该选择哪一个呢?

我的看法,QThread不应被使用。

      因为QThread是专为C++环境设计的,不适合Python程序。如果看过Qt文档的话,可以注意到QThread.terminate()本身也不是不推荐被使用的,如果一定要使用,就要调用QThread::setTerminationEnabled()设置可中断标志。如果C++代码仔细设置好可中断标志,QThread::terminate()就没有副作用。但是Python环境没办法像C++那样,在读写共享数据的时候设置可中断标志,除非你修改Python虚拟机,把这个调用整合到Python的源代码里面。

      另外,当QThread没有被其它对象引用的时候,根据Python的内存管理模型,这个QThread会被删除。不幸的是,如果QThread的析构函数检测到线程是被强制结束的,它会打印出一行错误信息,然后结束整个进程。于是整个Python程序会意外地退出。显然,这种行为相当的不好。竟然不是抛出一个异常。如果你不想因为一个小小的可挽回的错误导致整个程序失败的话,最好不要使用QThread。

      另外,尽量不要在非主线程使用QObject的对象。Qt的文档说,QObject与QThread有特别的关联。有以下几个注意事项:

  1. 只有创建QObject的进程才能使用它。不能在一个线程里面创建QTimer,而在另外一个线程里面调用QTimer.start()。
  2. 在一个线程里面创建的QObject不能在另外一个线程里面被销毁。
  3. Qt的内存管理模型区别“父QObject”和“子QObject”。Qt要求“子QObject”必须和“父QObject”同一个线程。

      又一次很不幸,第2条和Python的GC有冲突。Python的GC不固定地在某个线程里面运行。如果刚好回收了一个不在当前线程里面创建的QObject,程序就有可能会崩溃。注:貌似PyQt的开发者提到会解决这个问题,不知道现在怎么样了。

原文地址:http://besteam.im/blogs/article/80/

时间: 2025-01-27 05:56:44

【转载】Python中使用线程的技巧的相关文章

Python中字符串的处理技巧分享_python

一.如何拆分含有多种分隔符的字符串? 实际案例 我们要把某个字符串依据分隔符号拆分不同的字符段,该字符串包含多种不同的分隔符,例如: s = 'asd;aad|dasd|dasd,sdasd|asd,,Adas|sdasd;Asdasd,d|asd' 其中<,>,<;>,<|>,<\t>都是分隔符,如何处理? 解决方案 连续使用split()方法,每次处理一种分隔符 # 使用Python2 def mySplit(s,ds): res = [s] for d

使用Python中的线程进行网络编程的入门教程_python

引言 对于 Python 来说,并不缺少并发选项,其标准库中包括了对线程.进程和异步 I/O 的支持.在许多情况下,通过创建诸如异步.线程和子进程之类的高层模块,Python 简化了各种并发方法的使用.除了标准库之外,还有一些第三方的解决方案,例如 Twisted.Stackless 和进程模块.本文重点关注于使用 Python 的线程,并使用了一些实际的示例进行说明.虽然有许多很好的联机资源详细说明了线程 API,但本文尝试提供一些实际的示例,以说明一些常见的线程使用模式. 全局解释器锁 (G

对于Python中线程问题的简单讲解_python

我们将会看到一些在Python中使用线程的实例和如何避免线程之间的竞争.你应当将下边的例子运行多次,以便可以注意到线程是不可预测的和线程每次运行出的不同结果.声明:从这里开始忘掉你听到过的关于GIL的东西,因为GIL不会影响到我想要展示的东西. 示例1 我们将要请求五个不同的url:单线程  import time import urllib2 def get_responses(): urls = [ 'http://www.google.com', 'http://www.amazon.co

Python中线程编程之threading模块的使用详解

  这篇文章主要介绍了Python中线程编程之threading模块的使用详解,由于GIL的存在,线程一直是Python编程中的焦点问题,需要的朋友可以参考下 threading.Thread Thread 是threading模块中最重要的类之一,可以使用它来创建线程.有两种方式来创建线程:一种是通过继承Thread类,重写它的run方法;另一种是创建一个threading.Thread对象,在它的初始化函数(__init__)中将可调用对象作为参数传入.下面分别举例说明.先来看看通过继承th

Python中输出ASCII大文字、艺术字、字符字小技巧

  这篇文章主要介绍了Python中输出ASCII大文字.艺术字.字符字小技巧,本文首先给出了ASCII大文字.艺术字.字符字的图片效果,然后给出了Python中的实现方法,需要的朋友可以参考下 代码如下: display text in large ASCII art fonts 显示大ASCII艺术字体 这种东西在源码声明或者软件初始化控制台打印时候很有用. 例如下图: 这是查看HTML源码中截图而来,看到这种字体的网站名称,很cool,下面就介绍一下Python中如何输出这种字符字. 代码

python中如何对类的成员函数开启线程?

问题描述 python中如何对类的成员函数开启线程? 单独对某个函数开启线程是可以的,比如下面的代码: import threading import thread import time def doWaiting(): print 'start waiting:', time.strftime('%H:%M:%S') time.sleep(3) print 'stop waiting', time.strftime('%H:%M:%S') thread1 = threading.Thread

在Python中通过threading模块定义和调用线程的方法_python

定义线程 最简单的方法:使用target指定线程要执行的目标函数,再使用start()启动. 语法: class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}) group恒为None,保留未来使用.target为要执行的函数名.name为线程名,默认为Thread-N,通常使用默认即可.但服务器端程序线程功能不同时,建议命名. #!/usr/bin/env python3 # coding=utf

Python中字符串的常见操作技巧总结_python

本文实例总结了Python中字符串的常见操作技巧.分享给大家供大家参考,具体如下: 反转一个字符串 >>> S = 'abcdefghijklmnop' >>> S[::-1] 'ponmlkjihgfedcba' 这种用法叫做three-limit slices 除此之外,还可以使用slice对象,例如 >>> 'spam'[slice(None, None, -1)] >>> unicode码与字符(single-characte

Python中Collection的使用小技巧_python

本文所述实例来自独立软件开发者 Alex Marandon,在他的博客中曾介绍了数个关于 Python Collection 的实用小技巧,在此与大家分享.供大家学习借鉴之用.具体如下: 1.判断一个 list 是否为空 传统的方式: if len(mylist): # Do something with my list else: # The list is empty 由于一个空 list 本身等同于 False,所以可以直接: if mylist: # Do something with