多任务--线程

Posted zach0812

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多任务--线程相关的知识,希望对你有一定的参考价值。

简单的多任务demo:

技术图片
 1 import time
 2 import threading
 3 
 4 def sing():
 5     for i in range(5):
 6         print("------正在唱歌-------")
 7         time.sleep(1)
 8 
 9 
10 def dance():
11     for i in range(5):
12         print("------正在跳舞-------")
13         time.sleep(1)
14 
15 
16 def main():
17     target1 = threading.Thread(target=sing)
18     target2 = threading.Thread(target=dance)
19     target1.start()
20     target2.start()
21 
22 
23 if __name__ == __main__:
24     main()
25 ‘‘‘
26 ------正在唱歌-------
27 ------正在跳舞-------
28 ------正在唱歌-------
29 ------正在跳舞-------
30 ------正在跳舞-------
31 ------正在唱歌-------
32 ------正在唱歌-------
33 ------正在跳舞-------
34 ------正在唱歌-------
35 ------正在跳舞-------
36 ‘‘‘
View Code

技术图片

任务数大于cpu核数是并发!

任务数小于cpu核数是并行!

我们的电脑上基本上属于并发的!

现在所有的多任务基本上都是假的多任务!基本都是并发。而不是并行。

上面的多任务的实现是用threading 模块中的Thread() 类实现的!

 

查看正在运行的线程数量:

threading 模块中的enumerate()可以返回所有的线程,是个列表。

线程的执行是没有顺序的。

技术图片
 1 import time
 2 import threading
 3 
 4 def sing():
 5     for i in range(5):
 6         print("------正在唱歌-------")
 7         time.sleep(1)
 8 
 9 
10 def dance():
11     for i in range(5):
12         print("------正在跳舞-------")
13         time.sleep(1)
14 
15 
16 def main():
17     target1 = threading.Thread(target=sing)
18     target2 = threading.Thread(target=dance)
19     target1.start()
20     target2.start()
21 
22     print(threading.enumerate())  # 它是个列表,里面放的是线程对象
23     
24 
25 
26 
27 if __name__ == __main__:
28     main()
29 ‘‘‘
30 ------正在唱歌-------
31 ------正在跳舞-------
32 [<_MainThread(MainThread, started 9608)>, <Thread(Thread-2, started 14892)>, <Thread(Thread-1, started 13340)>]
33 ------正在唱歌-------
34 ------正在跳舞-------
35 ------正在唱歌-------
36 ------正在跳舞-------
37 ------正在跳舞-------
38 ------正在唱歌-------
39 ------正在跳舞-------
40 ------正在唱歌-------
41 ‘‘‘
View Code

 

如果创建Thread的时候执行的函数结束,就是这个线程结束了!

如下来验证上述观点:

技术图片
 1 import time
 2 import threading
 3 
 4 def sing():
 5     for i in range(5):
 6         print("------正在唱歌-------")
 7         time.sleep(1)
 8 
 9 
10 def dance():
11     for i in range(10):
12         print("------正在跳舞-------")
13         time.sleep(1)
14 
15 
16 def main():
17     target1 = threading.Thread(target=sing)
18     target2 = threading.Thread(target=dance)
19     target1.start()
20     target2.start()
21 
22     while True:
23         time.sleep(1)
24         print(threading.enumerate())  # 它是个列表,里面放的是线程对象
25 
26 
27 if __name__ == __main__:
28     main()
29 ‘‘‘
30 ------正在唱歌-------
31 ------正在跳舞-------
32 ------正在跳舞-------
33 [<Thread(Thread-1, started 1092)>, <_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
34 ------正在唱歌-------
35 ------正在唱歌-------
36 ------正在跳舞-------
37 [<Thread(Thread-1, started 1092)>, <_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
38 ------正在跳舞-------
39 ------正在唱歌-------
40 [<Thread(Thread-1, started 1092)>, <_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
41 ------正在唱歌-------
42 [<Thread(Thread-1, started 1092)>, <_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
43 ------正在跳舞-------
44 [<Thread(Thread-1, started 1092)>, <_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
45 ------正在跳舞-------
46 [<_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
47 ------正在跳舞-------
48 [<_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
49 ------正在跳舞-------
50 [<_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
51 ------正在跳舞-------
52 [<_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
53 ------正在跳舞-------
54 [<_MainThread(MainThread, started 2780)>, <Thread(Thread-2, started 14820)>]
55 [<_MainThread(MainThread, started 2780)>]
56 [<_MainThread(MainThread, started 2780)>]
57 [<_MainThread(MainThread, started 2780)>]
58 [<_MainThread(MainThread, started 2780)>]
59 ‘‘‘
线程数逐渐从3个降到1个,那两个都结束了

 

我们可以通过threading 中的enumrate() 得到的列表的个数来判断子线程是否执行完毕!

什么时候创建子线程,什么时候运行子线程?

它们都是从start() 开始的,执行到start 的时候创建子线程!并且开始执行子线程。

证明如下:

技术图片
 1 import time
 2 import threading
 3 
 4 def sing():
 5     for i in range(5):
 6         print("------正在唱歌-------")
 7         time.sleep(1)
 8 
 9 def main():
10     print(threading.enumerate())
11     target1 = threading.Thread(target=sing)
12     print(threading.enumerate())
13     target1.start()
14     print(threading.enumerate())
15 
16 if __name__ == __main__:
17     main()
18 ‘‘‘
19 [<_MainThread(MainThread, started 12700)>]
20 [<_MainThread(MainThread, started 12700)>]
21 ------正在唱歌-------
22 [<Thread(Thread-1, started 14500)>, <_MainThread(MainThread, started 12700)>]
23 ------正在唱歌-------
24 ------正在唱歌-------
25 ------正在唱歌-------
26 ------正在唱歌-------
27 ‘‘‘
View Code

调用threading.Thread() 类的时候,只是创建了一个普通的对象。

thread.start() 是真实创建线程并开始执行

 

到此,我们知道了一下几点:

1,线程的执行没有顺序,(可通过sleep 达到我们所期望的要求)

2,线程的创建和执行都是从start() 之后开始的,线程的结束是对应的函数执行完毕,结束子线程!

3,默认主线程会等子线程先执行完毕。

 

通过继承Thread类完成创建线程:

从上面我们知道,我们可以通过threading.Thread()中的target 可以指定一个函数名,从而实现一个线程的创建(真实创建和执行是start)。

但是,我们现在要创建的线程比较的复杂,不是简单的函数就能完成的,这时我们要通过继承threading.Thread() 这个类,然后我们必须要重写run() 方法

Thread() 类的两种使用方式 :

技术图片

 

继承 Thread的使用方式:

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import  threading
 5 import  time
 6 
 7 class MyThread(threading.Thread):
 8     def run(self):
 9         for i in range(3):
10             time.sleep(1)
11             print("I am "+self.name)
12 
13 if __name__ == __main__:
14     thread1 = MyThread()
15     thread1.start()
16 
17 ‘‘‘
18 输出:
19     I am Thread-1
20     I am Thread-1
21     I am Thread-1
22 ‘‘‘
View Code

其中的self.name 是线程的名字。

这种适合的是一个线程里面做的事情比较复杂,可以多个函数来做事情。

 

也许你可能会觉得调用的时候应该用thread1.run() 。

这里我们看官方文档中的内容:
技术图片

 它主要介绍了start() 的用法,它说的是对于每一个线程对象,必须用start() 调用,

start()复杂的调用run() 方法。

而且,如果一个线程对象调用多次start() 会抛出 RuntimeError 异常。

 

如果用继承方式使用多线程的话,此时的传参要注意:

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import  threading
 5 import  time
 6 
 7 class MyThread(threading.Thread):
 8     def __init__(self,args =None,kwargs=None):
 9         super().__init__(args = None,kwargs=None)
10         self.name = kwargs["name"]
11         self.age = kwargs["age"]
12         self.list = args[0]
13 
14     def run(self):
15         self.test()
16 
17     def test(self):
18         print("Name :,Age:".format(self.name,self.age))
19         for i in self.list:
20             print(i,end=" ")
21 
22 
23 if __name__ == __main__:
24     thread1 = MyThread(args=([1,2,3,4,5],) ,kwargs="name":"tom","age":18)
25     thread1.start()
26     for i in range(5,11):
27         print(i ,end=" ")
28 
29 ‘‘‘
30 输出:
31 Name :tom,Age:18
32 5 6 7 8 9 10 1 2 3 4 5 
33 ‘‘‘
View Code

 

前面第一种多线程的使用中,如果需要传递参数:

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 def test(ls,dt):
 8     for i in ls:
 9         print(i,end=" ")
10     print("Name:  Age: ".format(dt["name"],dt["age"]))
11 
12 def main():
13     ls = [1,2,3,4,5]
14     dt = "name":"tom","age":18
15     a = [1,2,3,4,5]
16     thread1 = threading.Thread(target=test,args=(ls,dt))
17     thread1.start()
18 
19 if __name__ == __main__:
20     main()
21     print("I am Main_Thread!")
22     for i in range(6,11):
23         print(i)
用args传递
技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 def test(ls,name,age):
 8     for i in ls:
 9         print(i,end=" ")
10     print("Name:  Age: ".format(name,age))  # 注意,如果是用kwargs 传递字典的话,要用字典的key 做形参
11 
12 def main():
13     ls = [1,2,3,4,5]
14     dt = "name":"tom","age":18
15     a = [1,2,3,4,5]
16     thread1 = threading.Thread(target=test,args=(ls,),kwargs=dt)
17     thread1.start()
18 
19 if __name__ == __main__:
20     main()
21     print("I am Main_Thread!")
22     for i in range(6,11):
23         print(i)
使用args 和kwargs 来传递参数

 

总结本质上都是通过args 和kwargs 来传参的。

 

多线程共享全局变量:

首先,说下如果想要操作全局变量,到底什么时候加global ,什么时候不用加:

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 ############################全局变量相关###############################
 5 
 6 # num = 15
 7 # def test():
 8 #     global num  #此时操作的num 为全局变量
 9 #     num +=1
10 # test()
11 # print(num)  # 此时为16
12 
13 
14 # num = [1,2,3]
15 # def test():
16 #     num.append(4)  #此时操作的num 是全局变量
17 # test()
18 # print(num)  #此时为 【1 2 3 4 】
19 
20 # num = [1,2,3]
21 # def test():
22 #     num +=[4,5]   #此时操作的num 就不是全局变量了  ,不能这样写,会报错
23 # test()
24 # print(num)
25 
26 num = [1,2,3]
27 def test():
28     global num
29     num +=[4,5]   #此时操作的num 就不是全局变量了  ,不能这样写,会报错
30 test()
31 print(num)
32 
33 #那么问题来了,操作全局变量的时候,到底什么时候加global ,
34 #记住,当指针没发生变化的时候,不用加,发生变化必须加
35 
36 ############################全局变量相关###############################
View Code

 

下面用程序来验证,多线程是共享全局变量的:

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 num = 100
 8 
 9 
10 def test():
11     global num
12     num +=1
13 
14 def main():
15     thread1 = threading.Thread(target=test)
16     thread1.start()
17     time.sleep(1)
18     print(num)
19 
20 if __name__ == __main__:
21     main()
View Code

输出是 101  ,这说明主线程和创建的子线程是共享全局变量的。

 

多线程共享全局变量的问题:

资源竞争。

当循环次数为100的时候:

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 num = 0
 8 
 9 def test01():
10     global num
11     for i in range(100):
12         num = num +1
13     print("in test01 ".format(num))
14 
15 def test02():
16     global num
17     for i in range(100):
18         num = num +1
19     print("in test02 ".format(num))
20 
21 def main():
22     thread1 = threading.Thread(target=test01)
23     thread2 = threading.Thread(target=test02)
24 
25     thread1.start()
26     thread2.start()
27 
28     time.sleep(2)
29 
30     print("in main ".format(num))
31 
32 if __name__ == __main__:
33     main()
34     ‘‘‘
35     输出:
36     in test01 100
37     in test02 200
38     in main 200
39     
View Code

结果是正常的

次数为一千,一万,十万都是正常的,

但是如果循环次数为一百万的话,此时就要出错了。

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 num = 0
 8 
 9 def test01():
10     global num
11     for i in range(1000000):
12         num = num +1
13     print("in test01 ".format(num))
14 
15 def test02():
16     global num
17     for i in range(1000000):
18         num = num +1
19     print("in test02 ".format(num))
20 
21 def main():
22     thread1 = threading.Thread(target=test01)
23     thread2 = threading.Thread(target=test02)
24 
25     thread1.start()
26     thread2.start()
27 
28     time.sleep(2)
29 
30     print("in main ".format(num))
31 
32 if __name__ == __main__:
33     main()
34     ‘‘‘
35     输出:
36     in test01 1206988
37     in test02 1191407
38     in main 1191407
39 
40     这里的问题是:
41         在执行num =num +1  的时候,cpu 会做很多语句
42         1,获取num 值
43         2,将num 值加1
44         2,将第二步得到的值写回到num 对应的内存中。
45 
46         这里可能出现如下情况:
47             假设num = 100时,
48             对于线程1,它可能执行了前两步  但是,它还没将得到的101写回到内存,
49                 此时轮到线程2来执行了,它也是执行了前两步,也没把101写回到内存,
50                 此时线程1再次执行,将101写回到内存,
51                 然后线程2也执行,又将101写回到内存。
52 
53             所以,此时两个线程都执行了一遍,但是num 的结构确实101 ,
54             而且随着次数的增加,这种可能性出现的概率就越大。
55     ‘‘‘
View Code


在执行num =num +1 的时候,cpu 会做很多语句
1,获取num 值
2,将num 值加1
2,将第二步得到的值写回到num 对应的内存中。

这里可能出现如下情况:
假设num = 100时,
对于线程1,它可能执行了前两步 但是,它还没将得到的101写回到内存,
此时轮到线程2来执行了,它也是执行了前两步,也没把101写回到内存,
此时线程1再次执行,将101写回到内存,
然后线程2也执行,又将101写回到内存。

所以,此时两个线程都执行了一遍,但是num 的结果确是101 ,
而且随着次数的增加,这种可能性出现的概率就越大。

同步概念:

上述出现的问题,如何解决呢?其实,这种多线程的问题在任何的编程语言中都是存在的。它是由于操作系统导致发生的这种情况。
如何解决呢?
其实,对于每个线程来说,它们不能只完成前两步,而没把最终结果写回内存,它应该是要么不做,要么就做完,这称为原子性

银行的存取款业务肯定也得是这样。不能一个线程取钱2步,另一个也取钱2步,然后,它们两个分别写回内存。就做到取了两份钱,银行却只扣了一份。

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

 

如何解决上述的问题呢?

用下面的东西:

互斥锁:

技术图片

技术图片

技术图片

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 num = 0
 8 
 9 def test01():
10     metux.acquire()
11     global num
12     for i in range(1000000):
13         num = num +1
14     print("in test01 ".format(num))
15     metux.release()
16 
17 
18 def test02():
19     metux.acquire()
20     global num
21     for i in range(1000000):
22         num = num +1
23     print("in test02 ".format(num))
24     metux.release()
25 
26 metux = threading.Lock()
27 
28 def main():
29     thread1 = threading.Thread(target=test01)
30     thread2 = threading.Thread(target=test02)
31 
32     thread1.start()
33     thread2.start()
34 
35     time.sleep(2)
36 
37     print("in main ".format(num))
38 
39 if __name__ == __main__:
40     main()
41 ‘‘‘
42 in test01 1000000
43 in test02 2000000
44 in main 2000000
45 ‘‘‘
View Code

此时,就已经解决了上述的问题了。

 

但是,上述的代码不好,上锁锁住的代码太多了,如果第一个锁上之后,它要执行100s ,那么第二个线程就只能等着了。

其实上锁有个原则,锁的代码越少越好。

其实,我们只需对关键的几步上锁就行了。

如下:

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 num = 0
 8 
 9 def test01():
10     global num
11     for i in range(1000000):
12         metux.acquire()
13         num = num +1
14         metux.release()
15 
16     print("in test01 ".format(num))
17 
18 
19 def test02():
20     global num
21     for i in range(1000000):
22         metux.acquire()
23         num = num +1
24         metux.release()
25     print("in test02 ".format(num))
26 
27 metux = threading.Lock()
28 
29 def main():
30     thread1 = threading.Thread(target=test01)
31     thread2 = threading.Thread(target=test02)
32 
33     thread1.start()
34     thread2.start()
35 
36     time.sleep(2)
37 
38     print("in main ".format(num))
39 
40 if __name__ == __main__:
41     t1 = time.perf_counter()
42     main()
43     print(time.perf_counter()-t1)
44 ‘‘‘
45 in test02 1981093
46 in test01 2000000
47 in main 2000000
48 2.019051503742717
49 ‘‘‘
View Code

此时,最终的结果是不变的。

上面,将锁放到 for  循环外面的用时::2.0241010994337283

上面,将锁放到 for  循环里面的用时::2.019051503742717

 

死锁:

程序中可能会有多个互斥锁,这时可能就出现问题了。

死锁是一种状态,就是一个线程等待对方释放锁,可是对方却也在等我释放锁。

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 class MyThread1(threading.Thread):
 8     def run(self):
 9         # 对mutexA上锁
10         mutexA.acquire()
11 
12         # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
13         print(self.name+----do1---up----)
14         time.sleep(1)
15 
16         # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
17         mutexB.acquire()
18         print(self.name+----do1---down----)
19         mutexB.release()
20 
21         # 对mutexA解锁
22         mutexA.release()
23 
24 class MyThread2(threading.Thread):
25     def run(self):
26         # 对mutexB上锁
27         mutexB.acquire()
28 
29         # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
30         print(self.name+----do2---up----)
31         time.sleep(1)
32 
33         # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
34         mutexA.acquire()
35         print(self.name+----do2---down----)
36         mutexA.release()
37 
38         # 对mutexB解锁
39         mutexB.release()
40 
41 mutexA = threading.Lock()
42 mutexB = threading.Lock()
43 
44 if __name__ == __main__:
45     t1 = MyThread1()
46     t2 = MyThread2()
47     t1.start()
48     t2.start()
死锁状态模拟
技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 def test1():
 8     lock1.acquire()
 9     print("in test1")
10     time.sleep(1)
11     lock2.acquire
12 
13 
14 def test2():
15     lock2.acquire()
16     print("in test2")
17     time.sleep(1)
18     lock1.acquire()
19 
20 lock1 = threading.Lock()
21 lock2 = threading.Lock()
22 def main():
23     thread1 = threading.Thread(target=test1)
24     thread2 = threading.Thread(target=test2)
25     thread1.start()
26     thread2.start()
27 
28 if __name__ == __main__:
29     main()
死锁

 

 

如何避免死锁:

有很多方法:

1,程序设计的时候要尽量避免(银行家算法)

2,添加超时时间

3,使用递归锁(当使用多个互斥锁的时候,用递归锁代替)

递归锁的实现的原理是:其实内部就是个计数器。只要是它大于0,其他人就不可能拿到这把锁。

技术图片
 1 # -*- coding:utf8 -*-
 2 #Author: ZCB
 3 
 4 import threading
 5 import time
 6 
 7 def test1():
 8     r_lock.acquire()  #内部计数器为1
 9     print("in test1")
10     time.sleep(1)
11     r_lock.acquire()  #内部计数器为2
12 
13 
14 def test2():
15     r_lock.acquire()  
16     print("in test2")
17     time.sleep(1)
18     r_lock.acquire()
19 
20 r_lock = threading.RLock()
21 
22 def main():
23     thread1 = threading.Thread(target=test1)
24     thread2 = threading.Thread(target=test2)
25     thread1.start()
26     thread2.start()
27 
28 if __name__ == __main__:
29     main()
递归锁

所以,如果在一个线程中想使用多个锁的时候,可以尝试使用递归锁!

案例-多线程版udp聊天器:

技术图片
 1 from socket import *
 2 import threading
 3 
 4 def recvData(udp_socket):
 5     while True:
 6         recv_data= udp_socket.recvfrom(1024)
 7         print(recv_data[0].decode("utf-8"))
 8 
 9 def sendData(udp_socket,c_addr):
10     while True:
11         send_data = input("to client >>>")
12         udp_socket.sendto(send_data.encode("utf-8"),c_addr)
13 
14 
15 def main():
16     udp_socket  = socket(AF_INET,SOCK_DGRAM)
17 
18     udp_socket.bind(("",9090))
19 
20     dest_ip = input("请输入ip ")
21     dest_port = int(input("请输入 port"))
22 
23     c_addr = (dest_ip,dest_port)
24     thread1 = threading.Thread(target=recvData,args=(udp_socket,))
25     thread2 = threading.Thread(target=sendData,args=(udp_socket,c_addr))
26 
27     thread1.start()
28     thread2.start()
29 
30 if __name__ == __main__:
31     main()
View Code

这里主要做的是用两个线程,一个专门管接收数据。一个专门管发送数据!

 

以上是关于多任务--线程的主要内容,如果未能解决你的问题,请参考以下文章

NodeJs多线程、多进程、定时任务

多线程

多任务编程进程线程

python多线程实现多任务

Python进阶:利用线程实现多任务

1.多线程-了解多线程与高并发