电赛必备知识线程与进程

简介: 电赛必备知识线程与进程

线程和进程


前言


因为在今年的电赛中出现失误,感觉到了自己的不足,那么就要对带你赛进行一下系统的分析。首先我们选择了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()是一个很好玩的东西,他就是:当前线程执行完后其他线程才会继续执行

但是问题也有很多。

  1. 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模块提供的方法


  1. threading.currentThread(): 返回当前的线程变量。
  2. threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  3. 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()
相关文章
|
3月前
|
Java 测试技术 API
【JUC】(1)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!
JUC是什么?你可以说它就是研究Java方面的并发过程。本篇是JUC专栏的第一章!带你了解并行与并发、线程与程序、线程的启动与休眠、打断和等待!全是干货!快快快!
656 2
|
3月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
236 1
|
3月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
250 1
|
11月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
9月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
334 67
|
7月前
|
调度 开发工具 Android开发
【HarmonyOS Next】鸿蒙应用进程和线程详解
进程的定义: 进程是系统进行资源分配的基本单位,是操作系统结构的基础。 在鸿蒙系统中,一个应用下会有三类进程:
297 0
|
10月前
|
SQL 监控 网络协议
YashanDB进程线程体系
YashanDB进程线程体系
|
消息中间件 调度
如何区分进程、线程和协程?看这篇就够了!
本课程主要探讨操作系统中的进程、线程和协程的区别。进程是资源分配的基本单位,具有独立性和隔离性;线程是CPU调度的基本单位,轻量且共享资源,适合并发执行;协程更轻量,由程序自身调度,适合I/O密集型任务。通过学习这些概念,可以更好地理解和应用它们,以实现最优的性能和资源利用。
408 11
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
324 6
|
11月前
|
数据采集 Java 数据处理
Python实用技巧:轻松驾驭多线程与多进程,加速任务执行
在Python编程中,多线程和多进程是提升程序效率的关键工具。多线程适用于I/O密集型任务,如文件读写、网络请求;多进程则适合CPU密集型任务,如科学计算、图像处理。本文详细介绍这两种并发编程方式的基本用法及应用场景,并通过实例代码展示如何使用threading、multiprocessing模块及线程池、进程池来优化程序性能。结合实际案例,帮助读者掌握并发编程技巧,提高程序执行速度和资源利用率。
600 0

热门文章

最新文章