Python is fast and beautiful [v1.0.0] [Thread synchronization]

Thread-safe

The thread scheduling of the system is random. When multiple threads can modify a resource at the same time, there will be a thread safety problem, which will eventually lead to the failure to achieve the expected results. However, because of the randomness of thread scheduling, we may run a lot There is nothing wrong with the program for a second or a long time, but it does not mean that there is no problem

For example, in a money withdrawal scenario, an account has a certain balance. When the amount of withdrawal is greater than the balance, the withdrawal will fail, and when it is less than the balance, the withdrawal will be successful. This logic has no problems in the single-threaded case, but it is In the multi-threaded scenario, there will be confusion. For example, two threads withdraw money. The first thread may withdraw money less than the account balance and the withdrawal can be successful, but the second thread also withdraws money. When there is no change, the second thread starts to withdraw money and also judges whether it is less than the balance, which happens to be less than the balance, and the withdrawal can be successful. If the total withdrawal amount of these two threads is greater than the balance, but the withdrawal of each thread Is less than the balance, both can succeed, but the remaining balance of the book will be negative, which is obviously not realistic
This is called thread unsafe

The scene code is as follows

First define an account

class Account:
    # 定义构造器
    def __init__(self, account_no, balance):
        # 封装账户编号、账户余额的两个成员变量
        self.account_no = account_no
        self.balance = balance

In defining a thread to withdraw money

import threading
import time
import Account

# 定义一个函数来模拟取钱操作
def draw(account, draw_amount):
    # 账户余额大于取钱数目
    if account.balance >= draw_amount:
        # 吐出钞票
        print(threading.current_thread().name + "取钱成功!吐出钞票:" + str(draw_amount))
		# time.sleep(1)
        # 修改余额
        account.balance -= draw_amount
        print("\t余额为: " + str(account.balance))
    else:
        print(threading.current_thread().name + "取钱失败!余额不足!")
# 创建一个账户
acct = Account.Account("1234567" , 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()

As long as the code is executed enough times, there will be a thread unsafe situation, resulting in a negative book balance. Because the system scheduling thread is random, it will always encounter accidental errors.
You can also time.sleep(0.001)release the note. This leads to the above situation, because one thread waits to enter the blocking state, then the other thread will continue to work, and eventually both threads can successfully withdraw money, the account is used for 1000 balance, and the amount taken out is 1600

Synchronous lock (Lock)

To sum up the above code, there are actually two concurrent threads that modify the Accout object, and the system happens to time.sleep(1)switch to another thread that modifies the Account object, so in addition to the thread unsafe phenomenon, in
order to solve the thread unsafe phenomenon , the Python threading module was introduced In addition to Lock, the threading module provides two classes, Lock and Rlock, and they provide the following methods:

  • acquire (blocking = True, timeout = 1): request to lock the Lock or RLock, timeout specifies the time to lock, in seconds
  • release (): release the lock

The difference between Lock and RLock

  • threading.Lock: It is a basic lock object, which can only be locked once at a time. The rest of the lock requests need to be acquired after the lock is released.
  • threading.RLock: It stands for Reentrant Lock. For reentrant locks, it can be locked multiple times or released multiple times in the same thread. If RLock is used, then acquire () and release ( ) The method must appear at the same time, and if N acquire () is called N times, release () must be called N times to release the lock
  • RLock lock is reentrant, which means that the same thread can lock the RLock lock that has been locked again. The RLock object will maintain a counter to track the nested calls of the acquire () method, and the thread calls acquire ( ) After the method is locked, you must call the release () method to release the lock, so the method protected by the lock can call another method protected by the same lock
  • Lock is a tool to control multi-threaded access to shared resources. Generally, locks provide exclusive access to shared resources. Only one thread can lock a Lock object at a time. Threads should request it before starting to access shared resources. Obtain the Lock object. When access to the shared resource is complete, the program releases the lock on the Lock object

In actual scenarios, RLock is more commonly used, and its structure is as follows

Class Demo_RLock:
	# 定义需要保证线程安全的方法
	def account():
		# 加锁
		self.lock.acquire()
		try:
			# 需要线程安全的代码
			# 方法体
		# 使用finally块来保证释放锁
		finally:
			# 修改完成,释放锁
			self.lock.release()

== Immutable classes are all thread-safe, because the state of his objects cannot be changed ==

Locked code

import threading
import time

class Account:
    # 定义构造器
    def __init__(self, account_no, balance):
        # 封装账户编号、账户余额的两个成员变量
        self.account_no = account_no
        self._balance = balance
        self.lock = threading.RLock()

    # 因为账户余额不允许随便修改,所以只为self._balance提供getter方法
    def getBalance(self):
        return self._balance
    # 提供一个线程安全的draw()方法来完成取钱操作
    def draw(self, draw_amount):
        # 加锁
        self.lock.acquire()
        try:
            # 账户余额大于取钱数目
            if self._balance >= draw_amount:
                # 吐出钞票
                print(threading.current_thread().name + "取钱成功!吐出钞票:" + str(draw_amount))
                time.sleep(0.001)
                # 修改余额
                self._balance -= draw_amount
                print("\t余额为: " + str(self._balance))
            else:
                print(threading.current_thread().name + "取钱失败!余额不足!")
        finally:
            # 修改完成,释放锁
            self.lock.release()
import threading
import Account

# 定义一个函数来模拟取钱操作
def draw(account, draw_amount):
    # 直接调用account对象的draw()方法来执行取钱操作
    account.draw(draw_amount)
# 创建一个账户
acct = Account.Account("1234567" , 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()

Thread synchronization analysis

Through the use of Lock objects, it is very convenient to implement thread-safe classes. Thread-safe classes have several characteristics: Objects of this class can be safely accessed by multiple threads. Each thread will call any method of the object. Get the correct result; after each thread calls any method of the object, the object still maintains a reasonable state

In this way, the safe access logic is realized 加锁===》修改===》释放锁. In this way, only one thread can enter the code area of ​​the modified shared resource at any time of the concurrent thread, which is also called the critical area.

The program defines the draw () method in Account to complete the cash withdrawal process, rather than implementing it in the execution body of the thread. This method is more in line with the object-oriented thinking and more embodies the design pattern of domain-driven design. This pattern considers each The class should be a complete domain object, for example, Account represents a user account, it should provide account-related functions, through draw () to complete the withdrawal of money, rather than expose setBanlance (), which also guarantees the Account object Completeness and consistency

The immutable class is thread-safe because its object state is immutable; the thread safety of the mutable class comes at the cost of reducing the operating efficiency of the program. In order to reduce the negative impact of thread safety, the program can be used as follows Strategy:

  • Do not synchronize all methods of the thread-safe class, only those methods that will change the relationship of competing resources. For example, the accountNo instance variable of the Account class does not need to be synchronized, only the draw () method needs to be synchronized
  • If the variable class has two operating environments: single-threaded environment and multi-threaded environment, the variable class should provide two versions, namely the thread-unsafe version and the thread-safe version
Published 216 original articles · praised 185 · 110,000 visits +

Guess you like

Origin blog.csdn.net/dawei_yang000000/article/details/105619021