目录
一、单任务
当唱歌执行完了之后再执行跳舞
from time import sleep
def sing():
for i in range(3):
print("正在唱歌。。。。%d" % i)
sleep(1)
def dance():
for i in range(3):
print("正在跳舞。。。%d" % i)
sleep(1)
if __name__ == "__main__":
sing()
dance()
二、多任务
看电影的时候既可以看画面又可以听音频就是多任务。一个人同时唱歌和跳舞就是多任务。
操作系统可以运行多个程序就是多任务的操作系统。如果一个程序是多任务的就是多任务的程序。(也就是说一个程序可以同时执行多个部分的代码)
from time import sleep
import threading
def sing():
for i in range(3):
print("正在唱歌。。。。%d" % i)
sleep(1)
def dance():
for i in range(3):
print("正在跳舞。。。%d" % i)
sleep(1)
if __name__ == "__main__":
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
三、多任务执行的原理
一个cpu是单核默认可以执行一个程序,如果想要多个程序一起执行,理论上就需要多个cpu执行
4核指的是同一时刻有四个任务真正执行。
如果一个cpu是四核,理论上同时只能有4个任务一起执行,但是事实上却可以运行多个程序,之所以有这个现象是因为操作系统控制cpu,让cpu做了一个特殊的事情,一会儿运行一个程序,然后快速的运行另外一个程序,再运行另外的程序,以此类推,实现了多个任务看上去"一起"运行。
操作系统为了让多个程序,都能够得到执行的机会,采用了一系列的方案来实现,例如:时间片调度。
四、并发、并行
并发:是对一个假的多任务的描述
并行:是真的多任务的描述
32核的服务器:一个cpu有多个核,多个cpu焊在一起的。
任务数大于cpu核数就是假的多任务也就是并发,当任务数小于或者等于cpu核数就是真的多任务。也就是并行。
五、线程
python中实现多任务有两种:进程和线程
线程:可以想象成程序在执行代码时的那个执行流。执行流可以想象成一个箭头,这个箭头指到哪就执行哪个部分的代码。当创建出多个线程,就可以想象成有多个箭头。多个箭头指向哪就一起执行。
当一个程序运行时,默认有一个线程,这个线程称为主线程。多任务也就是可以理解为让你的代码再运行的过程中额外创建一些线程,让这些线程去执行代码。
python中可以使用线程来实现多任务。可以使用thread模块,但是它比较底层,即意味着过程较为不方便使用;推荐使用threading模块,它是对thread做了一些包装的,可以更加方便使用。
六、threading模块---单任务
import threading
import time
def say_hello():
print("hello word!!!")
time.sleep(1)
for i in range(5):
t = threading.Thread(target=say_hello)
t.start()
target=say_hello而不是say_hello()。say_hello指的是传入这个函数。而say_hello()指的是传入这个函数的返回值。
import threading
import time
def task_1():
while True:
print(11111)
time.sleep(1)
t1 = threading.Thread(target=task_1)
t1.start()
while True:
print(22222222)
time.sleep(1)
主线程和子线程的顺序不一定。由操作系统决定
七、threading模块---多线程实现多任务
import threading
import time
def task_1():
while True:
print(11111)
time.sleep(1)
def task_2():
while True:
print(33333)
time.sleep(0.2)
t1 = threading.Thread(target=task_1)
t2 = threading.Thread(target=task_2)
t1.start()
t2.start()
while True:
print(22222222)
time.sleep(1)
小结:①想要执行一个单独的任务,那么就需要创建一个新的线程,创建出来的线程是不会执行的,只有调用了start方法才会让这个线程开始执行代码。
②一个程序中多个任务一起运行,就想办法创建多个Thread对象。
③多线程执行的顺序是不确定,因为在执行代码的时候,当前的运行环境可能不同以及分配可能不同,导致了操作系统在计算接下来应该调用那个程序的时候得到了不一样的答案,因此顺序不同
八、threading模块---多线程执行相同代码
import threading
import time
def say_hello():
for i in range(5):
print("hello word!!!")
time.sleep(1)
t1 = threading.Thread(target=say_hello)
t2 = threading.Thread(target=say_hello)
t1.start()
t2.start()
小结:
①一个程序中,可以有多个线程,执行相同的代码,但是每个线程执行每个线程的功能,互不影响,仅仅是做的事情形同罢了。
②当在创建Thread对象是target执行的函数的代码执行完了之后,意味着这个子线程接收。
③虽然主线程没有了代码,但是它依然会等待所有的子线程结束之后,它才会真正的结束,原因是:主线程有个特殊的功能用来对子线程产生的垃圾进行回收处理
④当主线程接收之后,才意味着整个程序真正的结束
九、查看线程数量
import threading
from time import sleep
print(threading.enumerate())
def task1():
for i in range(5):
print(111111)
sleep(1)
t1 = threading.Thread(target=task1)
print(threading.enumerate())
t1.start()
print(threading.enumerate())
sleep(6)
print(threading.enumerate())
小结:
①创建一个Thread对象根本不会创建一个线程,仅仅是一个对象罢了,而当调用它的start方法之后,才会真正创建一个新的子线程
十、threading模块---创建线程对象时传递参数
10.1一个参数:
import threading
from time import sleep
def print_lines(num):
for i in range(num):
print("1111")
sleep(0.1)
t = threading.Thread(target=print_lines, args=(5,))
t.start()
10.2 两个参数:
import threading
from time import sleep
def print_lines(num, num2):
for i in range(num):
print("1111")
sleep(0.1)
for i in range(num2):
print("2222")
sleep(0.1)
t = threading.Thread(target=print_lines, args=(1, 2))
t.start()
10.3 传递命名参数
import threading
from time import sleep
def print_lines(num, num2, num3, n):
for i in range(num):
print("1111")
sleep(0.1)
for i in range(num2):
print("2222")
sleep(0.1)
for i in range(num3):
print("3333")
sleep(0.1)
for i in range(n):
print("4444")
sleep(0.1)
t = threading.Thread(target=print_lines, args=(1, 2), kwargs={"n": 4, "num3": 2})
t.start()
十一、自定义类来创建线程
import threading
import time
class Task(threading.Thread):
def run(self):
print("1111")
time.sleep(1)
u_t = Task()
u_t.start() # start()去调用run方法
print("main")
time.sleep(1)
运行结果:
小结:
①类里面必须要实现run()方法
②创建的对象调用start()方法。由start()方法来调用子类的run()方法。而不是直接调用run()方法
③run()方法什么时候执行完意味着这个线程什么时候结束
④如果除了run()方法,自定义类里面还有其他方法,这些方法需要在run()方法中自己去调用,线程是不会自己调用的
十二、自定义类来创建线程实例----并发TCP服务器
import socket
import threading
import sys
class HandleData(threading.Thread):
def __init__(self, client_socket): # 需要传入自己的套接字,又需要父类的其他属性
super().__init__()
self.client_socket = client_socket
def run(self):
# 5.接收/发送数据
while True:
recv_content = self.client_socket.recv(1024)
if len(recv_content) != 0:
print(recv_content)
self.client_socket.send(recv_content)
else:
self.client_socket.close()
break
class TCPServer(threading.Thread):
def __init__(self, port):
super().__init__()
# 1.创建套接字
self.server_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.绑定本地信息
self.server_s.bind(("", port))
# 3.将套接字由默认的主动连接模式改为被动模式(监听模块)
self.server_s.listen(128)
def run(self):
while True:
# 4.等待客户端进行连接。new_s 是新的套接字,client_info是客户端的ip、port
new_s, client_info = self.server_s.accept()
print(client_info)
# 创建一个线程,为刚刚连接的客户端服务
handle_data_thread = HandleData(new_s)
handle_data_thread.start()
def __del__(self):
# 6.关闭套接字
self.server_s.close()
def main():
"""控制整体"""
# python xxx.py 8888
if len(sys.argv) == 2:
port = sys.argv[1]
if port.isdigit():
port = int(port)
else:
print("运行方式如:python xxx.py 8888")
return
print("http服务器使用的port:%s" % port)
tcp_server = TCPServer(port)
tcp_server.start()
if __name__ == "__main__":
main()
小结:
①tcp使用的是套接字,套接字是全双工的。
十三、线程之间共享全局变量
类名大驼峰,变量名是下划线,全大写往往是全局变量
import threading
import time
# 1、定义一个全局变量
g_num = 0
# 2、定义两个函数,用他们来充当线程要执行的代码
def task1():
global g_num
g_num = 100
print("在task1中,g_num=%d" % g_num)
def task2():
print("在task2中,g_num=%d" % g_num)
# 3、创建线程对象
t1 = threading.Thread(target=task1)
t2 = threading.Thread(target=task2)
# 4、调用start创建线程,让线程开始运行
t1.start()
time.sleep(2) # 让主线程延迟一会儿,保证让task1这个任务先执行完毕
t2.start()
小结:
①如果一个程序有多个线程,每个线程可以单独执行自己的代码,多个线程之间需要共享数据,最简单的方式是通过全局变量来实现。一个线程修改全局变量,另外一个线程从全局变量中获取数据
十四、线程之间共享全局变量---资源竞争
通过全局变量可以解决多个线程之间共享数据的问题,但是,如果使用不当,会导致数据错乱。
例如:有三个线程,线程1和线程2修改全局变量。线程3需要读取线程1修改后的数据,由于操作系统调度的问题,可能线程3读取到的是线程2修改后的数据
import threading
# 1、定义一个全局变量
g_num = 0
# 2、定义两个函数,用他们来充当线程要执行的代码
def task1(num):
global g_num
for i in range(num):
g_num += 1
print("在task1中,g_num=%d" % g_num)
def task2(num):
global g_num
for i in range(num):
g_num += 1
print("在task2中,g_num=%d" % g_num)
# 3、创建线程对象
t1 = threading.Thread(target=task1, args=(100,))
t2 = threading.Thread(target=task2, args=(100,))
# 4、调用start创建线程,让线程开始运行
t1.start()
t2.start()
import threading
# 1、定义一个全局变量
g_num = 0
# 2、定义两个函数,用他们来充当线程要执行的代码
def task1(num):
global g_num
for i in range(num):
g_num += 1
print("在task1中,g_num=%d" % g_num)
def task2(num):
global g_num
for i in range(num):
g_num += 1
print("在task2中,g_num=%d" % g_num)
# 3、创建线程对象
t1 = threading.Thread(target=task1, args=(1000000,))
t2 = threading.Thread(target=task2, args=(1000000,))
# 4、调用start创建线程,让线程开始运行
t1.start()
t2.start()
对比这两个程序的结果,如果传入的num值很大,例如这里的1000000,就会发现,task1中,g_num=1490414。如果你多执行几遍,就会发现结果又不一样。按理说应该是task1中g_num=1000000,task2中,g_num=2000000。为什么会出现这样的结果呢?是因为,如果task1执行的时候获取到g_num的值为0,然后cpu让task1先不执行之后的动作,让task2获取到g_num的值为0,然后进行运算之后,g_num的值为1,然后赋值给全局变量g_num为1。这时,task1继续执行,因为之前获取的是g_num是0,所以这里运算后g_num的值就为1,然后赋值给全局变量g_num=1。所以其实task1和task2最终获取到的g_num的值都为1。
也就是说。一个线程并不是在另一个线程执行完了之后再去执行,有可能线程task1执行了一部分,比如说获取到了g_num的值为0,这时候task2就去竞争资源,去执行自己的代码。
十五、资源竞争的解决办法---互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下的数据的正确性。
import threading
# 创建一个互斥锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
print('哈哈哈哈')
# 解锁
mutex.release()
如果想要对某些代码一起执行,不行被其他线程打扰,可以将这些代码放到互斥锁上锁和解锁之间
import threading
# 创建一个互斥锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
print('哈哈哈哈')
# 再次上锁
mutex.acquire()
# 解锁
mutex.release()
如果这个互斥锁已经被上锁,那么在这个锁被解开之前,是不能再次上锁的,也就是说:如果这个锁在解开之前谁要是再次调用acquire()对其上锁,那么谁就被堵塞,直到这个互斥锁被解锁。一定是同一把锁。
import threading
# 5、定义一个全局的的变量,来存储互斥锁
mutex = threading.Lock()
# 1、定义一个全局变量
g_num = 0
# 2、定义两个函数,用他们来充当线程要执行的代码
def task1(num):
global g_num
# 上锁
mutex.acquire()
for i in range(num):
g_num += 1
# 解锁
mutex.release()
print("在task1中,g_num=%d" % g_num)
def task2(num):
global g_num
# 上锁
mutex.acquire()
for i in range(num):
g_num += 1
# 解锁
mutex.release()
print("在task2中,g_num=%d" % g_num)
# 3、创建线程对象
t1 = threading.Thread(target=task1, args=(1000000,))
t2 = threading.Thread(target=task2, args=(1000000,))
# 4、调用start创建线程,让线程开始运行
t1.start()
t2.start()