并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
一、线程的概念
线程是一个进程内部的一条执行路径,是程序执行的最小单位
线程有自己的栈和局部变量,多个线程共享同一进程的地址空间
多线程就是在一个进程中创建多个线程,每个线程完成一个任务
二、线程的实现
1. 使用threading模块
(1)单线程执行
import time def saySorry(): print('亲爱的,我错了,我能吃饭了吗') time.sleep(1) if __name__ == '__main__': for i in range(5): saySorry()
运行会发现,每隔一秒打印一句“亲爱的,我错了,我能吃饭了吗”
(2)多线程执行
实例化Thread对象,通过target参数指定线程将要执行的函数名,args参数通过元组来指定函数的位置实参,kwargs参数通过字典来指定函数的关键字实参。
import time import threading def saySorry(): print('亲爱的,我错了,我能吃饭了吗') time.sleep(1) if __name__ == '__main__': for i in range(5): t = threading.Thread(target=saySorry) t.start()
可以明显看出来,使用了多线程并发的操作,花费的时间要短很多
当调用start()时,才会真正创建线程,并且开始执行
2、主线程会等待所有子线程结束后才结束
import threading from time import ctime, sleep def sing(): for i in range(5): print('正在唱歌。。。%d' %i) sleep(1) def dance(): for i in range(5): 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())
运行结果:
---开始---:Mon Sep 16 20:02:46 2019 正在唱歌。。。0 正在跳舞。。。0 正在跳舞。。。1 正在唱歌。。。1 正在跳舞。。。2 正在唱歌。。。2 正在跳舞。。。3 正在唱歌。。。3 正在跳舞。。。4 正在唱歌。。。4 ---结束---Mon Sep 16 20:02:51 2019
import threading from time import ctime, sleep def sing(): for i in range(5): print('正在唱歌。。。%d' %i) sleep(1) def dance(): for i in range(5): 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())
输出结果:
---开始---:Mon Sep 16 20:04:45 2019 正在唱歌。。。0 正在跳舞。。。0 ---结束---Mon Sep 16 20:04:45 2019 正在唱歌。。。1 正在跳舞。。。1 正在跳舞。。。2 正在唱歌。。。2 正在跳舞。。。3 正在唱歌。。。3 正在唱歌。。。4 正在跳舞。。。4
3、查看线程数量
import threading from time import sleep, ctime def sing(): for i in range(5): print('正在唱歌。。。%d' %i) sleep(1) def dance(): for i in range(5): 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 1: length = len(threading.enumerate()) print('当前运行的线程数为:%d' %length) if length<=1: break sleep(0.5)
运行结果:
---开始---:Mon Sep 16 20:12:37 2019 正在唱歌。。。0 正在跳舞。。。0 当前运行的线程数为:3 当前运行的线程数为:3 正在跳舞。。。1 正在唱歌。。。1 当前运行的线程数为:3 当前运行的线程数为:3 正在唱歌。。。2正在跳舞。。。2 当前运行的线程数为:3 当前运行的线程数为:3 正在跳舞。。。3正在唱歌。。。3 当前运行的线程数为:3 当前运行的线程数为:3 正在跳舞。。。4正在唱歌。。。4 当前运行的线程数为:3 当前运行的线程数为:3 当前运行的线程数为:1
4、线程执行代码的封装
通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread
就可以了,然后重写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) # name属性中保存的是当前线程的名字 print(msg) if __name__ == '__main__': t = MyThread() t.start()
运行结果:
I'mThread-1@0 I'mThread-1@1 I'mThread-1@2
说明:
python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。
每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。
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) print(msg) def test(): for i in range(5): t = MyThread() t.start() if __name__ == '__main__': test()
执行结果:(运行的结果可能不一样,但是大体是一致的)
I'mThread-3@0 I'mThread-1@0 I'mThread-2@0 I'mThread-4@0 I'mThread-5@0 I'mThread-1@1 I'mThread-3@1 I'mThread-2@1 I'mThread-5@1I'mThread-4@1 I'mThread-1@2 I'mThread-2@2I'mThread-3@2 I'mThread-5@2I'mThread-4@2
从代码和执行结果来看,多线程程序的执行顺序是不确定的,当执行到sleep语句时候,线程将被阻塞,到sleep结束,线程进入就绪状态,等待调度。而线程调度将自动选择一个线程执行。上面代码只能保证每个线程都运行完整的run函数,但是线程的启动顺序,run函数中每次循环的执行顺序都不能确定。
当线程的run()方法结束时该线程完成。
无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
三、多线程共享全局变量
1、共享全局变量
import time import threading 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 = threading.Thread(target=work1) t1.start() time.sleep(3) # 延时,保证t1线程中的事情做完 t2 = threading.Thread(target=work2) t2.start()
运行结果:
---线程创建之前g_num is 100--- ---in work1, g_num is 103---- ---in work2,g_num is 103---
2、列表当作实参传递到线程中
import time import threading def work1(nums): nums.append(44) print('---in work1---', nums) def work2(nums): time.sleep(1) # 延时保证t1线程中的事情做完 print('---in work2---',nums) g_nums = [11,22,33] t1 = threading.Thread(target=work1, args=(g_nums,)) t1.start() t2 = threading.Thread(target=work2,args=(g_nums,)) t2.start()
运行结果
---in work1--- [11, 22, 33, 44] ---in work2--- [11, 22, 33, 44]
在一个进程内,所有线程共享全局变量,很方便多个线程间共享数据
缺点就是,线程对全局变量随意更改,可能造成多线程之间对全局变量的混乱(即线程非安全)
3、多线程全局共享变量开发可能会遇到的问题
假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算,t1和t2都各对g_num加10次,g_num的最终的结果应该为20。
但是由于是多线程同时操作,有可能出现下面情况:
a、在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0
b、然后t2对得到的值进行加1并赋给g_num,使得g_num=1
c、然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
d、这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1
测试1:
import time import threading g_num = 0 def word1(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=word1, 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)
运行结果:
---线程创建之前g_num is 0--- ---in work1,g_num is 100--- ---in work2, g_num is 200--- 2个线程对同一个全局变量操作之后的最终结果是:200
测试2:
import time import threading g_num = 0 def word1(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=word1, 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 1331011--- ---in work1,g_num is 1382473--- 2个线程对同一个全局变量操作之后的最终结果是:1382473
结论:如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确。
四、同步
1、同步的概念
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
“同”字从字面上容易理解为一起动作。
其实不是,"同"字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。