首页 > 技术文章 > python 多线程学习

guanfuchang 2016-10-29 11:27 原文

多线程(multithreaded,MT),是指从软件或者硬件上实现多个线程并发执行的技术

什么是进程?

计算机程序只不过是磁盘中可执行的二进制(或其他类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命周期。

进程是程序的一次执行。每个进程都有自己的地址空间、内存、数据栈及其他运行轨迹的辅助数据。比如打开播放器就是一个进程。

什么是线程?

线程跟进程相似,不同的是所有的线程运行在同一个进程中,共享相同的运行环境,共享同一片数据空间。比如在播放器中一边播放画面,一遍播放声音,就是多个线程在同时执行。线程有开始,顺序执行和结束三部分。实际上,只有多核CPU才能实现真正意义上的多线程,在一个CPU上,在同一时刻只能执行一个线程。但是即便处理器只能运行一个线程,操作系统也可以通过快速的在不同线程之间进行切换,由于时间间隔很小,来给用户造成一种多个线程同时运行的假象。

 

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

 

ps:如果大家还不是很了解进程与线程,可以看下廖雪峰老师的文章 ,写的非常棒!

参考地址:http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014319272686365ec7ceaeca33428c914edf8f70cca383000

单线程与多线程实例感受

假设:loop0() 执行需要4s,loop1()执行需要2s。

单线程代码实例:

#!/usr/bin/env python 
# -*- coding: utf-8 -*-

from time import sleep, ctime


def loop0():
    print "loop 0 start at:%s" % ctime()
    sleep(4)
    print "loop 0 done at:%s" % ctime()


def loop1():
    print "loop 1 start at:%s" % ctime()
    sleep(2)
    print "loop 1 done at:%s" % ctime()


def main():
    loop0()
    loop1()


if __name__ == '__main__':
    print "main program start at:%s" % ctime()
    main()
    print "main program all done at:%s" % ctime()

输出结果如下:

main program start at:Sat Oct 22 15:35:32 2016
loop 0 start at:Sat Oct 22 15:35:32 2016
loop 0 done at:Sat Oct 22 15:35:36 2016
loop 1 start at:Sat Oct 22 15:35:36 2016
loop 1 done at:Sat Oct 22 15:35:38 2016
main program all done at:Sat Oct 22 15:35:38 2016

从结果中可分析得知,loop0与loop1串行执行,先完成loop0,在执行loop1.总共花费了4s+2s=6s

多线程代码实例:

#!/usr/bin/env python 
# -*- coding: utf-8 -*-

from time import sleep, ctime
import thread


def loop0():
    print "loop 0 start at:%s" % ctime()
    sleep(4)
    print "loop 0 done at:%s" % ctime()


def loop1():
    print "loop 1 start at:%s" % ctime()
    sleep(2)
    print "loop 1 done at:%s" % ctime()


def main():
    thread.start_new_thread(loop0, ())
    thread.start_new_thread(loop1, ())
    sleep(4)


if __name__ == '__main__':
    print "main program start at:%s" % ctime()
    main()
    print "main program all done at:%s" % ctime()

输出结果如下:

main program start at:Sat Oct 22 15:44:06 2016
loop 0 start at:Sat Oct 22 15:44:06 2016
loop 1 start at:Sat Oct 22 15:44:06 2016
loop 1 done at:Sat Oct 22 15:44:08 2016
mian program all done at:Sat Oct 22 15:44:10 2016
loop 0 done at:Sat Oct 22 15:44:10 2016

从结果中可分析得知,loop0与loop1并行执行,总共花的时间为最长线程的时间4s

多线程实现方式介绍

python主要提供了三个模块thread、threading和Queue 来支持python的多线程操作。thread 和 threading模块允许程序员创建和管理线程,Queue模块允许用户创建一个可以用于多线程之间共享数据的队列数据结构。

一般现在我们只使用threading模块来编程了,thread模块定义了很多原始行为,更接近底层,而threading模块抽象了thread模块可用性更好,同时提供更多特性。另外一个避免使用thread模块的原因是,thread不支持守护线程,当主线程退出时,所有的子线程无论他们是否还在工作,都会被强行退出。

threading模块

threading的Thread类是主要的运行对象。他有很多thrad模块里没有的函数,如下表:

 
函数 描述
start() 开始线程的执行
run() 定义线程的功能的函数(一般会被子类重写)
join(timeout=None) 程序挂起,直到该子线程结束再继续执行主线程;如果给了timeout,则最多阻塞timeout秒
getName() 返回线程的名字
setName() 设置线程的名字
isAlive() 布尔标志,表示这个线程是否还在运行中
isDaemon() 返回线程的daemon标志
setDaemon(daemonic)                 把线程的daemon标志设为daemonic(一定要在调用start()函数前调用)

使用threading模块的Thread类创建线程,有三种比较相像的方法:

  • 创建一个Thread的实例,传给它一个函数;
  • 创建一个Thread的实例,传给它一个可调用的类对象
  • 从Thread派生出一个子类,创建一个这个子类的实例。(现在创建线程的通用方法一般是创建一个类并且继承threading.Thread,然后重写其中的__init__和run()方法)

创建多线程的方式

  • 实例一:创建一个Thread的实例,传给它一个函数
#!/usr/bin/env python 
# -*- coding: utf-8 -*-
"""
多线程在后台执行,与主线程并行运行
join() 等待子线程停止,才继续往下执行主线程
"""
import threading
from time import sleep, ctime


def work(nloop, nsec):
    print "Thread-%s start working at %s \n" % (nloop, ctime())
    sleep(nsec)
    print 'Thread-%s finished background working at %s \n' % (nloop, ctime())


if __name__ == '__main__':
    print "\033[1;31;40m The main program start to run in foreground at %s\033[0m \n" % ctime()
    sleep_sec = [2, 4]
    loop_times = len(sleep_sec)
    threads_list = []

    for i in range(loop_times):  # create threads
        t = threading.Thread(target=work, args=(i, sleep_sec[i]))   # 创建一个Thread实例,传给它一个函数
        threads_list.append(t)

    for i in range(loop_times):  # start threads
        threads_list[i].start()

    print "\033[1;31;40m The main program continues to run in foreground!\033[0m \n"

    for i in range(loop_times):  # wait for all threads to finish
        threads_list[i].join()

    print '\033[1;31;40m Main program waited until background was done at %s\033[0m \n' % ctime()

实例一输出结果如下:

  • 实例二:创建一个Thread的实例,传给它一个可调用的类对象
#!/usr/bin/env python 
# -*- coding: utf-8 -*-

import threading
from time import sleep, ctime


class ThreadFunc(object):
    def __init__(self, func, args):
        self.func = func
        self.args = args

    def __call__(self):
        apply(self.func, self.args)


def work(nloop, nsec):
    print "Thread-%s start working at %s \n" % (nloop, ctime())
    sleep(nsec)
    print 'Thread-%s finished background working at %s \n' % (nloop, ctime())


if __name__ == '__main__':
    print "\033[1;31;40m The main program start to run in foreground at %s\033[0m \n" % ctime()
    sleep_sec = [2, 4]
    loop_times = len(sleep_sec)
    threads_list = []

    for i in range(loop_times):  # create threads
        t = threading.Thread(target=ThreadFunc(work, (i, sleep_sec[i])))  # 创建一个Thread实例,传给它一个可调用的类对象
        threads_list.append(t)

    for i in range(loop_times):  # start threads
        threads_list[i].start()

    print "\033[1;31;40m The main program continues to run in foreground!\033[0m \n"

    for i in range(loop_times):  # wait for all threads to finish
        threads_list[i].join()

    print '\033[1;31;40m Main program waited until background was done at %s\033[0m \n' % ctime()

实例二输出结果如下:

  • 实例三:从Thread派生出一个子类,创建一个这个子类的实例。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading
from time import sleep, ctime


class AsyncWork(threading.Thread):
    def __init__(self, nsec):
        super(AsyncWork, self).__init__()
        self.nsec = nsec

    def work(self):
        print "%s start working at %s \n" % (self.getName(), ctime())
        sleep(self.nsec)
        print '%s finished background working at %s \n' % (self.getName(), ctime())

    def run(self):
        self.work()


if __name__ == '__main__':
    print "\033[1;31;40m The main program start to run in foreground at %s\033[0m \n" % ctime()
    sleep_nsec = [2, 4]
    loop_times = len(sleep_nsec)
    threads_list = []

    for i in range(loop_times):  # create threads
        t = AsyncWork(sleep_nsec[i])  # 创建一个Thread子类的实例
        threads_list.append(t)

    for i in range(loop_times):  # start threads
        threads_list[i].start()

    print "\033[1;31;40m The main program continues to run in foreground!\033[0m \n"

    for i in range(loop_times):  # wait for all threads to finish
        threads_list[i].join()

    print '\033[1;31;40m Main program waited until background was done at %s\033[0m \n' % ctime()

实例三输出结果如下:

多线程的数据共享与锁

 前面已经提到过,多线程共享相同的运行环境,共享同一片数据空间。因此当多线程在操作同一数据时,有可能会出现数据同步不正确问题。例如有一个列表aList=[0,0,0,0,0,0,...]

如果有一个线程从后往前修改,将0改为1,同时又有另外一个进程从前往后读取aList。则有可能出现在读取过程中,前面一部分是0,后面一部分已经被篡改为了1.如下

数据不同步实例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading
from time import sleep

aList = []
for i in range(100):
    aList.append(0)


class AsyncUpdate(threading.Thread):
    def __init__(self):
        super(AsyncUpdate, self).__init__()

    def run(self):
        self.update_alist()

    def update_alist(self):
        global aList
        nums = len(aList) - 1
        for i in range(nums, -1, -1):
            aList[i] = 1
            sleep(0.0001)


class AsyncRead(threading.Thread):
    def __init__(self):
        super(AsyncRead, self).__init__()

    def run(self):
        self.read_alist()

    def read_alist(self):
        global aList
        nums = len(aList)
        for i in range(nums):
            print aList[i],


if __name__ == '__main__':
    t1 = AsyncUpdate()
    t2 = AsyncRead()
    t1.start()
    t2.start()
    t1.join()
    t2.join()

输入结果如下:

明显,以上实例证明了我们的观点,在多线程中,我们引出了线程同步的概念,锁。

假如我们在读取alist列表时,先锁定aList,不让其他线程更改,线程在更改aList时,锁定aList,不让其他线程操作,那么我们读取出来的aList就只能是全部为0或者全部为1了。如以下实例

数据同步实例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading
from time import sleep

aList = []
for i in range(100):
    aList.append(0)

lock = threading.Lock() #创建线程锁


class AsyncUpdate(threading.Thread):
    def __init__(self):
        super(AsyncUpdate, self).__init__()

    def run(self):
        # 先要获取锁:
        lock.acquire()
        try:
            self.update_alist()
        finally:
            # 释放锁:
            lock.release()

    def update_alist(self):
        global aList
        nums = len(aList) - 1
        for i in range(nums, -1, -1):
            aList[i] = 1
            sleep(0.0001)


class AsyncRead(threading.Thread):
    def __init__(self):
        super(AsyncRead, self).__init__()

    def run(self):
        # 先要获取锁:
        lock.acquire()
        try:
            self.read_alist()
        finally:
            # 释放锁:
            lock.release()

    def read_alist(self):
        global aList
        nums = len(aList)
        for i in range(nums):
            print aList[i],


if __name__ == '__main__':
    t1 = AsyncUpdate()
    t2 = AsyncRead()
    t1.start()
    t2.start()
    t1.join()
    t2.join()

输出结果如下:

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

死锁

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

 假定有两个资源 ResA,ResB。一个线程先锁定ResA,然后锁定ResB,一个线程先锁定ResB,然后再锁定ResA。在多线程运行时,假设线程1锁定了资源A,还没来得及锁定资源B,线程2就已经锁定了资源B,此时线程1等待线程2释放资源B,线程2等待线程1释放资源A,双方都在僵持着等待对方解锁才能继续执行,此时程序就成为死进程,死锁。如下实例:

#!/usr/bin/env python 
# -*- coding: utf-8 -*-

import threading
from time import sleep

mutexA = threading.Lock()
mutexB = threading.Lock()


class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):
        """ fun1 先锁定资源A,然后再锁定资源B """
        global mutexA, mutexB
        if mutexA.acquire():
            print "I am %s , locked res: %s" % (self.name, "ResA")
            if mutexB.acquire():
                print "I am %s , locked res: %s" % (self.name, "ResB")
                # sleep(0.5)
                mutexB.release()
                print "I am %s , released res: %s" % (self.name, "ResB")
            mutexA.release()
            print "I am %s , released res: %s" % (self.name, "ResA")

    def fun2(self):
        """ fun2 先锁定资源B,然后再锁定资源A """
        global mutexA, mutexB
        if mutexB.acquire():
            print "I am %s , locked res: %s" % (self.name, "ResB")
            sleep(0.5)
            if mutexA.acquire():
                print "I am %s , locked res: %s" % (self.name, "ResA")
                mutexA.release()
                print "I am %s , released res: %s" % (self.name, "ResA")
            mutexB.release()
            print "I am %s , released res: %s" % (self.name, "ResB")


if __name__ == "__main__":
    for i in range(0, 100):
        my_thread = MyThread()
        my_thread.start()

输出结果如下:

最后一行,显示Thread-2 锁定了ResA,同时,Thread-1 锁定了ResB,此时,Thread-2要等待ResB才能完成工作,Thread-1要等待ResA才能完成工作。各自一直处于等待中...

 

Queue实现生产者与消费者模型

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading, time
import Queue   # 导入消息队列模块
import random  # 导入随机数模块,是为了模拟生产者与消费者速度不一致的情形

q = Queue.Queue()    # 实例化一个对象


def Producer(name):  # 生产者函数
    for i in range(10):
        q.put(i)     # 将结果放入消息队列中
        print '\033[32;1m生产者 %s 生产了一个包子[%s],总包子数:%s\033[0m' % (name, i, q.qsize())
        time.sleep(random.randrange(2))  # 生产者的生产速度,2s内


def Consumer(name):     # 消费者函数
    while True:
        data = q.get()  # 取用消息队列中存放的结果
        print '\033[31;1m消费者 %s 消费了一个包子[%s],包子剩余:%s\033[0m' % (name, data, q.qsize())
        time.sleep(random.randrange(1, 3))  # 消费者的消费速度,1-3s内


p = threading.Thread(target=Producer, args=('Milton',))
c = threading.Thread(target=Consumer, args=('Cherish',))

p.start()
c.start()

上例输出结果如下:

Queue是线程安全的,不需要加锁。如上截图箭头处“消费者 Cherish 消费了一个包子[4],包子剩余:4生产者 Milton 生产了一个包子[8],总包子数:4”

显然消费者 Cherish 消费了一个包子[4],包子剩余:4--》有朋友可能认为这里似乎包子剩余3才对,此处应该是出现了并发,在输出此语句前,生产者在队列中又生产了一个包子[8],所以吃完一个包子后,还是4,输出4是正确的。“生产者 Milton 生产了一个包子[8],总包子数:4” 此处生产包子[8]后,包子原本应该是5的,但是输出之前,又被Cherish消费了包子[4],所以输出4

同样由此处也可以看出,线程是安全的,在线程执行过程中,数据没有出错,包子没有多没有少,Milton总共生产了10个包子,Cherish总共吃了10个包子,最终剩下0个包子。

 

推荐阅读