python多任务——线程

多任务:操作系统可以同时运⾏多个任务

单核CPU执行多任务:操作系统轮流让各个任务交替执⾏,任务1执⾏0.01秒,切换到任务2,任务2执⾏0.01秒,再切换到任务3,执⾏0.01秒……这样反复执⾏下去。表⾯上看,每个任务都是交替执⾏的,但是,由于CPU的执⾏速度实在是太快了,我们感觉就像所有任务都在同时执⾏⼀样。

多核CPU执行多任务:由于任务数量远远多于CPU的核⼼数量,所以,操作系统也会⾃动把很多任务轮流调度到每个核⼼上执⾏。

  • 并发:指的是任务数多于cpu核数,通过操作系统的各种任务调度算法,实现⽤多个任务“⼀起”执⾏(实际上总有⼀些任务不在执⾏,因为切换任务的速度相当快,看上去⼀起执⾏⽽已)
  • 并⾏:指的是任务数⼩于等于cpu核数,即任务真的是⼀起执⾏的。

线程

单个进程中执行每个任务就是一个线程。线程是进程中执行运算的最小单位。

使⽤threading模块,threading模块中包含了关于线程操作的丰富功能,包括:常用线程函数,线程对象,锁对象,递归锁对象,事件对象,条件变量对象,信号量对象,定时器对象,栅栏对象。

threading是一个创建多线程的库(调用threading库的threading.Thread方法),语法为:threading.Thread(target=函数名,args=(函数参数1,....函数参数n),name=’线程名’)

单线程

import time
def saySorry():
    print("亲爱的,我错了,我能吃饭了吗?")
    time.sleep(1)
if __name__ == "__main__":
    for i in range(5):
        saySorry()

多线程

1. 每个线程默认有⼀个名字,尽管下⾯的例⼦中没有指定线程对象的name,但是python会⾃动为线程指定⼀个名字;
2. 当线程的run()⽅法结束时该线程完成;
3. ⽆法控制线程调度程序,但可以通过别的⽅式来影响线程调度的⽅式。

import threading
import time
#先定义函数再用Thread方法创建线程
def saySorry():
    print("亲爱的,我错了,我能吃饭了吗?")
    time.sleep(1)
if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=saySorry)
        # 启动线程,即让线程开始执⾏
        t.start()

可以明显看出使⽤了多线程并发的操作,花费时间要短很多。当调⽤ start() 时,才会真正的创建线程,并且开始执⾏。

threading.enumerate():以列表形式返回当前所有存活的 Thread 对象。可以看出当前允许的线程数。

import threading
from time import sleep,ctime
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()

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

通过使⽤threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使⽤threading模块时,往往会定义⼀个新的⼦类class,只要继承 threading.Thread 就可以了,然后重写 run ⽅法。

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)
if __name__ == '__main__':
    t = MyThread()
    t.start()

结果:

I'm Thread-1 @ 0
I'm Thread-1 @ 1
I'm Thread-1 @ 2

另:多线程程序的执⾏顺序是不确定的。当执⾏到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进⼊就绪(Runnable)状态,等待调度。⽽线程调度将⾃⾏选择⼀个线程执⾏。上⾯的代码中只能保证每个线程都运⾏完整个run函数,但是线程的启动顺序、run函数中每次循环的执⾏顺序都不能确定。

共享全局变量

在⼀个进程内的所有线程共享全局变量,很⽅便在多个线程间共享数据。缺点就是,线程是对全局变量随意修改可能造成多线程之间对全局变量的混乱(即线程⾮安全)。

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

结果是:

---线程创建之前g_num is 100---
----in work1, g_num is 103---
----in work2, g_num is 103---

列表当做实参传递到线程

from threading import Thread
import time
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()

结果是:

----in work1--- [11, 22, 33, 44]
----in work2--- [11, 22, 33, 44]

同步

同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。"同"字从字⾯上容易理解为⼀起动作其实不是,"同"字应是指协同、协助、互相配合。如进程、线程同步,可理解为进程或线程A和B⼀块配合,A执⾏到⼀定程度时要依靠B的某个结果,于是停下来,示意B运⾏;B执⾏,再将结果给A,A再继续操作。

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

import threading
import time
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=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
while len(threading.enumerate()) != 1:
    time.sleep(1)
print("2个线程对同⼀个全局变量操作之后的最终结果是:%s" % g_num)

结果是:

---线程创建之前g_num is 0---
----in work2, g_num is 1184155-------in work1, g_num is 1269087---

2个线程对同⼀个全局变量操作之后的最终结果是:1269087

通过线程同步来进⾏解决思路,如下:
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进⾏修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性。

互斥锁

当多个线程⼏乎同时修改某⼀个共享数据的时候,需要进⾏同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引⼊互斥锁。
互斥锁为资源引⼊⼀个状态:锁定/⾮锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“⾮锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有⼀个线程进⾏写⼊操作,从⽽保证了多线程情况下数据的正确性。

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

  • acquire(blocking=True, timeout=-1):获取锁,并将锁的状态改为“锁定”,成功返回True,失败返回False。当一个线程获得锁时,会阻塞其他尝试获取锁的线程,直到这个锁被释放掉。timeout默认值为-1,即将无限阻塞等待直到获得锁,如果设为其他的值时(单位为秒的浮点数),将最多阻塞等待timeout指定的秒数。当blocking为False时,timeout参数被忽略,即没有获得锁也不进行阻塞。
  • release():释放一个锁,并将其状态改为“非锁定”,需要注意的是任何线程都可以释放锁,不只是获得锁的线程(因为锁不属于特定的线程)。release()方法只能在锁处于“锁定”状态时调用,如果在“非锁定”状态时调用则会报RuntimeError错误。

当⼀个线程调⽤锁的acquire()⽅法获得锁时,锁就进⼊“locked”状态。
每次只有⼀个线程可以获得锁。如果此时另⼀个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调⽤锁的release()⽅法释放锁之后,锁进⼊“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择⼀个来获得锁,并使得该线程进⼊运⾏(running)状态。

#使⽤互斥锁完成2个线程对同⼀个全局变量各加100万次的操作
import threading
import time
g_num = 0
def work1(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁
def work2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁
print("---线程创建之前g_num is %d---"%g_num)
mutex = threading.Lock()
t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()
time.sleep(10)
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
while len(threading.enumerate()) != 1:
    time.sleep(1)
print("2个线程对同⼀个全局变量操作之后的最终结果是:%s" % g_num)

结果是:

---线程创建之前g_num is 0---
2个线程对同⼀个全局变量操作之后的最终结果是:2000000

锁的好处:
确保了某段关键代码只能由⼀个线程从头到尾完整地执⾏
锁的坏处:
阻⽌了多线程并发执⾏,包含锁的某段代码实际上只能以单线程模式执⾏,效率就⼤⼤地下降了;
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对⽅持有的锁时,可能会造成死锁。

死锁

在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时等待对⽅的资源,就会造成死锁。尽管死锁很少发⽣,但⼀旦发⽣就会造成应⽤的停⽌响应。

如下例子,进入死锁状态:

import threading
import time
class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()
        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name + '----do1---up----')
        time.sleep(1)
        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name + '----do1---down----')
        mutexB.release()
        # 对mutexA解锁
        mutexA.release()

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

        # 对mutexB上锁
        mutexB.acquire()
        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name + '----do2---up----')
        time.sleep(1)
        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name + '----do2---down----')
        mutexA.release()
        # 对mutexB解锁
        mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

避免死锁

程序设计时要尽量避免(银⾏家算法)
       添加超时时间等

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值