前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文读懂Python多线程

一文读懂Python多线程

作者头像
石晓文
发布2018-04-11 15:23:53
8520
发布2018-04-11 15:23:53
举报
文章被收录于专栏:小小挖掘机小小挖掘机

1、线程和进程

计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。

假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。

进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

一个车间里,可以有很多工人。他们协同完成一个任务。

线程就好比车间里的工人。一个进程可以包括多个线程。

车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。 不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

2、多线程与多进程

从上面关于线程和进程的的通俗解释来看,多线程和多进程的含义如下: 多进程:允许多个任务同时进行 多线程:允许单个任务分成不同的部分运行

3、Python多线程编程

3.1 单线程

在好些年前的MS-DOS时代,操作系统处理问题都是单任务的,我想做听音乐和看电影两件事儿,那么一定要先排一下顺序。

代码语言:javascript
复制
from time import ctime,sleep

def music():
    for i in range(2):
        print "I was listening to music. %s" %ctime()
        sleep(1)

def move():
    for i in range(2):
        print "I was at the movies! %s" %ctime()
        sleep(5)

if __name__ == '__main__':
    music()
    move()
    print "all over %s" %ctime()

我们先听了一首音乐,通过for循环来控制音乐的播放了两次,每首音乐播放需要1秒钟,sleep()来控制音乐播放的时长。接着我们又看了一场电影, 每一场电影需要5秒钟,因为太好看了,所以我也通过for循环看两遍。在整个休闲娱乐活动结束后,我通过 print "all over %s" %ctime() 看了一下当前时间,差不多该睡觉了。 运行结果:

代码语言:javascript
复制
I was listening to music. Thu Apr 17 10:47:08 2014
I was listening to music. Thu Apr 17 10:47:09 2014
I was at the movies! Thu Apr 17 10:47:10 2014
I was at the movies! Thu Apr 17 10:47:15 2014
all over Thu Apr 17 10:47:20 2014

其实,music()和move()更应该被看作是音乐和视频播放器,至于要播放什么歌曲和视频应该由我们使用时决定。所以,我们对上面代码做了改造:

代码语言:javascript
复制
import threading
from time import ctime,sleep

def music(func):
    for i in range(2):
        print ("I was listening to %s. %s" %(func,ctime()))
        sleep(1)

def move(func):
    for i in range(2):
        print ("I was at the %s! %s" %(func,ctime()))
        sleep(5)


if __name__ == '__main__':
    music(u'爱情买卖')
    move(u'阿凡达')

    print ("all over %s" %ctime())

运行结果:

代码语言:javascript
复制
I was listening to 爱情买卖. Thu Apr 17 11:48:59 2014
I was listening to 爱情买卖. Thu Apr 17 11:49:00 2014
I was at the 阿凡达! Thu Apr 17 11:49:01 2014
I was at the 阿凡达! Thu Apr 17 11:49:06 2014
all over Thu Apr 17 11:49:11 2014

3.2 多线程

Python3 通过两个标准库 _thread (python2中是thread模块)和 threading 提供对线程的支持。 _thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。

3.2.1使用_thread模块

调用_thread模块中的start_new_thread()函数来产生新线程。 先用一个实例感受一下:

代码语言:javascript
复制
import _thread
import time
 
 
# 为线程定义一个函数
def print_time(threadName, delay):
    count = 0
    while count < 5:
        time.sleep(delay)
        count += 1
        print ("%s: %s" % (threadName, time.ctime(time.time())))
 
 
# 创建两个线程
try:
    _thread.start_new_thread(print_time, ("Thread-1", 2,))
    _thread.start_new_thread(print_time, ("Thread-2", 4,))
except:
    print ("Error: unable to start thread")
 
 
while 1:
   pass
 
print ("Main Finished")

代码输出为:

代码语言:javascript
复制
Thread-1: Thu Aug 10 16:35:47 2017
Thread-2: Thu Aug 10 16:35:49 2017
Thread-1: Thu Aug 10 16:35:49 2017
Thread-1: Thu Aug 10 16:35:51 2017
Thread-2: Thu Aug 10 16:35:53 2017
Thread-1: Thu Aug 10 16:35:53 2017
Thread-1: Thu Aug 10 16:35:55 2017
Thread-2: Thu Aug 10 16:35:57 2017
Thread-2: Thu Aug 10 16:36:01 2017

注意到,在主线程写了:

代码语言:javascript
复制
while 1:
   pass

这是让主线程一直在等待. 如果去掉上面两行,那就直接输出并结束程序执行:

代码语言:javascript
复制
"Main Finished"
3.2.2使用threading模块

threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法: threading.currentThread(): 返回当前的线程变量。 threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。 除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法: run(): 用以表示线程活动的方法。 start():启动线程活动。 join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。 isAlive(): 返回线程是否活动的。 getName(): 返回线程名。 setName(): 设置线程名。

直接创建线程

接上面的听音乐和看电影的例子,我们可以直接使用threading.Thread 创建线程,并指定执行的方法以及传递的参数:

代码语言:javascript
复制
import threading
from time import ctime,sleep


def music(func):
    for i in range(2):
        print ("I was listening to %s. %s" %(func,ctime()))
        sleep(1)

def move(func):
    for i in range(2):
        print ("I was at the %s! %s" %(func,ctime()))
        sleep(5)

threads = []
t1 = threading.Thread(target=music,args=(u'爱情买卖',))
threads.append(t1)
t2 = threading.Thread(target=move,args=(u'阿凡达',))
threads.append(t2)

if __name__ == '__main__':
    for t in threads:
        t.start()

    print ("all over %s" %ctime())

结果输出为:

代码语言:javascript
复制
I was listening to 爱情买卖. Thu Aug 10 16:57:12 2017
I was at the 阿凡达! Thu Aug 10 16:57:12 2017
all over Thu Aug 10 16:57:12 2017
I was listening to 爱情买卖. Thu Aug 10 16:57:13 2017
I was at the 阿凡达! Thu Aug 10 16:57:17 2017
构造线程类

我们也可以通过直接从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动新线程,即它调用了线程的 run() 方法:

代码语言:javascript
复制
#!/usr/bin/python3

import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("开始线程:" + self.name)
        print_time(self.name, self.counter, 5)
        print ("退出线程:" + self.name)

def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.start()
thread2.start()
print ("退出主线程")

结果输出为:

代码语言:javascript
复制
开始线程:Thread-1
开始线程:Thread-2
退出主线程
Thread-1: Thu Aug 10 16:48:41 2017
Thread-2: Thu Aug 10 16:48:42 2017
Thread-1: Thu Aug 10 16:48:42 2017
Thread-1: Thu Aug 10 16:48:43 2017
Thread-2: Thu Aug 10 16:48:44 2017
Thread-1: Thu Aug 10 16:48:44 2017
Thread-1: Thu Aug 10 16:48:45 2017
退出线程:Thread-1
Thread-2: Thu Aug 10 16:48:46 2017
Thread-2: Thu Aug 10 16:48:48 2017
Thread-2: Thu Aug 10 16:48:50 2017
退出线程:Thread-2

从结果可以看到,为什么我们开启了两个线程之后,主线程立即退出了?因为我们没有使用join方法,对于主线程来说,thread1和thread2是子线程,使用join方法,会让主线程等待子线程执行解说再继续执行。

join()方法

我们修改一下代码:

代码语言:javascript
复制
#!/usr/bin/python3

import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("开始线程:" + self.name)
        print_time(self.name, self.counter, 5)
        print ("退出线程:" + self.name)

def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("退出主线程")

结果就变为:

代码语言:javascript
复制
开始线程:Thread-1
开始线程:Thread-2
Thread-1: Thu Aug 10 16:52:07 2017
Thread-2: Thu Aug 10 16:52:08 2017
Thread-1: Thu Aug 10 16:52:08 2017
Thread-1: Thu Aug 10 16:52:09 2017
Thread-2: Thu Aug 10 16:52:10 2017
Thread-1: Thu Aug 10 16:52:10 2017
Thread-1: Thu Aug 10 16:52:11 2017
退出线程:Thread-1
Thread-2: Thu Aug 10 16:52:12 2017
Thread-2: Thu Aug 10 16:52:14 2017
Thread-2: Thu Aug 10 16:52:16 2017
退出线程:Thread-2
退出主线程

可以看到 退出主线程 在最后才被打印出来。

setDaemon()方法

有一个方法常常拿来与join方法做比较,那就是setDaemon()方法。我们首先来看一下setDaemon()方法的使用效果:

代码语言:javascript
复制
#!/usr/bin/python3

import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("开始线程:" + self.name)
        print_time(self.name, self.counter, 5)
        print ("退出线程:" + self.name)

def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.setDaemon(True)
thread2.setDaemon(True)
thread1.start()
thread2.start()

print ("退出主线程")

结果输出为:

代码语言:javascript
复制
开始线程:Thread-1
开始线程:Thread-2
退出主线程

可以看到,在主线程结束之后,程序就终止了,也就是说两个子线程也被终止了,这就是setDaemon方法的作用。主线程A中,创建了子线程B,并且在主线程A中调用了B.setDaemon(),这个的意思是,把主线程A设置为守护线程,这时候,要是主线程A执行结束了,就不管子线程B是否完成,一并和主线程A退出.这就是setDaemon方法的含义,这基本和join是相反的。此外,还有个要特别注意的:必须在start() 方法调用之前设置,如果不设置为守护线程,程序会被无限挂起。

两个疑问

我们刚才介绍了两种使用多线程的方式,一种是直接调用threading.Thread 创建线程,另一种是从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动进程。学到这里,我就抛出了两个疑问,为什么第一种方法中我们可以为不同的线程指定运行的方法,而第二种我们都运行的是同一个方法,那么它内部的实现机制是什么呢?第二个疑问是,第二种方法中,我们没有实例化start()方法,那么run和start这两个方法的联系是什么呢? 首先,start方法和run方法的关系如下:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

而run()方法的源码如下,可以看到,如果我们指定了target即线程执行的函数的话,run方法可以转而调用那个函数,如果没有的话,将不执行,而我们在自定义的Thread类里面重写了这个run 方法,所以程序会执行这一段。

代码语言:javascript
复制
    def run(self):
        """Method representing the thread's activity.

        You may override this method in a subclass. The standard run() method
        invokes the callable object passed to the object's constructor as the
        target argument, if any, with sequential and keyword arguments taken
        from the args and kwargs arguments, respectively.

        """
        try:
            if self._target:
                self._target(*self._args, **self._kwargs)
        finally:
            # Avoid a refcycle if the thread is running a function with
            # an argument that has a member that points to the thread.
            del self._target, self._args, self._kwargs
线程同步

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。 使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。如下: 多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。 考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。 那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。 锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。 经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。 实例:

代码语言:javascript
复制
#!/usr/bin/python3

import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("开启线程: " + self.name)
        # 获取锁,用于线程同步
        threadLock.acquire()
        print_time(self.name, self.counter, 3)
        # 释放锁,开启下一个线程
        threadLock.release()

def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

threadLock = threading.Lock()
threads = []

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.start()
thread2.start()

# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有线程完成
for t in threads:
    t.join()
print ("退出主线程")

输出为:

代码语言:javascript
复制
开启线程: Thread-1
开启线程: Thread-2
Thread-1: Thu Aug 10 20:45:59 2017
Thread-1: Thu Aug 10 20:46:00 2017
Thread-1: Thu Aug 10 20:46:01 2017
Thread-2: Thu Aug 10 20:46:03 2017
Thread-2: Thu Aug 10 20:46:05 2017
Thread-2: Thu Aug 10 20:46:07 2017
退出主线程
线程优先级队列( Queue)

Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。 这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。 Queue 模块中的常用方法: Queue.qsize() 返回队列的大小 Queue.empty() 如果队列为空,返回True,反之False Queue.full() 如果队列满了,返回True,反之False Queue.full 与 maxsize 大小对应 Queue.get([block[, timeout]])获取队列,timeout等待时间 Queue.get_nowait() 相当Queue.get(False) Queue.put(item) 写入队列,timeout等待时间 Queue.put_nowait(item) 相当Queue.put(item, False) Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号 Queue.join() 实际上意味着等到队列为空,再执行别的操作

代码语言:javascript
复制
#!/usr/bin/python3

import queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q
    def run(self):
        print ("开启线程:" + self.name)
        process_data(self.name, self.q)
        print ("退出线程:" + self.name)

def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print ("%s processing %s" % (threadName, data))
        else:
            queueLock.release()
        time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1

# 创建新线程
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1

# 填充队列
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()

# 等待队列清空
while not workQueue.empty():
    pass

# 通知线程是时候退出
exitFlag = 1

# 等待所有线程完成
for t in threads:
    t.join()
print ("退出主线程")

上面的代码每次执行的结果是不一样的,取决于哪个进程先获得锁,一次运行的输出如下:

代码语言:javascript
复制
开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-3
Thread-2 processing One
Thread-3 processing Two
Thread-1 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出线程:Thread-3
退出线程:Thread-2
退出线程:Thread-1
退出主线程
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小小挖掘机 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、线程和进程
  • 2、多线程与多进程
  • 3、Python多线程编程
    • 3.1 单线程
      • 3.2 多线程
        • 3.2.1使用_thread模块
        • 3.2.2使用threading模块
        • 直接创建线程
        • 构造线程类
        • join()方法
        • setDaemon()方法
        • 两个疑问
        • 线程同步
        • 线程优先级队列( Queue)
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档