我正在尝试编写一个多线程Python应用程序,其中在线程之间共享一个SQlite连接。我无法让这个工作。真正的应用程序是一个令人讨厌的Web服务器,但以下简单的代码演示了我的问题。
我需要在下面成功运行示例代码进行哪些更改或更改?
当我运行THREAD_COUNT设置为1的程序时,它工作正常,我的数据库按照我的预期更新(即,字母“X”被添加到SectorGroup列中的文本值)。
当我在THREAD_COUNT设置为高于1的任何值的情况下运行它时,除1之外的所有线程都会过早地终止与SQLite相关的异常。不同的线程抛出不同的异常(没有可辨别的模式),包括:
OperationalError: cannot start a transaction within a transaction
(发生在UPDATE
声明中)
OperationalError: cannot commit - no transaction is active
(发生在.commit()调用上)
InterfaceError: Error binding parameter 0 - probably unsupported type.
(发生在UPDATE
和SELECT
声明中)
IndexError: tuple index out of range
(这个让我感到困惑,它出现在group = rows[0][0] or ''
语句中,但只有在多个线程运行时才会出现)
以下是代码:
CONNECTION = sqlite3.connect('./database/mydb', detect_types=sqlite3.PARSE_DECLTYPES, check_same_thread = False)
CONNECTION.row_factory = sqlite3.Row
def commands(start_id):
# loop over 100 records, read the SectorGroup column, and write it back with "X" appended.
for inv_id in range(start_id, start_id + 100):
rows = CONNECTION.execute('SELECT SectorGroup FROM Investment WHERE InvestmentID = ?;', [inv_id]).fetchall()
if rows:
group = rows[0][0] or ''
msg = '{} inv {} = {}'.format(current_thread().name, inv_id, group)
print msg
CONNECTION.execute('UPDATE Investment SET SectorGroup = ? WHERE InvestmentID = ?;', [group + 'X', inv_id])
CONNECTION.commit()
if __name__ == '__main__':
THREAD_COUNT = 10
for i in range(THREAD_COUNT):
t = Thread(target=commands, args=(i*100,))
t.start()
答案 0 :(得分:13)
在线程之间共享连接是不安全的;至少你需要使用一个锁来序列化访问。还请阅读http://docs.python.org/2/library/sqlite3.html#multithreading,因为较旧的SQLite版本仍有更多问题。
在这方面,check_same_thread
选项似乎故意记录不足,请参阅http://bugs.python.org/issue16509。
您可以使用每个线程的连接,或者查看SQLAlchemy以获取连接池(以及非常有效的工作声明和排队系统)。
答案 1 :(得分:3)
在编写一个简单的WSGI服务器以进行娱乐和学习时,我遇到了SqLite线程问题。 在Apache下运行时,WSGI本质上是多线程的。 以下代码似乎对我有用:
import sqlite3
import threading
class LockableCursor:
def __init__ (self, cursor):
self.cursor = cursor
self.lock = threading.Lock ()
def execute (self, arg0, arg1 = None):
self.lock.acquire ()
try:
self.cursor.execute (arg1 if arg1 else arg0)
if arg1:
if arg0 == 'all':
result = self.cursor.fetchall ()
elif arg0 == 'one':
result = self.cursor.fetchone ()
except Exception as exception:
raise exception
finally:
self.lock.release ()
if arg1:
return result
def dictFactory (cursor, row):
aDict = {}
for iField, field in enumerate (cursor.description):
aDict [field [0]] = row [iField]
return aDict
class Db:
def __init__ (self, app):
self.app = app
def connect (self):
self.connection = sqlite3.connect (self.app.dbFileName, check_same_thread = False, isolation_level = None) # Will create db if nonexistent
self.connection.row_factory = dictFactory
self.cs = LockableCursor (self.connection.cursor ())
使用示例:
if not ok and self.user: # Not logged out
# Get role data for any later use
userIdsRoleIds = self.cs.execute ('all', 'SELECT role_id FROM users_roles WHERE user_id == {}'.format (self.user ['id']))
for userIdRoleId in userIdsRoleIds:
self.userRoles.append (self.cs.execute ('one', 'SELECT name FROM roles WHERE id == {}'.format (userIdRoleId ['role_id'])))
另一个例子:
self.cs.execute ('CREATE TABLE users (id INTEGER PRIMARY KEY, email_address, password, token)')
self.cs.execute ('INSERT INTO users (email_address, password) VALUES ("{}", "{}")'.format (self.app.defaultUserEmailAddress, self.app.defaultUserPassword))
# Create roles table and insert default role
self.cs.execute ('CREATE TABLE roles (id INTEGER PRIMARY KEY, name)')
self.cs.execute ('INSERT INTO roles (name) VALUES ("{}")'.format (self.app.defaultRoleName))
# Create users_roles table and assign default role to default user
self.cs.execute ('CREATE TABLE users_roles (id INTEGER PRIMARY KEY, user_id, role_id)')
defaultUserId = self.cs.execute ('one', 'SELECT id FROM users WHERE email_address = "{}"'.format (self.app.defaultUserEmailAddress)) ['id']
defaultRoleId = self.cs.execute ('one', 'SELECT id FROM roles WHERE name = "{}"'.format (self.app.defaultRoleName)) ['id']
self.cs.execute ('INSERT INTO users_roles (user_id, role_id) VALUES ({}, {})'.format (defaultUserId, defaultRoleId))
使用此结构的完整程序可下载: http://www.josmith.org/
N.B。上面的代码是实验性的,当与(许多)并发请求(例如作为WSGI服务器的一部分)一起使用时,可能存在(基本)问题。性能对我的应用程序并不重要。最简单的事情可能只是使用MySql,但我喜欢尝试一下,关于SqLite的零安装事情吸引了我。如果有人认为上面的代码存在根本缺陷,请作出反应,因为我的目的是学习。如果没有,我希望这对其他人有用。
答案 2 :(得分:0)
我猜这里,但看起来你这样做的原因是性能问题。
对于此用例,Python线程无法以任何有意义的方式执行。相反,使用超快速的sqlite事务。
如果您在交易中完成所有更新,您将发现一个数量级的加速。