1.线程安全问题
1.1 线程访问全局变量
import threading
g_num = 0
def test(n):
global g_num
for x in range(n):
g_num += x
g_num -= x
print(g_num)
if __name__ == '__main__':
t1 = threading.Thread(target=test, args=(10,))
t2 = threading.Thread(target=test, args=(10,))
t1.start()
t2.start()
在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据。缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)。
1.2 线程的安全问题
import threading
import time
ticket = 20
def sell_ticket():
global ticket
while True:
if ticket > 0:
time.sleep(0.5)
ticket -= 1
print('{}卖了一张票,还剩{}'.format(threading.current_thread().name, ticket))
else:
print('{}票卖完了'.format(threading.current_thread().name))
break
for i in range(5):
t = threading.Thread(target=sell_ticket, name='thread-{}'.format(i + 1))
t.start()
2.线程锁
2.1 同步
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。同步就是协同步调,按预定的先后次序进行运行。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
2.2 互斥锁
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
注意:
-
如果这个锁之前是没有上锁的,那么acquire不会堵塞
-
如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止。
-
和文件操作一样,Lock也可以使用with语句快速的实现打开和关闭操作。
2.3 使用互斥锁解决卖票问题
import threading
import time
ticket = 20
lock = threading.Lock()
def sell_ticket():
global ticket
while True:
lock.acquire()
if ticket > 0:
time.sleep(0.5)
ticket -= 1
lock.release()
print('{}卖了一张票,还剩{}'.format(threading.current_thread().name, ticket))
else:
print('{}票卖完了'.format(threading.current_thread().name))
lock.release()
break
for i in range(5):
t = threading.Thread(target=sell_ticket, name='thread-{}'.format(i + 1))
t.start()
2.4 上锁过程:
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
总结
锁的好处:
-
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
-
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。
-
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
3.线程间通信
3.1 线程间通过
线程之间有时需要通信,操作系统提供了很多机制来实现进程间的通信,其中我们使用最多的是队列Queue.
3.2 Queue的原理
Queue是一个先进先出(First In First Out)的队列,主进程中创建一个Queue对象,并作为参数传入子进程,两者之间通过put( )放入数据,通过get( )取出数据,执行了get( )函数之后队列中的数据会被同时删除,可以使用multiprocessing模块的Queue实现多进程之间的数据传递。
import threading
import time
from queue import Queue
def producer(queue):
for i in range(100):
print('{}存入了{}'.format(threading.current_thread().name, i))
queue.put(i)
time.sleep(0.1)
return
def consumer(queue):
for x in range(100):
value = queue.get()
print('{}取到了{}'.format(threading.current_thread().name, value))
time.sleep(0.1)
if not value:
return
if __name__ == '__main__':
queue = Queue()
t1 = threading.Thread(target=producer, args=(queue,))
t2 = threading.Thread(target=consumer, args=(queue,))
t3 = threading.Thread(target=consumer, args=(queue,))
t4 = threading.Thread(target=consumer, args=(queue,))
t6 = threading.Thread(target=consumer, args=(queue,))
t1.start()
t2.start()
t3.start()
t4.start()
t6.start()
4.多线程版聊天
import socket
import threading
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', 8080))
def send_msg():
ip = input('请输入您要聊天的ip:')
port = int(input('请输入对方的端口号:'))
while True:
msg = input('请输入聊天内容:')
s.sendto(msg.encode('utf-8'), (ip, port))
if msg == "bye":
ip = input('请输入您要聊天的ip:')
port = int(input('请输入对方的端口号:'))
def recv_msg():
while True:
content, addr = s.recvfrom(1024)
print('接收到了{}主机{}端口的消息:{}'.format(addr[0], addr[1], content.decode('utf-8')),file=open('history.txt', 'a', encoding='utf-8'))
send_thread = threading.Thread(target=send_msg)
recv_thread = threading.Thread(target=recv_msg)
send_thread.start()
recv_thread.start()