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 语言解决

你可能感兴趣的:(Python 多线程 threading)