电赛必备知识线程与进程

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

线程和进程


前言


因为在今年的电赛中出现失误,感觉到了自己的不足,那么就要对带你赛进行一下系统的分析。首先我们选择了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()
相关文章
|
26天前
|
消息中间件 并行计算 安全
进程、线程、协程
【10月更文挑战第16天】进程、线程和协程是计算机程序执行的三种基本形式。进程是操作系统资源分配和调度的基本单位,具有独立的内存空间,稳定性高但资源消耗大。线程是进程内的执行单元,共享内存,轻量级且并发性好,但同步复杂。协程是用户态的轻量级调度单位,适用于高并发和IO密集型任务,资源消耗最小,但不支持多核并行。
40 1
|
5天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
10天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
7天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
17 1
|
12天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
27 2
|
14天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
24天前
|
Python
Python中的多线程与多进程
本文将探讨Python中多线程和多进程的基本概念、使用场景以及实现方式。通过对比分析,我们将了解何时使用多线程或多进程更为合适,并提供一些实用的代码示例来帮助读者更好地理解这两种并发编程技术。
|
27天前
|
消息中间件 并行计算 安全
进程、线程、协程
【10月更文挑战第15天】进程、线程和协程是操作系统中三种不同的执行单元。进程是资源分配和调度的基本单位,每个进程有独立的内存空间;线程是进程内的执行路径,共享进程资源,切换成本较低;协程则更轻量,由用户态调度,适合处理高并发和IO密集型任务。进程提供高隔离性和安全性,线程支持高并发,协程则在资源消耗和调度灵活性方面表现优异。
44 2
|
1月前
|
算法 安全 调度
深入理解操作系统:进程与线程的管理
【10月更文挑战第9天】在数字世界的心脏跳动着的,不是别的,正是操作系统。它如同一位无形的指挥家,协调着硬件与软件的和谐合作。本文将揭开操作系统中进程与线程管理的神秘面纱,通过浅显易懂的语言和生动的比喻,带你走进这一复杂而又精妙的世界。我们将从进程的诞生讲起,探索线程的微妙关系,直至深入内核,理解调度算法的智慧。让我们一起跟随代码的脚步,解锁操作系统的更多秘密。
37 1
|
18天前
|
Linux 调度
探索操作系统核心:进程与线程管理
【10月更文挑战第24天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是计算机硬件与软件之间的桥梁,更是管理和调度资源的大管家。本文将深入探讨操作系统的两大基石——进程与线程,揭示它们如何协同工作以确保系统运行得井井有条。通过深入浅出的解释和直观的代码示例,我们将一起解锁操作系统的管理奥秘,理解其对计算任务高效执行的影响。

热门文章

最新文章

相关实验场景

更多