线程和进程
前言
因为在今年的电赛中出现失误,感觉到了自己的不足,那么就要对带你赛进行一下系统的分析。首先我们选择了D题目,也就是基于互联网的视觉测量系统。这个题目要同时协同两个相机进行测量。但是与此同时就是双线程等知识。但是这些知识我只是平时学习一些基础,并没有深入的学习其中的问题,例如互斥锁,同步,异步等。赛前没有充分准备,这是我在比赛前期力不从心,不知道怎么梳理逻辑关系。所以在比赛结束后,写下这个文章。那么为了明年不在留下遗憾,就不能停下学习的脚步。那么后续学习的路线基本上定为:
线程与进程
Qt编程
分析线程与进程
由于进程是资源拥有者,创建、撤消与切换存在较大的内存开销,因此需要引入轻型进程,即线程,进程是资源分配的最小单位,线程是CPU调度的最小单位(程序真正执行的时候调用的是线程).每一个进程中至少有一个线程。
举个例子就是:我们的电脑每次使用的时候我们都会打开很多的进程,这些进程简单的来说可能是QQ,微信,网抑云,浏览器。那么多进程处理的意思就是,我看着网页还能听歌,对吧这个时候我们的电脑就在同时处理两个进程。那什么是线程。就是你看微信,不可能跟这个人发短信,就接收不了那个人的短信了,对吧。这说明你的电脑在多个线程的工作,保证你可以同时接受很多人的短信,这个比方不恰当但是还是比较形象的。我们从刚刚的例子里可以知道的是,进程内包含了线程,而进程内还有主进程和子进程之分。
使用Python实现简单线程
from threading import Thread import time def sing(): for i in range(3): print('正在唱歌。。。', i) time.sleep(2) def dance(): for i in range(3): print('正在跳舞。。。', i) time.sleep(2) def main(): t1 = Thread(target=sing) t2 = Thread(target=dance) t1.start() t2.start() if __name__ == '__main__': main() print('程序结束。。。')
我们仔细观察这个代码,这个时候我们的主进程中包含了三个线程,其中两个为我们自己创建的,我们叫他子线程,而程序自身的叫做主线程。那么这个程序就要等待所有的线程都结束才可以结束,而且程序不会因为某一个线程的结束而结束。这就是线程的一个特点。我们使用target来指定子线程要进行的工作是什么,这里分别是sing和dance两个函数。
线程中传递参数
给函数传递参数,使用线程的关键字 args=()进行传递参数。
我们可以书写一个代码来测试一下。
''' Author: scc Date: 2021-11-08 09:51:38 LastEditTime: 2021-11-08 09:58:31 FilePath: /项目文件/线程与进程/简单线程和参数传递.py ''' import time from threading import Thread def sing(num): for i in range(num): print("sing") time.sleep(2) def dance(num): for i in range(num): print("dance") time.sleep(2) def main(): t1 = Thread(target = sing, args = (2,)) t2 = Thread(target = dance, args = (2,)) t1.start() t2.start() if __name__ == "__main__": main() print("程序结束")
我们仔细看args的位置,我们会发现args里的参数很奇怪。这个args的位置需要的是一个tuple类型的参数。其实翻译过来即使我们python中的元祖类型,它类似于我们的列表,但是不同的是他不能进行修改,所以他可以是我们的代码中的数据更加安全,但是我们会发现其实我们的tuple可以被重新赋值,如果我们要创建一个只包含一个数字的tuple的话,就要在那个数字后面加上逗号。后面在记录这个笔记的时候再遇到tuple,便不在赘述。
join()的使用方法
join()是一个很好玩的东西,他就是:当前线程执行完后其他线程才会继续执行。
但是问题也有很多。
- join的安放位置是不是会影响程序的运行方式。
- 答:会
怎么能看出区别呢?我写了一段代码
''' Author: 史川诚 Date: 2021-11-08 10:14:39 LastEditTime: 2021-11-08 10:22:18 FilePath: /项目文件/线程与进程/线程中的join用法.py ''' import threading import time from threading import Thread def sing(): for i in range(3): print("sing") time.sleep(2) def dance(): for i in range(3): print("dance") time.sleep(10) def main(): t1 = threading.Thread(target = sing) t2 = threading.Thread(target = dance) t1.start() t1.join() t2.start() #t1.join() if __name__ == "__main__": main() print("程序结束")
这里我们可以看到join函数被我放到了第109行和111行。那么这么放置的区别是什么。我逐个测试了一下,首先我们运行109行的join,我们会发现线程2(t2)不运行,原因是其他的线程都在等待线程1执行完毕。其次就是111行,我们会发现t1和t2都可以执行,但是主线程不运行。也就是说t2在111行的join中并没有被限制住。
SetDaemon方法
启动线程前设置thread.setDaemon(True) 即 设置该线程为守护线程,
表示该线程是不重要的,进程退出时不需要等待这个线程执行完成。
这样做的意义在于:避免子线程无限死循环,导致退不出程序,也就是避免传说中的孤儿进程。
''' Author: 史川诚 Date: 2021-11-08 10:36:25 LastEditTime: 2021-11-08 11:21:46 FilePath: /项目文件/线程与进程/守护线程.py ''' import time from threading import Thread def sing(): for i in range(3): print("sing") time.sleep(2) def dance(): for i in range(3): print("dance") time.sleep(2) def main(): t1 = Thread(target = sing) t2 = Thread(target = dance) t1.setDaemon(True) t2.setDaemon(True) t1.start() t2.start() if __name__ == "__main__": main() print("程序结束")
我们会发现当我们的守护线程为t1和t2.用户线程为子进程。所以在主线程结束后,我们的两个守护线程一并销毁。
Tips:当我把t2的setDaemon的设置为False的时候,我发现主线程结束后并没有让守护线程结束搜索了很多文章,最后得到了以下结
论,总结一下就是:守护线程就是服务线程,而其他的就是用户线程,当主线程结束后,用户线程还会存在,直到运行完毕,但是服务线程在无用户可服务时就会自我销毁,也就是说我们的用户只要存在,守护就不会消失。
所以当我的t2为用户线程时,我的t1分别有两个用户,也就是主线程和t2线程。那么我的主线程在结束之后,我们的t2子线程还在继续。所以我们的守护线程和t2线程才会继续运行。
线程对象的实例方法
- setName() 定义线程的名字
- getName() 获取线程的名字
- is_alive() 获取当前线程是否存活,存活返回True,销毁返回False
''' Author: 史川诚 Date: 2021-11-08 12:32:33 LastEditTime: 2021-11-08 12:41:42 FilePath: /项目文件/线程与进程/线程的实例方法.py ''' import time from threading import Thread def sing(num): for i in range(num): print("sing") time.sleep(3) def dance(num): for i in range(num): print("dance") time.sleep(3) def main(): t1 = Thread(target = sing, args = (3,)) t2 = Thread(target = dance, args = (3,)) t1.setName("线程1") t2.setName("线程2") print(t1.getName()) print(t2.getName()) t1.start() print(t1.is_alive()) t2.start() print(t1.is_alive()) if __name__ == "__main__": main() print("程序结束")
- setName函数,也就是对线程进行命名,我们将线程进行命名的目的就是为了后续我们获取线程内信息时,不会出现Thread—1这种用梳子排序的编号,线程目的一目了然。这样方便我们后续的代码debug。(具体效果可以往下看(threading模块提供的方法))
- getName函数的用处就是在我们设置了现场的名称之后,我们利用get来获取我们的线程名称。
- is_alive是线程是否存活的一个检测,他会返回true或者是False。当返回True的时候就是说明,线程还未被销毁,仍然在运行。当返回False的时候,代表我们的线程已经被销毁。这个时候我们可以具体分析。
threading模块提供的方法
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount():返回正在运行的线程的数量,与len(threading.enumerate())的效果一样。
''' Author: 史川诚 Date: 2021-11-08 12:47:33 LastEditTime: 2021-11-08 21:03:34 FilePath: /项目文件/线程与进程/threading模块提供的方法.py ''' import time import threading def sing(num): for i in range(num): print("sing") time.sleep(2) print(threading.currentThread()) # <Thread(Thread-1, started 123145492099072)> def dance(num): for i in range(num): print("dance") time.sleep(2) print(threading.currentThread()) # <Thread(Thread-2, started 123145560637440)> def main(): t1 = threading.Thread(target = sing, args = (3,)) t2 = threading.Thread(target = dance, args = (3,)) t1.setName("makabaka") t1.start() t2.start() if __name__ == "__main__": main() print(threading.enumerate())# [<_MainThread(MainThread, started 4777426368)>, <Thread(makabaka, started 123145320669184)>, <Thread(Thread-2, started 123145337458688)>] print(threading.active_count())# 3 print("程序结束") print(threading.currentThread()) # <_MainThread(MainThread, started 4723908032)>
这里我写了三个注释,我们可以看到dance,sing,main着三段的线程信息。主线程的现显示是MainThread,Thread-1,Thread-2。这几个就是他目前的线程名字,这里我就知道setname的作用了。因为我们线程数量多了之后不可能一个一个记录1,2,3,4,5,6都是谁,我们只需要给线程进行命名即可。
然后我们再去看enumerate函数返回的线程列表。这个就是说明我们的线程都有哪些。
最后是我们的activatecount函数,他可以返回我们运行中的线程的个数。
使用继承的方法开启线程
''' Author: 史川诚 Date: 2021-11-08 21:14:34 LastEditTime: 2021-11-08 21:30:02 FilePath: /项目文件/线程与进程/使用继承方式开启线程.py ''' import time import threading class MyThread(threading.Thread): def __init__(self, num): super().__init__() self.num = num def run(self): for i in range(self.num): print("makabaka") time.sleep(2) if __name__ == "__main__": MyThread = MyThread(3) MyThread.start()
这个代码比较容易理解。不再过多赘述。
线程之间共享全局变量
''' Author: 史川诚 Date: 2021-11-09 09:19:51 LastEditTime: 2021-11-09 09:23:34 FilePath: /线程与进程/线程之间共享全局变量.py ''' import time import threading g_num = 100 # 全局变量 def test1(): global g_num g_num += 1 print("test1---->>",g_num) time.sleep(2) def test2(): print("test2---->>",g_num) time.sleep(2) def main(): t1 = threading.Thread(target = test1) t2 = threading.Thread(target = test2) t1.start() t2.start() if __name__ == "__main__": main()
这个代码我们可以看出,首先我们声明一个全局变量g_num,之后我们再去定义我们各个线程里面所涵盖的内容,我们在test1这个函数里面,声明全局变量g_num,这样我们的代码中的g_num就链接起来了,也就是说我现在不管是在函数体以外修改,还是说在函数体内进行修改,都可以让g_num的值出现相应的改变。我们最后输出的时候,tset1---->>g_num的这个g_num的值就是我们全局变量的值。
共享全局变量的问题
''' Author: 史川诚 Date: 2021-11-09 09:31:46 LastEditTime: 2021-11-09 10:00:49 FilePath: /线程与进程/共享全局变量的问题.py ''' import threading g_num = 0 def test1(num): global g_num for i in range(num): g_num += 1 print("test1------>>", g_num) #test1----->> 1252671 def test2(num): global g_num for i in range(num): g_num += 1 print("test2------>>", g_num) # test2------>> 1357265 def main(): t1 = threading.Thread(target = test1, args = (1000000,)) t2 = threading.Thread(target = test2, args = (1000000,)) t1.start() t2.start() if __name__ == "__main__": main()
按道理来说的话,t1线程对g_num进行了100万次加一操作,t2也是如此,那么最后的结果应该是200万呀。但是实际不是这样的。
注意:g_num+=1 在真正执行的时候会解析很多代码
1.先获取g_num的值。
2.获取的值+1
3.把结果保存到g_num中
我们现在来模拟一下cpu执行,首先我们的tset1获取cpu,执行了g_num的前两步,然后test2获取了cpu,执行了g_num三步,现在g_num == 1,然后test1又获取了cpu,执行他的第三步g_num == 1将之前的值就覆盖了。以此往复来回的覆盖最终出现这个效果。
同步与异步
同步的意思很好理解,就是所谓的协同步调,按预定的先后次序执行。就像排队办手续一样,我办完,就到你来办。但是刚开始很多人(包括我),都把同步理解为一起做一个东西,做一个动作,但其实不是这样的。同步是协同,协助,互相配合。
例如线程同步,可以理解为线程A和B一块配合工作,A执行到一定程度要依靠B的某个结果,于是停下来示意B执行,B执行完将执行结果给A,A再继续执行。
那么总结一下就是,相互强依赖。也就是说,我的一个动作,必须在另一个人做完一个事情后才可以做,否则我就一直等,中间少一步都不可以,或者说中间那一个环节出错都不可以。
异步恰巧相反,两者并不强依赖,A对B的响应时间不care,无论B是否返回,A都可以做自己的事情,B响应了又返回了,A就返回去做那个事情。B没响应,那A干自己的,也就是说A不存在等待的概念。
打个比方:一男一女搞对象,这个女生很忙,这个男生很闲,早上男生发了一“早”,女生没有回复,男生就躺床上一直盯着手机,啥也不干,就盯着,等到晚上,女生回了一句“晚安”,然后男生一天啥也没干,我们把这个行为叫做。。。舔狗。但是这也就是同步强依赖的反应。那么男生如果发了句早,然后干自己的事情,等女生回复他之后,他在继续和她聊。是不是就是我们说的异步。
互斥锁
当多个线程几乎同时修改一个共享数据的时候,需要进行同步控制,线程同步能够保证多个线程安全的访问竞争资源(全局内容),最简单的同步机制就是使用互斥锁。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为锁定状态,其他线程就能更改,直到该线程将资源状态改为非锁定状态,也就是释放资源,其他的线程才能再次锁定资源。互斥锁保证了每一次只有一个线程进入写入操作。从而保证了多线程下数据的安全性。
看到这里是不是想到了那个200万的问题,没错那个问题就可以这样改正。
''' Author: scc Date: 2021-11-09 15:32:56 LastEditTime: 2021-11-09 15:57:37 FilePath: /线程与进程/互斥锁.py ''' import time import threading g_num = 0 def test1(num): global g_num lock.acquire() # 上锁 for i in range(num): g_num += 1 lock.release() # 释放 print("test1----->>",g_num) def test2(num): global g_num lock.acquire() # 上锁 for i in range(num): g_num += 1 lock.release() # 释放 print("test2----->>",g_num) lock = threading.Lock() # 设置一个互斥锁 def main(): t1 = threading.Thread(target = test1, args = (1000000,)) t2 = threading.Thread(target = test2, args = (1000000,)) t1.start() t2.start() if __name__ == "__main__": main()