Python多线程编程

1、线程和进程

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

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

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

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

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

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

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

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

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

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

2、多线程与多进程

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

3、Python多线程编程

3.1 单线程

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

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()
看了一下当前时间,差不多该睡觉了。
运行结果:

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()更应该被看作是音乐和视频播放器,至于要播放什么歌曲和视频应该由我们使用时决定。所以,我们对上面代码做了改造:

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())

运行结果:

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()函数来产生新线程。
先用一个实例感受一下:

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")

代码输出为:

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

注意到,在主线程写了:

while 1:
   pass

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

"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 创建线程,并指定执行的方法以及传递的参数:

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())

结果输出为:

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() 方法:

#!/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 ("退出主线程")

结果输出为:

开始线程: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()方法

我们修改一下代码:

#!/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 ("退出主线程")

结果就变为:

开始线程: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()方法的使用效果:

#!/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 ("退出主线程")

结果输出为:

开始线程: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 方法,所以程序会执行这一段。

    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的尴尬场面。
实例:

#!/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 ("退出主线程")

输出为:

开启线程: 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() 实际上意味着等到队列为空,再执行别的操作

#!/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 ("退出主线程")

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

开启线程: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
退出主线程

参考文章

进程与线程的一个简单解释:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
python 多线程就这么简单:
http://www.cnblogs.com/fnng/p/3670789.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 1、线程和进程 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。 假定工厂的电力有限,一...
    Andone1cc阅读 493评论 0 1
  • 来源:数据分析网Threading 模块从 Python 1.5.2 版开始出现,用于增强底层的多线程模块 thr...
    PyChina阅读 1,738评论 0 5
  • 线程 引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0....
    不浪漫的浪漫_ea03阅读 359评论 0 0
  • 引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0.1秒,...
    chen_000阅读 505评论 0 0
  • 今天室外的气温只有9度,小雨淅淅沥沥,这样阴雨寒冷的天,还是留不住啷当猪那颗想离巢的骚动的心。 中午的时候啷当猪就...
    云问雨阅读 999评论 3 9