并发编程之多线程

并发编程之多线程

一. 线程基础

1.1 进程的缺点

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。

  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

1.2 什么是线程

线程:能够独立运行的基本单位

进程是资源单位,线程是执行单位。每一个进程中至少有一个线程。

二. 进程和线程的区别

线程与进程的区别可以归纳为以下4点:

  1. 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。

  2. 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

  3. 调度和切换:线程上下文切换比进程上下文切换要快得多。

  4. 在多线程操作系统中,进程不是一个可执行的实体。   

三. 创建多线程

3.1 通过threading.Thread类创建线程

3.1.1 方式一

from threading import Thread
import time
def sayhi(name):
   time.sleep(2)
   print('%s say hello' %name)
   
if __name__ == '__main__':
   t = Thread(target=sayhi,args=('tom',))
   t.start()
   print('主进程')

3.1.2 方式二

from threading import Thread
import time
class Sayhi(Thread):
   def __init__(self,name):
       super().__init__()
       self.name = name
   def run(self):
       time.sleep(2)
       print('%s say hello' % self.name)
       
if __name__ == '__main__':
t = Sayhi('tom')
   t.start()
   print('主进程')

四. 多线程与多进程

4.1 pid的比较

from threading import Thread
from multiprocessing import Process
mport os

def work():
   print('hello',os.getpid())
   
if __name__ == '__main__':
   # part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
   t1 = Thread(target=work)
   t2 = Thread(target=work)
   t1.start()
   t2.start()
   print('主线程:',os.getpid())
   
   # part2:开多个进程,每个进程都有不同的pid
   p1 = Process(target=work)
   p2 = Process(target=work)
   p1.start()
   p2.start()
   print('主进程:',os.getpid())

4.2 开启效率的较量

from threading import Thread
from multiprocessing import Process
import os

def work():
   print('hello')
   
if __name__ == '__main__':
   #在主进程下开启线程
   t = Thread(target=work)
   t.start()
   print('主进程')
   
   '''
  打印结果:
  hello
  主线程/主进程
  '''
   
   #在主进程下开启子进程
   t = Process(target=work)
   t.start()
   print('主进程')
   
   '''
  打印结果:
  主线程/主进程
  hello
  '''

4.3 内存数据的共享问题

from threading import Thread
from multiprocessing import Processing
import os
def work():
   global n
   n = 0
   
if __name__ == '__main__':
    # n=100
   # p=Process(target=work)
   # p.start()
   # p.join()
   # print('主',n) # 毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
   

   n = 1
   t = Threading(target=work)
   t.start()
   t.join()
   print('主',n) # 查看结果为0,因为同一进程内的线程之间共享进程内的数据

五. Thread类的其他方法

Thread实例对象的方法:

  • isAlive():返回线程是否活动的。

  • getName():返回线程名。

  • setName():设置线程名。

threading模块提供的一些方法:

  • threading.currentThread():返回当前的线程变量。

  • threading.enumerate():返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。

  • threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

5.1 代码示例

from threading import Thread
import threading
from threading import Process
import os

def work():
   import time
   time.sleep(3)
   print(threading.current_thread().getName())
   
if __name__ == '__main__':
   #在主进程下开启线程
   t = Thread(target=work)
   t.start()
   
   print(threading.current_thread().getName())
   print(threading.current_thread()) #主线程
   print(threading.enumerate()) #连同主线程在内有两个运行的线程
   print(threading.active_count())
   print('主进程')
   
   '''
  打印结果:
  MainThread
  <_MainThread(MainThread, started 140735268892672)>
  [<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>]
  主线程/主进程
  Thread-1
  '''

5.2 join方法

from threading import Thread
import time
def sayhi(name):
   time.sleep(2)
   print('%s say hello' %name)
   
if __name__ == '__main__':
   t = Thread(target=sayhi,args=('tom',))
   t.start()
   print('主线程')
   print(t.is_alive)
   
   '''
  nick say hello
  主线程
  False
  '''

六. 多线程实现socket

6.1 服务端

import multiprocessing 
import threading
import socket

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)

def action(con):
   while True:
       data = conn.recv(1024)
       print(data)
       conn.send(data.upper())
       
if __name__ == '__main__':
   while True:
       conn,addr = s.accept()
       p = threading.Thread(target=action,args=(conn,))
       p.start()

6.2 客户端

import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',8080))

while True:
msg = input('>>>:').strip()
if not msg:
break

s.send(msg.encode('utf-8'))
data = s.recv(1024)
print(data)

七. 守护线程

7.1 解释

  1. 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。

  2. 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。

7.2 守护线程实例1

from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)

if __name__ == '__main__':
t = Thread(target=sayhi,srgs=('tom',))
t.setDaemon(True)
t.start()
print('主进程')
print(t.is_alive())

'''
主线程
True
'''

7.3 守护线程实例2

from threading import Thread 
import time
def foo():
print(123)
time.sleep(1)
print('end123')

def bar():
print(456)
time.sleep(3)
print('end456')

t1 = Thread(target=foo)
t2 = Thread(target=bar)

t1.daemon = True
t1.start()
t2.start()
print('main......')

八. 锁--信号量--Event事件

8.1 同步锁

三个需要注意的点:
#1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来

#2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高

8.1.1 多个线程抢占资源的情况

from threading import Thread
import os,time
def work():
global n
temp = n
time.sleep(0.1)
n = temp - 1

if __name__ = '__main__':
n = 100
l = []
for i in range(100):
p = Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
print(n) #结果可能为99

8.1.2 同步锁的引用

from threading import Thread,Lock
import time

def work():
global n
lock.acquire()
temp = n
time.sleep(0.01)
n = temp - 1
lock.release()

if __name__ == '__main__':
lock = Lock()
n = 100
l_list = []
for i in range(100):
q = Thread(target=work)
q.start()
l_list.append(q)
for q in l_list:
q.join()
print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全

8.1.3 互斥锁与join的区别

#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock
import os,time
def task():
global n
print('%s is running' %current_thread().getName())
temp=n
time.sleep(0.5)
n=temp-1


if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()

stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''


#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
#未加锁的代码并发运行
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
#加锁的代码串行运行
lock.acquire()
temp=n
time.sleep(0.5)
n=temp-1
lock.release()

if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''

# 在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是

# start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的

# 单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
temp=n
time.sleep(0.5)
n=temp-1


if __name__ == '__main__':
n=100
lock=Lock()
start_time=time.time()
for i in range(100):
t=Thread(target=task)
t.start()
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
'''

九. 死锁与递归锁

9.1 死锁

死锁:是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

from threading import Lock as Lock
import time
mutexA = Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

解决方法:递归锁

9.2 递归锁RLock

RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

from threading import RLock as Lock
import time
mutexA = Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

9.3 典型问题:科学家吃面

9.3.1 死锁问题
import time
from threading import Thread, Lock
noodle_lock = Lock()
fork_lock = lock()
def eat1():
noodle_lock.acquire()
print('%s抢到了面条'%name)
fork_lock.acquire()
print('%s抢到了叉子'%name)
print('%s吃面'%name)
fork_lock.release()
noodle_lock.release()

def eat2():
fork_lock.acquire()
print('%s抢到了叉子'%name)
time.sleep(1)
noodle_lock.acquire()
print('%s抢到了面条'%name)
print('%s吃面'%name)
noodle_lock.release()
fork_lock.release()

for name in ['tom','lili','momo']:
t1 = Thread(target=eat1,args=(name,))
t2 = Thread(target=eat2,args=(name,))
t1.start()
t2.start()

9.3.2 递归锁解决死锁问题

import time
from threading import Thread,RLock
fork_lock = noodle_lock = RLock()
def eat1(name):
noodle_lock.acquire()
print('%s 抢到了面条'%name)
fork_lock.acquire()
print('%s 抢到了叉子'%name)
print('%s 吃面'%name)
fork_lock.release()
noodle_lock.release()

def eat2(name):
fork_lock.acquire()
print('%s 抢到了叉子' % name)
time.sleep(1)
noodle_lock.acquire()
print('%s 抢到了面条' % name)
print('%s 吃面' % name)
noodle_lock.release()
fork_lock.release()

for name in ['哪吒','lqz','egon']:
t1 = Thread(target=eat1,args=(name,))
t2 = Thread(target=eat2,args=(name,))
t1.start()
t2.start()

十. 信号量(Semaphore)

同进程的一样

Semaphore管理一个内置的计数器, 每当调用acquire()时内置计数器-1; 调用release() 时内置计数器+1; 计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

#   Semaphore:信号量可以理解为多把锁,同时允许多个线程来更改数据
from threading import Thread,Semaphore
import time
import random
sm=Semaphore(5)

def task(name):
sm.acquire()
print('%s正在蹲坑'%name)

time.sleep(random.randint(1,3))
sm.release()

if __name__ == '__main__':
for i in range(100):
t=Thread(target=task,args=(i,))
t.start()

十一. Event事件

同进程的一样

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

event.isSet():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。

img

例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作

from threading import Thread,Event
import threading
import time,random
def conn_mysql():
count=1
while not event.is_set():
if count > 3:
raise TimeoutError('链接超时')
print('<%s>第%s次尝试链接' % (threading.current_thread().getName(), count))
event.wait(0.5)
count+=1
print('<%s>链接成功' %threading.current_thread().getName())


def check_mysql():
print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())
time.sleep(random.randint(2,4))
event.set()
if __name__ == '__main__':
event=Event()
conn1=Thread(target=conn_mysql)
conn2=Thread(target=conn_mysql)
check=Thread(target=check_mysql)

conn1.start()
conn2.start()
check.start()

十二. 定时器

定时器:指定n秒后执行某操作

from threading import Timer

def hello():
print('hello,world')

t = Timer(1,hello)
t.start()

验证码定时器

from threading import Timer
import random,time

class Code:
def __init__(self):
self.make_cache()

def make_cache(self,interval=5):
self.cache = self.make_code()
print(self.cache)
self.t = Timer(interval,self.make_cache)
self.t.start()

def make_code(self,n=4):
res = ''
for i in range(n):
s1 = str(random.randint(0,9))
s2 = str(random.randint(65,90))
res += random.choice([s1,s2])
return res

def check(self):
while True:
inp = input('>>>:').strip()
if inp.upper() == self.cache:
print('验证成功',end='\n')
self.t.cancel()
break

if __name__ == '__main__':
obj = Code()
obj.check()

 

 

 

 

posted @ 2021-09-26 11:00  vonmo  阅读(43)  评论(0编辑  收藏  举报