Sqlite3的多线程问题-获得意外结果

bakd9h0s  于 2022-11-15  发布在  SQLite
关注(0)|答案(2)|浏览(154)

我尝试使用多个线程测试将数据写入/读取到SQLite数据库中。
有时,它似乎得不到正确的结果。那是虫子吗?
我制作了两个文件来测试它。第一个是test.py

import threading
import master

def add():
    for i in range(10):
        num = master.get()
        tmp = num + 1
        master.update(tmp)
        print(f"add: {i}, {num}")

def sub():
    for i in range(10):
        num = master.get()
        tmp = num - 1
        master.update(tmp)
        print(f"sub: {i}, {num}")

if __name__ == "__main__":
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)
    subThread01.start()
    subThread02.start()
    subThread01.join()
    subThread02.join()
    print(master.get())

第二个文件是master.py

import sqlite3
import threading

lock = threading.Lock()

conn = sqlite3.connect(':memory:', check_same_thread=False)
cur = conn.cursor()

# creat table
cur.execute("""CREATE TABLE IF NOT EXISTS info ( userid INT PRIMARY KEY, data INT );""")
conn.commit()

# insert init data
db = (0, 0)
cur.execute("INSERT INTO info VALUES(?, ?);", db)
conn.commit()

# update data
def update(num):
    with lock:
        db = (num, 0)
        cur.execute("UPDATE info set data = ? where userid = ?;", db)
        conn.commit()

# get data
def get():
    with lock:
        cur.execute(f"SELECT data FROM info where userid = 0;")
        result = cur.fetchone()
        return result[0]

当我运行test.py时,我预期的结果是0。但实际结果是随机的,有时是-3,有时是9,等等。
问题出在哪里?

qyuhtwio

qyuhtwio1#

这可能是一个功能,而不是一个错误。
为了使结果为0,两个线程必须被调度为按顺序“准确地”运行。如果你只有两条线,那可能行得通。
然而,还有第三个线程(主线程)。如果没有额外的措施,就没有办法知道在那之后将选择哪个线程运行。
但是,您可以使用Barrier而不是Lock来强制线程一个接一个地运行。

sqserrrh

sqserrrh2#

updateget函数是thread-safe,但addsub函数不是。这会造成同步问题。您还应该执行线程安全的addsub函数,如;

def add():
    for i in range(10):
        with lock:
            num = master.get()
            tmp = num + 1
            master.update(tmp)
            print(f"add: {i}, {num}")

def sub():
    for i in range(10):
        with lock:
            num = master.get()
            tmp = num - 1
            master.update(tmp)
            print(f"sub: {i}, {num}")

编辑:我的答案丢失,我忘记指定一个新的锁对象。它应该是这样的:

import threading
import master

lock=threading.Lock()

def add():
    for i in range(10):
        with lock:
            num = master.get()
            tmp = num + 1
            master.update(tmp)
            print(f"add: {i}, {num}")

def sub():
    for i in range(10):
        with lock:
            num = master.get()
            tmp = num - 1
            master.update(tmp)
            print(f"sub: {i}, {num}")

编辑2(作为对OP评论的答复):
让我们来看看,(请阅读add函数中的评论)

def add():
    for i in range(10):
        num = master.get() # let's say num==0
        tmp = num + 1 
        """
        Now tmp==1. And think that, GIL released and OS switch to subThread02.
        When switching, i==0 this is where we left
        """
        master.update(tmp)

继续使用subThread02;

def sub():
    for i in range(10):
        num = master.get()
        tmp = num - 1
        master.update(tmp)

想想看,GIL没有释放,FOR循环结束(没有任何中断)。最后一次操作将是master.update(-10)
在最后一次操作之后,GIL将被释放,然后操作系统切换到subThread01。
add函数中,我们将从我们离开的地方继续,在add函数中,master.update(0)(请注意)将被求值,然后for loop将迭代9次,最后它将执行master.更新(10)。因此将出现同步问题,print(master.get())将显示10,但结果可能不同,可能是5、-3或0
您还说“我删除了sqlite并设置了一个变量,然后我测试了它,没有任何同步问题”,我希望您在两个线程中将这个for i in range(100):更改为for i in range(100000):。(因为for i in range(100):循环立即结束,没有任何中断,您将看到正确的结果,但这不能保证,中断随时可能发生),那么您将看到错误的结果(请多次运行它以查看错误的结果)。
请也看看this

相关问题