Python 多线程 threading


img

中文文档


一个进程包含一个或多个线程

Thread

Thread 类表示在单独的控制线程中运行的活动。有两种方法来指定活动:通过将可调用对象传递给构造函数,或者通过重写子类中的 run() 方法。


普通

import time


def show():
    print("咕嘿嘿~")
    time.sleep(1)


if __name__ == '__main__':
    sr = time.time()
    for i in range(5):
        show()
    ed = time.time()
    zw = ed - sr
    print("耗时 %.3f" % zw)



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 05-线程-threading.py 
咕嘿嘿~
咕嘿嘿~
咕嘿嘿~
咕嘿嘿~
咕嘿嘿~
耗时 5.006

加入线程

import threading
import time


def show():
    print("咕嘿嘿~")
    time.sleep(1)


if __name__ == '__main__':
    sr = time.time()
    for i in range(5):  # 5 个线程
        t = threading.Thread(target=show)
        t.start()
    ed = time.time()
    zw = ed - sr
    print("耗时 %.3f" % zw)



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 05-线程-threading.py 
咕嘿嘿~
咕嘿嘿~
咕嘿嘿~
咕嘿嘿~
咕嘿嘿~
耗时 0.002

主线程等待所有子线程结束后才停止运行,emmm,是为了给子线程收尸

如果多个线程执行的都是同一个函数,各自之间是不会有影响的,各用各地。


Thread子类

如果子类覆盖了构造函数,它必须在对线程做任何其他事情之前调用基类构造函数(Thread.__init__()

import threading
import time


class NewThread(threading.Thread):  # 与创建多进程的子类差不多

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

    def run(self):
        print("咕嘿嘿~%s" % self.name)
        time.sleep(1)


if __name__ == '__main__':
    sr = time.time()
    for i in range(4):
        t = NewThread()
        t.start()
    ed = time.time()
    zw = ed - sr
    print("共耗时: %.3f" % zw)



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 06-线程-threading-子类.py 
咕嘿嘿~Thread-1
咕嘿嘿~Thread-2
咕嘿嘿~Thread-3
咕嘿嘿~Thread-4
共耗时: 0.001


共享全局变量

进程中的资源数据都是互不影响的,线程之间共享全局变量

import threading
import time


num = 10


def addNum():
    global num
    num += 1
    print("线程1--num = %d" % num)

def showNum():
    global num
    print("线程2--num = %d" % num)


if __name__ == '__main__':
    print("线程创建前:num = %d" % num)
    t1 = threading.Thread(target=addNum)
    t1.start()
    time.sleep(1)
    t2 = threading.Thread(target=showNum)
    t2.start()



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 07-线程-全局变量共享.py 
线程创建前:num = 10
线程1--num = 11
线程2--num = 11

线程都在同一个进程里,全局变量也在这个进程里,所以同一进程里的线程之间共享全局变量

所以就没有进程间的通信那么麻烦,都是共享的

线程共享全局变量的问题

import threading
import time


num = 0


def addNum():
    global num
    for i in range(100000):
        num += 1
    print("线程1--num = %d" % num)

def showNum():
    global num
    for i in range(100000):
        num += 1
    print("线程2--num = %d" % num)


if __name__ == '__main__':
    print("线程创建前:num = %d" % num)
    t1 = threading.Thread(target=addNum)
    t1.start()
    # time.sleep(1)  # 注释掉
    t2 = threading.Thread(target=showNum)
    t2.start()



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 08-线程-全局变量共享的问题.py 
线程创建前:num = 0
线程1--num = 138376
线程2--num = 150962

两个线程都加 100000 理应出现一个 200000,但却一个也没有

将列表当做参数传递
import threading
import time


num = 0


def addNum(nums):
    nums.append(4)
    print("线程1--num = ", nums)

def showNum(nums):
    time.sleep(1) # 等待1s,保证线程1完成
    print("线程2--num = ", nums)


if __name__ == '__main__':
    num = [1, 2, 3]
    t1 = threading.Thread(target=addNum, args=(num,))
    t1.start()

    t2 = threading.Thread(target=showNum, args=(num,))
    t2.start()



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 09-线程-列表传递.py 
线程1--num =  [1, 2, 3, 4]
线程2--num =  [1, 2, 3, 4]

在使用多线程的时候,数据共享是方便,但要确定结果是不是自己想要的


为什么上一个程序加了两次 100000,并没有得到 200000

操作系统不一定会将一条语句彻底执行完在执行下一条: num += 1,可能先执行完线程一+=1后,再执行线程二+=1,再执行线程一num = 1,再线程二num = 1,那么全局变量就还是 1,得不到预想的结果

解决方法1 -- 轮询
import threading
import time


num = 0
flage = 1

def addNum():
    global num
    global flage
    if flage == 1:
        for i in range(100000):
            num += 1
    flage = 0
    print("线程1--num = %d" % num)

def showNum():
    global num
    while True:
        if flage == 0:
            for i in range(100000):
                num += 1
            break
    print("线程2--num = %d" % num)


if __name__ == '__main__':
    print("线程创建前:num = %d" % num)
    t1 = threading.Thread(target=addNum)
    t1.start()
    t2 = threading.Thread(target=showNum)
    t2.start()


root@H2o2:~/文档/PycharmProjects/进程与线程# python3 10-线程-解决全局变量共享问题.py 
线程创建前:num = 0
线程1--num = 100000
线程2--num = 200000

虽然能解决,但是效率不高

解决方法2 -- 互斥锁
Lock

当多个线程几乎同时修改某一个共享数据时,需要进行同步控制,最简单的同步机制就是互斥锁(lock)。

from threading import Thread, Lock
import time


num = 0


def addNum():
    global num
    lk.acquire() # 上锁
    for i in range(100000):
        num += 1
    lk.release() # 解锁

    print("线程1--num = %d" % num)


def showNum():
    global num
    lk.acquire()  # 上锁
    for i in range(100000):
        num += 1
    lk.release() # 解锁

    print("线程2--num = %d" % num)


if __name__ == '__main__':
    print("线程创建前:num = %d" % num)
    lk = Lock() # 创建锁
    t1 = Thread(target=addNum)
    t1.start()
    t2 = Thread(target=showNum)
    t2.start()



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 11-线程-解决全局变量共享问题-互斥锁.py 
线程创建前:num = 0
线程1--num = 100000
线程2--num = 200000

Lock 默认是没有上锁的

无论哪一个线程先上锁,另一个便无法上锁,进行阻塞,等待解锁

Ps:在不修改只读取全局变量的时候,是不需要加锁的


非共享数据(局部)

import threading
import time


class NewClass(threading.Thread):

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

    def run(self):
        num = 10
        print("当前是 %s 在执行" % self.name)
        if self.name == "Thread-1":
            num += 1
        else:
            time.sleep(2)
        print("线程:%s ,num = %d" % (self.name, num))


if __name__ == '__main__':
    lk = threading.Lock()
    t1 = NewClass()
    t1.start()
    t2 = NewClass()
    t2.start()



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 12-线程-非共享数据.py 
当前是 Thread-1 在执行
线程:Thread-1 ,num = 11
当前是 Thread-2 在执行
线程:Thread-2 ,num = 10

Thread-2的值并没有被改变,说明 不同线程执行同一函数时,其内的所有数据是独有的

不同线程间的全局变量是共享的,局部变量是私有的

各线程间的非共享数据是独立的,也就不需要加锁了


死锁

在线程共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁

import threading
import time


class NewClass(threading.Thread):

    def run(self):
        if lkA.acquire():  # 默认为开锁,正常执行后上锁
            print(self.name + "---do1--up---")
            time.sleep(1)
        
            if lkB.acquire():   # 等待 lkB 解锁
                print(self.name + "---do2--down---")
                lkB.release()
            lkA.release()

class NewClassTwo(threading.Thread):

    def run(self):
        if lkB.acquire():    # 默认为开锁,正常执行后上锁 
            print(self.name + "---do2--up---")
            time.sleep(1)

            if lkA.acquire(): # 等待 lkA 解锁
                print(self.name + "---do2--down---")
                lkA.release()
            lkB.release()




if __name__ == '__main__':
    lkA = threading.Lock()
    lkB = threading.Lock()

    t1 = NewClass()
    t2 = NewClassTwo()
    t1.start()
    t2.start()



root@H2o2:~/文档/PycharmProjects/进程与线程# python3 13-死锁.py 
Thread-1---do1--up---
Thread-2---do1--up---
|                            # 持续等待

lkA在等待 lkB 解锁,lkB又在等待 lkA 解锁,相互僵持,形成死锁

避免死锁

  1. 程序设计尽量避开(银行家算法)
  2. 上锁时添加超时时间 acquire(blocking=True, timeout=-1)

同步

同步就是协调步调,按预定的先后次序进行运行。如:你先说,我再说

这个不是一起执行,而是协同、协助、相互配合。如进程、线程同步,可理解为进程或线程A与B一块配合,A执行的一定程度时依靠B的某个结果,于是停下来,示意B运行,再将结果给A,A继续运行

import threading
import time


class NewClassT1(threading.Thread):

    def run(self):
        while True:
            if lkA.acquire():
                print("---AAA---")
                time.sleep(1)
                lkB.release()

class NewClassT2(threading.Thread):

    def run(self):
        while True:
            if lkB.acquire():
                print("---BBB---")
                time.sleep(1)
                lkC.release()

class NewClassT3(threading.Thread):

    def run(self):
        while True:
            if lkC.acquire():
                print("---CCC---")
                time.sleep(1)
                lkA.release()




if __name__ == '__main__':
    lkA = threading.Lock()
    lkB = threading.Lock()
    lkB.acquire()
    lkC = threading.Lock()
    lkC.acquire()

    t1 = NewClassT1()
    t2 = NewClassT2()
    t3 = NewClassT3()
    t1.start()
    t2.start()
    t3.start()




root@H2o2:~/文档/PycharmProjects/进程与线程# python3 14-同步的应用.py 
---AAA---
---BBB---
---CCC---
---AAA---
---BBB---
---CCC---
---AAA---
---BBB---
---CCC---
---AAA---
---BBB---
...


生产者与消费者模式(数据生产与数据处理)

在做爬虫的时候,会遇到爬去的数据跟不上处理速度或处理速度跟不上爬取速度,会堆积在那里,所造成一些问题,就需要在爬取与处理间放一个缓存来解决,可以用Queue来解决

Queue

python2 导入方式: from Queue import Queue

python3 导入方式: from queue import Queue

import threading
import time
from queue import Queue


class NewClassT1(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 NewClassT2(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()

    for i in range(500):
        queue.put("初始添加数据:" + str(i))
    for i in range(2):
        t1 = NewClassT1()
        t1.start()
    for i in range(5):
        t2 = NewClassT2()
        t2.start()


root@H2o2:~/文档/PycharmProjects/进程与线程# python3 15-生产者与消费者模式.py 
生成了:1
生成了:2
生成了:3
生成了:4
生成了:5
生成了:6
生成了:1
Thread-3处理了:初始添加数据:0
Thread-3处理了:初始添加数据:3
Thread-5处理了:初始添加数据:2
Thread-4处理了:初始添加数据:1
生成了:2
生成了:7
Thread-6处理了:初始添加数据:6
Thread-4处理了:初始添加数据:7
生成了:8
生成了:9
Thread-6处理了:初始添加数据:8
生成了:3
生成了:4
Thread-3处理了:初始添加数据:4
Thread-5处理了:初始添加数据:5
Thread-5处理了:初始添加数据:12
Thread-6处理了:初始添加数据:11
生成了:10
Thread-4处理了:初始添加数据:9
Thread-7处理了:初始添加数据:10
生成了:11
Thread-7处理了:初始添加数据:13
生成了:12
Thread-7处理了:初始添加数据:14
生成了:13
生成了:14
生成了:5
生成了:15
生成了:6
生成了:16
...


ThreadLocal

多个线程访问同一个函数,函数内(局部)的数据是独立互不影响的

import threading
import time


def run():
    num = 100
    num += 1
    print(num)



if __name__ =='__main__':
    t1 = threading.Thread(target=run)
    t2 = threading.Thread(target=run)
    t1.start()
    time.sleep(2)
    t2.start()


h2o2@h2o2-PC:~/Documents/01-工程文件/PycharmProjects/04-进程与线程$ python3 16-线程-ThreadLocal对象.py 
101
101

A函数要得到B函数的值,要么返回值,要么全局变量,在线程中,两种方式要么用不了,要么太枯燥麻烦。这时候可以用全局字典

import threading
import time


global_dict = {}

def run_th(num):
    global_dict[threading.current_thread()] = num
    cc = global_dict[threading.current_thread()]
    print("线程:%s -- 值:%s" % (threading.current_thread().name, cc))
    do_run_1()
    do_run_2()



def do_run_1():
    th = threading.current_thread().name
    vul_1 = global_dict[threading.current_thread()]
    print("当前线程是:%s -- 值:%s" % (th, vul_1))


def do_run_2():
    th = threading.current_thread().name
    vul_1 = global_dict[threading.current_thread()]
    print("当前线程是:%s -- 值:%s" % (th, vul_1))


if __name__ == '__main__':
    t1 = threading.Thread(target=run_th, args=(1,))
    t2 = threading.Thread(target=run_th, args=(2,))
    t1.start()
    # time.sleep(2)
    t2.start()




h2o2@h2o2-PC:~/Documents/01-工程文件/PycharmProjects/04-进程与线程$ python3 17-线程-全局字典传值.py 
线程:Thread-1 -- 值:1
当前线程是:Thread-1 -- 值:1
当前线程是:Thread-1 -- 值:1
线程:Thread-2 -- 值:2
当前线程是:Thread-2 -- 值:2
当前线程是:Thread-2 -- 值:2


全局字典也麻烦,不太合适..最好、最简单的方法还是ThreadLocal

Thread-local数据是其值是线程特定的数据。

无论有多少线程,threadlocal的值都是各自线程的值,不会因为下一个线程将threadlocal值修改后而改变上一个线程的threadlocal

import threading


def do_run_1():
    st = local_1.number
    print("线程:%s -- 值:%s" % (threading.current_thread().name, st))



def run_th(num):
    local_1.number = num  # 绑定local_1 的属性number 的值 num
    do_run_1()




if __name__ == '__main__':
    # 创建全局threadlocal 对象
    local_1 = threading.local()
    t1 = threading.Thread(target=run_th, args=(1,), name="T-A")
    t2 = threading.Thread(target=run_th, args=(2,), name="T-B")
    t1.start()
    t2.start()
    t1.join()
    t2.join()



h2o2@h2o2-PC:~/Documents/01-工程文件/PycharmProjects/04-进程与线程$ python3 18-线程-ThreadLocal.py 
线程:T-A -- 值:1
线程:T-B -- 值:2


异步

import time
from multiprocessing import Pool
import os



def do_run_1():
    print("---线程池中的进程的 pid:%d,ppid:%d--" % (os.getpid(), os.getppid()))
    for i in range(3):
        print("---%d---" % i)
        time.sleep(1)
    return "FFF"



def do_run_2(args):
    print("回调函数--pid=%d" % os.getpid())
    print("回调函数返回值=%s" % args)



if __name__ == '__main__':
    pool = Pool(3)
    pool.apply_async(func=do_run_1, callback=do_run_2) # 进程池中加入一个进程
    time.sleep(5)        # 子进程结束后,回调函数由 父进程操作

    print("---主线程--pid=%d ---" % os.getpid()) # 主进程在子进程结束后,暂停自己的事情去执行回调函数,后执行自己的任务



h2o2@h2o2-PC:~/Documents/01-工程文件/PycharmProjects/04-进程与线程$ python3 19-异步.py 
---线程池中的进程的 pid:7987,ppid:7986--
---0---
---1---
---2---
回调函数--pid=7986
回调函数返回值=FFF
---主线程--pid=7986 ---


GIL(全局解释器锁)

  1. 同步:你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,说明不支持并发也不支持并行
  2. 异步:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后电话以后继续吃饭,说明支持并发
  3. 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行
    并发:交替处理多个任务的能力;执行的任务大于核数
    并行:同时处理多个任务的能力;执行的任务小于核数
    并发的关键是你有处理多个任务的能力,不一定要同时
    并行的关键是你有同时处理多个任务的能力,强调的是同时.
    
    所以它们最大的区别就是:是否是『同时』处理任务。
    对于一个多核cpu来说并行要比并发快的多

cpython解释器中存在一个GIL(全局解释器锁),它的作用就是保证同一时刻只有一个线程可以执行代码,因此造成了我们使用多线程的时候无法实现并行。

多核CPU最好的多任务选择还是多进程,多进程的效率远远大于多进程

Python的GIL是什么鬼,多线程性能究竟如何

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

推荐阅读更多精彩内容