Python基础之多线程

目录

1、多任务介绍

2、多任务的概念

3、threading模块介绍

4、Thread类使用说明

4.2 实例化threading.Thread

4.3 继承threading.Thread

4.4 多线程-共享全局变量

4.5 多线程-共享全局变量问题

5、线程同步概念

6、互斥锁

7、死锁


1、多任务介绍

多场景中事情同时进行

from time import sleep


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


def dance():
    for i in range(3):
        print("正在跳舞...{}".format(i))
        sleep(i)


if __name__ == '__main__':
    sing()
    dance()

2、多任务的概念

真正并行执行多任务只能再多核cpu上实现,但是由于任务数量远远多于cpu核心数量,所以操作系统也会自动把很多任务轮流调度到每个核心运行。

并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务一起执行(实际上总有一些任务不再执行,因为切换任务的速度相当快,看上去一起执行而已)

并行:指的是任务数小于等于cpu核数,即任务真的是一起执行

3、threading模块介绍

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便被使用。

threading模块常用方法如下:

threading.active_count() # 返回当前处于active状态的Thread对象

threading.current_thread() # 返回当前Thread对象

threading.get_ident() # 返回当前线程的线程标识符,线程标识符是一个非负整数,并无特殊含义,只是用来标识线程,该整数可能会被循环使用。py3.3及以后支持

threading.enumerate()  # 返回当前处于active状态的所有Thread对象列表

threading.main_thread()  # 返回主线程对象,即启动python解释器的线程对象,py3.4及以后支持

threading.stack_size()  # 返回创建线程时使用的栈的大小,如果指定size参数,则用来指定后续创建的线程使用的栈大小,size必须是0(标识使用系统默认值)或大于32K的正整数。

4、Thread类使用说明

threading模块提供了Thread,Lock,RLock,Condition,Event,Timer和Semaphore等类来支持多线程,Thread是其中最重要也是最基本的一个类,可以通过该类创建线程并控制线程运行。

使用Thread创建线程的方法:

1.为构造函数传递一个可调用对象

2.继承Thread类并在子类中重写__init__()和run()方法

语法格式:threading.Thread(group=None,target=None, name=None,args=(),kwargs={},*,daemon=None)

参数说明:

group:通常默认即可,作为日后扩展ThreadGroup类实现而保留

target:用于run()方法调用的可调用对象,默认为None

name:线程名称,默认是Thread-N格式构成的唯一名称,其中N是十进制数

args:用于调用目标函数的参数元组,默认为()

kwargs:用于调用目标函数的关键字参数字典,默认为{}

daemon:设置线程是否为守护模式,默认为None

线程对象threading.Thread的方法和属性:

start()  启动线程

run()  线程代码,用来实现线程的功能和业务逻辑,可以在子类中重写该方法来自定义线程的行为

init(self,group=None,target=None, name=None,args=(),kwargs=None,daemon=None)  构造函数

is_alive()  判断线程是否存活

getName()  返回线程名

setName()  设置线程名

isDaemon()  判断线程是否为守护线程

setDaemon()  设置线程是否为守护线程

name  用来读取或设置线程的名字

ident  线程标识,用非0数字或None(线程未被启动)

daemon  标识线程是否为守护线程,默认为False

join(timeout=None)  当timeout为None时,会等待至线程结束;当timeout不为None时,会等待至timeout时间结束,单位为秒。

4.2 实例化threading.Thread

1.单线程执行

import time


def saySorry():
    print("saySorry")
    time.sleep(1)
    

if __name__ == '__main__':
    for i in range(5):
        saySorry()

2.使用threading模块

import time
import threading

def saySorry():
    print("saySorry")
    time.sleep(1)


if __name__ == '__main__':
    for i in range(5):
        t = threading.Thread(target=saySorry)
        t.start()

使用多线程并发的操作,花费时间要短很多

当调用start()时,才会真正的创建线程,并开始执行

3.主线程会等待所有的子线程结束后才结束

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


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


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

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

    t1.start()
    t2.start()

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

4.查看线程数量

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


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

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

    t1.start()
    t2.start()

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

        sleep(0.5)

4.3 继承threading.Thread

1.线程执行代码的封装

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

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

2.线程的执行顺序

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)


def demo():
    for i in range(5):
        t = MyThread()
        t.start()


if __name__ == '__main__':
    demo()

说明:从代码和执行结果可以看出,多线程程序的执行顺序时不确定的。当执行得到sleep语句时,线程将会被阻塞,到sleep结束后,线程进入就绪状态,等待调度。而线程调度将自行选择一个线程执行。上面代码只能保证每个线程都运行完整个run函数,但线程的启动顺序,run函数中每次循环的执行顺序都不能确定。

总结:

1.每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字

2.当线程的run()方法结束时该线程完成

3.无法控制线程调度程序,但可以通过别的方法来影响线程调度的方式

4.4 多线程-共享全局变量

from threading import Thread
import time


g_num = 100


def work1():
    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)


print("---线程创建之前g_num is %d" % g_num)

t1 = Thread(target=work1)
t1.start()

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

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

列表当做实参传递到线程中:

from threading import Thread
import time

"""
g_num = 100


def work1():
    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)


print("---线程创建之前g_num is %d" % g_num)

t1 = Thread(target=work1)
t1.start()

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

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


def work1(nums):
    nums.append(44)
    print("---in work1---", nums)


def work2(nums):
    # 延时一会儿,保证t1线程中事情做完
    time.sleep(1)
    print("---in work2---", nums)


g_nums = [11, 22, 33]

t1 = Thread(target=work1, args=(g_nums,))
t1.start()

t2 = Thread(target=work2, args=(g_nums,))
t2.start()

总结:

在一个进程内所有进程共享全局变量,很方便在多个线程之间共享数据

缺点就是,线程是对全程变量随意篡改可能造成多线程之间对全局变量的混乱(即线程非安全)

4.5 多线程-共享全局变量问题

多线程开发可能遇到的问题:假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算

,t1和t2都各对g_num加10次,g_num的最终结果应该为20

但是由于多线程同时进行操作,可能出现下面情况:

1.在g_num=0时,t1取得g_num=0。此时系统把t1调度成sleeping状态,那t2转换为running状态,t2也获得g_num=0

2.然后t2对得到的值进行加1并赋值给g_num,使得g_num=1

3.然后系统又把t2调度为sleeping,把t1转为running。线程t1又把它之前得到的0加1后赋值给g_num

4.这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1

g_num = 0


def work1(num):
    global g_num
    for i in range(num):
        g_num += 1

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


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("---in work2,g_num is %d---" % g_num)


print("---线程创建之前g_num is %d" % g_num)


t1 = threading.Thread(target=work1, args=(100,))
t1.start()

t2 = threading.Thread(target=work2, args=(100,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2个线程对同一个全局变量进行操作之后的最终结果是:%s" % g_num)

结论:如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确

5、线程同步概念

同步就是协同步骤,按预定的先后次序进行运行。如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据正确性,需要对多个线程进行同步。

使用Thread对象的Lock和Rlock可以时间简单的线程同步,这两个对象都有acquire方法release方法,对于那些只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间

对于上面提出的计算错误问题,可以通过线程同步来进行解决思路,如下:

1.系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num

2.t1对g_num的值进行+1

3.t1解锁,此时g_num的值为1,其他线程就可以使用g_num了,而且是g_num的值不是0而是1

4.同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证数据正确性

6、互斥锁

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制

线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制时引入互斥锁

互斥锁为资源引入一个状态:锁定/非锁定

某个线程要更改共享数据时,先将其锁定,此时资源的状态为锁定,其他线程不能更改,直到该线程释放资源,将资源的状态变成非锁定,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证多线程情况下数据的正确性。

threading模块中定义了Lock类,可以方便的处理锁定:

import threading

# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()

注意:

如果这个锁之前没有上锁,那么acquire不会堵塞

如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁 那么此时acquire会堵塞,直到这个锁被解锁为止

使用互斥锁完成两个线程对同一个全局变量各加100万次的操作:

g_num = 0
# 创建锁
mutex = threading.Lock()


def demo1(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁

    print("---demo1---g_num=%d" % g_num)


def demo2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁

    print("---demo2---g_num=%d" % g_num)


# 创建两个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=demo1, args=(1000000,))
p1.start()

p2 = threading.Thread(target=demo2, args=(1000000,))
p2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%d" % g_num)

上锁解锁过程:

当一个线程调用锁acquire()方法获得锁时,锁就进入locked状态

每次只有一个线程可以获得锁,如果此时另一个线程试图获得这个锁,该线程就会变为block状态,称为阻塞,知道拥有锁的线程调用锁的release()方法释放锁之后,锁进入unlocked状态

线程调度程序从处于同步阻塞时状态的线程中选择一个来获得锁,并使得该线程进入运行状态

总结:

锁的好处:确保了某段关键代码只能由一个线程从头到尾完成地执行

锁的坏处:

阻止了多线程并发执行,包含锁的某段代码实际只能单线程模式执行,效率就大大下降了,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有锁时,可能会造成死锁。

7、死锁

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

import threading
import time


class MyThread1(threading.Thread):
    def run(self):
        # 上锁
        mutexA.acquire()

        # 延时1s,等待另外线程,把mutexB上锁
        print(self.name+'---do1---up---')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name + '---do1---down---')
        mutexB.release()

        mutexA.release()


class MyThread2(threading.Thread):
    def run(self):
        mutexB.acquire()

        print(self.name+'---do2---up---')
        time.sleep(1)

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

如何避免死锁:

1.程序设计时要尽量避免银行家算法

2.添加超时时间等

猜你喜欢

转载自blog.csdn.net/xiao__dashen/article/details/125416102