python线程

多任务可以由多进程完成,也可以由一个进程内的多线程完成。
我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行
以下是单线程的例子:

import time
import threading


def saySorry():
    print("亲爱的,我错了,我能吃饭了吗?")
    time.sleep(1)


if __name__ == "__main__":
    for i in range(5):
        #打印当前线程的名字
        print(threading.current_thread().name)
        saySorry()

结果如下:



这全部都是main线程执行的,运行时间比较慢。接下来采用多线程进行上面的操作。

import threading
import time


def saySorry():
    print(threading.current_thread().name)
    print("亲爱的,我错了,我能吃饭了吗?")
    time.sleep(1)


if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=saySorry)
        t.start()  # 启动线程,即让线程开始执行

结果如下:



可以明显看出使用了多线程并发的操作,花费时间要短很多
创建好的线程,需要调用start()方法来启动。主线程会等待所有的子线程运行结束才会结束。

import threading
import time

def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        time.sleep(1)

def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        time.sleep(1)

if __name__ == '__main__':
    print('---开始---:%s'%time.ctime())

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    #time.sleep(5)
    print('---结束---:%s'%time.ctime())

结果如下:



使用threading.enumerate可以查看线程数量。

import threading
import time

def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        time.sleep(1)

def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        time.sleep(1)

if __name__ == '__main__':
    print('---开始---:%s'%time.ctime())

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    while True:
        length = len(threading.enumerate())
        print('当前运行的线程数为:%d'%length)
        if length<=1:
            break

        time.sleep(0.5)

结果如下:



从这个例子也可以看出主线程会等待所有的子线程运行结束。
根据我们对进程的学习,线程同样可以通过继承的方法创建。

import threading
import time

class MyThread(threading.Thread):

    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
            print(msg)


if __name__ == '__main__':
    t = MyThread()
    t.start()

结果如下:



跟进程一样,可以在创建线程的时候进行参数的传递。比如修改线程的名字,代码如下:

import threading
import time

class MyThread(threading.Thread):
    def __init__(self,name):
        super().__init__()
        self.name = name

    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
            print(msg)


if __name__ == '__main__':
    t = MyThread('xx')
    t.start()

结果如下:



python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。
关于线程的执行顺序,看以下代码:

import threading
import time

class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i)
            print(msg)
def test():
    for i in range(5):
        t = MyThread()
        t.start()
if __name__ == '__main__':
    test()

结果如下:



从代码和执行结果我们可以看出,多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、run函数中每次循环的执行顺序都不能确定。
从以上例子我们可以总结出:每个线程一定会有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。当线程的run()方法结束时该线程完成。无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
线程的几种状态



线程在运行时具有三种基本状态:
1、执行状态,表示线程已获得处理机而正在运行;

2、就绪状态,指线程已具备了各种执行条件,只须再获得CPU便可立即执行;
3、阻塞状态:指线程在执行中因某事件受阻而处于暂停状态,例如,当一个线程执行从键盘读入数据的系统调用时,该线程就被阻塞。
在学习进程的时候,我们知道,进程的全局变量是不共享的,每个进程各运行各的,互不影响,也就是说进程无法修改全局变量。那线程全局变量是否可以共享、修改呢?看如下代码:

import threading
import time

# 全局变量
g_num = 100


# 修改全局变量
def work1():
    # 如果想修改全局变量,必须加global
    global g_num
    for i in range(3):
        g_num += 1

    print("----in work1, g_num is %d---" % g_num)


# 获取全局变量
def work2():
    global g_num
    print("----in work2, g_num is %d---" % g_num)


# 将功能包装成函数方法
def main():
    print("---线程创建之前g_num is %d---" % g_num)
    # 创建一个线程
    t1 = threading.Thread(target=work1)
    # 就绪
    t1.start()

    # 延时一会,保证t1线程中的事情做完
    time.sleep(1)

    t2 = threading.Thread(target=work2)
    t2.start()


if __name__ == '__main__':
    # 调用方法
    main()

结果如下:


线程修改全局变量.jpg

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。来看看多个线程同时操作一个变量怎么把内容给改乱了:

import threading
import time

# 全局变量
g_num = 0


# 函数1
def test1():
    global  g_num
    for i in range(1000000):
        g_num = g_num+1
    print("---test1---g_num=%d" % g_num)

# 函数2
def test2():
    global g_num
    for i in range(1000000):
        g_num = g_num + 1
    print("---test2---g_num=%d" % g_num)


def main():
    #创建线程
    t1 = threading.Thread(target=test1)
    #就绪
    t1.start()

    # 创建线程
    t2 = threading.Thread(target=test2)
    # 就绪
    t2.start()


    time.sleep(5)
    print("---main---g_num=%d" % g_num)

if __name__ == '__main__':
    main()

结果如下:

多线程共享全局变量的错误.jpg

多线程公用全局变量可能存在的问题假设两个线程t1和t2都要对g_num=0进行增1运算,t1和t2都各对g_num修改1000000次,g_num的最终的结果应该为2000000。
为什么会出现这样的结果,原因在于高级语言的一条语句在CPU执行时是若干条语句,即使是这么简单的一个计算:g_num = g_num+1也会分成两部:
计算g_num+1,存入临时变量中;
将临时变量的值赋给g_num。
可以看成是:
temp = g_num+1
g_num = temp
由于temp是局部变量,两个线程各自都有自己的temp。按理想状态下,代码是这么运行的:

初始值 g_num = 0

t1: temp = g_num + 1 # temp = 0 + 1 = 1
t1: g_num = temp     # g_num = 1

t2: temp = g_num + 1 # temp = 1 + 1 = 2
t2: g_num = temp     # g_num = 2

就这样一直循环,最后将得到2000000的结果。
但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:

初始值 g_num = 0
t1: temp = g_num + 1 # temp = 0 + 1 = 1
t2: temp = g_num + 1 # temp = 0 + 1 = 1
t2: g_num = temp     # g_num = 1
t1: g_num = temp     # g_num = 1

结果就是我们刚才看到的,这就是由于多线程访问,有可能出现下面情况:在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0。然后t2对得到的值进行加1并赋给g_num,使得g_num=1。然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。这样,明明t1和t2都完成了1次加1工作,但结果仍然是g_num=1。
问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。
究其原因,是因为修改g_num需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。所以,我们必须确保一个线程在修改g_num的时候,别的线程一定不能改。系统调用t1,然后获取到num的值为0,此时上一把锁,即不允许其他线程操作num,对num的值进行+1,解锁。此时num的值为1,其他的线程就可以使用num了,而且是num的值不是0而是1。同理其他线程在对num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性。

import threading

# 全局变量
g_num = 0
myLock = threading.Lock()


# 函数1
def test1():
    global  g_num
    # 上锁
    myLock.acquire()
    print('test1...上锁...')
    for i in range(1000000):
        g_num = g_num+1
    # 开锁
    myLock.release()
    print('test1...开锁...')
    print("---test1---g_num=%d" % g_num)

# 函数2
def test2():
    print('test2...')
    '''
    print('g_num:%s'%g_num)
    如使用这句话将报错SyntaxWarning: name 'g_num' is used
    prior to global declaration global g_num
    '''
    global g_num
    myLock.acquire()
    print('test2...上锁...')
    print('g_num:%s'%g_num)
    for i in range(1000000):
        g_num = g_num + 1
        # 开锁
    myLock.release()
    print('test2...开锁...')
    print("---test2---g_num=%d" % g_num)


def main():
    #创建线程
    t1 = threading.Thread(target=test1)
    #就绪
    t1.start()


    # 创建线程
    t2 = threading.Thread(target=test2)
    # 就绪
    t2.start()
    print("---main---g_num=%d" % g_num)

if __name__ == '__main__':
    main()

结果如下:


线程上锁.jpg

上锁是为了保护线程访问数据的唯一性,并不妨碍上锁后,cpu对线程的切换,当上锁的代码段执行过程中,cpu切换走,其他线程如果是上锁等待,则cpu继续切换,如果cpu切换的其他线程不是上锁等待,则其他线程依然可执行。cpu的线程切换是不固定的。并不是一定要等待上锁的程序走完。这也就解释了为什么打印test1...上锁...后又打印了test2...。
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
从刚才的例子中可以看出对于全局变量,在多线程中要格外小心,否则容易造成数据错乱的情况发生。那对于非全局变量是否需要加锁呢?看如下代码:

import threading
import time


class MyThread(threading.Thread):
    # 重写 构造方法
    def __init__(self, num, sleepTime):
        # 重写父类的__init__()方法
        threading.Thread.__init__(self)
        self.num = num
        self.sleepTime = sleepTime

    def run(self):
        self.num += 1
        time.sleep(self.sleepTime)
        print('线程(%s),num=%d' % (self.name, self.num))


if __name__ == '__main__':
    mutex = threading.Lock()
    t1 = MyThread(100, 5)
    t1.start()
    t2 = MyThread(200, 1)
    t2.start()

结果如下:


多线程局部变量.jpg

再观察如下代码:

import threading
import time


def test(sleepTime):
    num = 1
    time.sleep(sleepTime)
    num += 1
    print('---(%s)--num=%d' % (threading.current_thread(), num))


t1 = threading.Thread(target=test, args=(5,))
t2 = threading.Thread(target=test, args=(1,))

t1.start()
t2.start()

结果如下:


多线程局部变量2.jpg

也就是说在多线程开发中,全局变量是多个线程都共享的数据,而局部变量等是各自线程的,是非共享的。
在刚才讲锁的时候说到了死锁,那么什么是死锁呢?在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子:

import threading
import time


class MyThread1(threading.Thread):
    def run(self):
        if mutexA.acquire():
            print(self.name + '----do1---up----')
            time.sleep(1)

            if mutexB.acquire():
                print(self.name + '----do1---down----')
                mutexB.release()
            mutexA.release()


class MyThread2(threading.Thread):
    def run(self):
        if mutexB.acquire():
            print(self.name + '----do2---up----')
            time.sleep(1)
            if mutexA.acquire():
                print(self.name + '----do2---down----')
                mutexA.release()
            mutexB.release()


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

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

结果如下:


死锁.jpg

死锁原理.jpg

那我们如何避免死锁呢?常用的是在程序设计时要尽量避免(比如采用银行家算法),要么添加超时时间等。
有了以上的知识,就可以让线程按我们的需求的顺序执行:

import threading
import time


class Task1(threading.Thread):
    def run(self):
        while True:
            if lock1.acquire():
                print("------Task 1 -----")
                time.sleep(0.5)
                lock2.release()


class Task2(threading.Thread):
    def run(self):
        while True:
            if lock2.acquire():
                print("------Task 2 -----")
                time.sleep(0.5)
                lock3.release()


class Task3(threading.Thread):
    def run(self):
        while True:
            if lock3.acquire():
                print("------Task 3 -----")
                time.sleep(0.5)
                lock1.release()


# 使用Lock创建出的锁默认没有“锁上”
lock1 = threading.Lock()
# 创建另外一把锁,并且“锁上”
lock2 = threading.Lock()
lock2.acquire()
# 创建另外一把锁,并且“锁上”
lock3 = threading.Lock()
lock3.acquire()

t1 = Task1()
t2 = Task2()
t3 = Task3()

t1.start()
t2.start()
t3.start()

结果如下:


进程同步应用.jpg

也就是说我们可以可以使用互斥锁完成多个任务,有序的进程工作,这就是线程的同步。接下来,我们模拟一下车站的卖票:

import threading
import time
import os


def doChore():  # 作为间隔  每次调用间隔0.5s
    time.sleep(0.5)


def booth(tid):
    global i
    global lock
    while True:
        lock.acquire()  # 得到一个锁,锁定
        if i != 0:
            i = i - 1  # 售票 售出一张减少一张
            print(tid, ':now left:', i)  # 剩下的票数
            doChore()
        else:
            print("Thread_id", tid, " No more tickets")
            os._exit(0)  # 票售完   退出程序
        lock.release()  # 释放锁
        doChore()


# 全局变量
i = 15  # 初始化票数
lock = threading.Lock()  # 创建锁


def main():
    # 总共设置了3个线程
    for k in range(3):
        # 创建线程; Python使用threading.Thread对象来代表线程
        new_thread = threading.Thread(target=booth, args=(k,))
        # 调用start()方法启动线程
        new_thread.start()


if __name__ == '__main__':
    main()

结果如下:


模拟卖票.jpg

以上就是关于线程同步的总结。
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
用FIFO队列实现上述生产者与消费者问题的代码如下:

import threading
import time
import queue


class Producer(threading.Thread):
    def run(self):
        global queue
        count = 0
        while True:
            if queue.qsize() < 1000:
                for i in range(100):
                    count = count + 1
                    msg = '生成产品' + str(count)
                    queue.put(msg)
                    print(msg)
            time.sleep(0.5)


class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            if queue.qsize() > 100:
                for i in range(3):
                    msg = self.name + '消费了 ' + queue.get()
                    print(msg)
            time.sleep(1)


if __name__ == '__main__':
    queue = queue.Queue()

    for i in range(500):
        queue.put('初始产品' + str(i))
    for i in range(2):
        p = Producer()
        p.start()
    for i in range(5):
        c = Consumer()
        c.start()

结果如下:


生产者消费者.jpg

为什么要使用生产者和消费者模式?
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式?
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦。

  1. Queue的说明
    对于Queue,在多线程通信之间扮演重要的角色
    1、添加数据到队列中,使用put()方法
    2、从队列中取数据,使用get()方法
    3、判断队列中是否还有数据,使用qsize()方法
    在多线程中,如果使用全局变量,会出问题,一旦一个线程修改了,会影响其它线程。如果使用局部变量,一旦想让这个局部变量,一直使用下去,一直传参,比较麻烦。既不想使用全局变量,又一直想用这个局部变量。可以将这个局部变量绑定到对应的线程中,以后此线程一直用个。与其它线程中的无关。
    尝试用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象:
import threading
# 创建字典对象
myDict = {}
def process_student():
    # 获取当前线程关联的student
    std = myDict[threading.current_thread()]
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
    # 绑定ThreadLocal的student
    myDict[threading.current_thread()] = name
    process_student()
t1 = threading.Thread(target=process_thread, args=('yongGe',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()

结果如下:


局部变量传参.jpg

这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点low。看下面的代码:

import threading
# 创建全局ThreadLocal对象
local_school = threading.local()
def process_student():
    # 获取当前线程关联的student
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
    # 绑定ThreadLocal的student
    local_school.student = name
    process_student()
t1 = threading.Thread(target=process_thread, args=('yongGe',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()

结果如下:


局部变量传参local.jpg

全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
异步

from multiprocessing import Pool
import time
import os
def test():
    print("---进程池中的进程---pid=%d,ppid=%d--" % (os.getpid(), os.getppid()))
    for i in range(3):
        print("----%d---" % i)
    return "老王"
def test2(args):
    print('1...')
    print("---callback func--pid=%d" % os.getpid())
    print("---callback func--args=%s" % args)
    print('2...')
if __name__ == '__main__':
    pool = Pool(3)
    # callback表示前面的func方法执行完,再执行callback,并且可以获取func的返回值作为callback的参数
    pool.apply_async(func=test, callback=test2)
    # pool.apply_async(func=test)

    # 模拟主进程在做任务
    time.sleep(5)

    print("----主进程-pid=%d.....----" % os.getpid())

结果如下:

异步1.jpg

可以看到执行callback的是主进程。也就是说当主进程在做任务时,进程又分出了一个线程去做其他任务。
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。

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

推荐阅读更多精彩内容

  • 1.进程和线程 队列:1、进程之间的通信: q = multiprocessing.Queue()2、...
    一只写程序的猿阅读 1,063评论 0 17
  • 线程状态新建,就绪,运行,阻塞,死亡。 线程同步多线程可以同时运行多个任务,线程需要共享数据的时候,可能出现数据不...
    KevinCool阅读 746评论 0 0
  • 线程 1.同步概念 1.多线程开发可能遇到的问题 同步不是一起的意思,是协同步调 假设两个线程t1和t2都要对nu...
    TENG书阅读 578评论 0 1
  • 1.线程 Python中使用线程有两种方式:函数或者用类来包装线程对象。 1.函数式:调用thread模块中的st...
    一只写程序的猿阅读 945评论 0 1
  • 十几年前春梅的老公晚上在家刚刚洗完脚,做沙发上就再也没起来,心梗!料理后事的时候,发现他身上有一串钥匙,也不是家...
    蓝夜伊梦阅读 195评论 0 0