我在我的iPhone应用程序中使用SQLite数据库。在启动时,我想在一个单独的线程中执行一些数据库操作。 (我这样做主要是为了减少启动时间。)
偶尔/随机,当从后台线程进行这些数据库调用时,应用程序将因这些错误而崩溃:
2009-04-13 17:36:09.932 Action Lists[1537:20b] *** Assertion failure in -[InboxRootViewController getInboxTasks], /Users/cperry/Dropbox/Projects/iPhone GTD/GTD/Classes/InboxRootViewController.m:74
2009-04-13 17:36:09.932 Action Lists[1537:3d0b] *** Assertion failure in +[Task deleteCompletedTasksInDatabase:completedMonthsAgo:], /Users/cperry/Dropbox/Projects/iPhone GTD/GTD/Classes/Data Classes/Task.m:957
2009-04-13 17:36:09.933 Action Lists[1537:20b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Error: failed to prepare statement with message 'library routine called out of sequence'.'
2009-04-13 17:36:09.933 Action Lists[1537:3d0b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Error: failed to prepare statement with message 'library routine called out of sequence'.'
虽然我无法可靠地重现错误,但我确信这是因为在两个活动线程中都调用了SQLite函数。 应该如何我从一个单独的线程调用SQLite函数?有缺点我不知道吗?我对iPhone,SQLite和Objective-C都很陌生,所以它可能对你来说很明显,但对我来说却不是那么明显。
以下是一些代码示例。
MainApplication.m:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Take care of jobs that have to run at startup
[NSThread detachNewThreadSelector:@selector(startUpJobs) toTarget:self withObject:nil];
}
// Jobs that run in the background at startup
- (void)startUpJobs {
// Anticipating that this method will be called in its own NSThread, set up an autorelease pool.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Get user preferences
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// This Class Method calls SQLite functions and sometimes causes errors.
[Task revertFutureTasksStatus:database];
[pool release];
}
Task.m:
static sqlite3_stmt *revert_future_statement = nil;
+ (void) revertFutureTasksStatus:(sqlite3 *)db {
if (revert_future_statement == nil) {
// Find all tasks that meet criteria
static char *sql = "SELECT task_id FROM tasks where ((deleted IS NULL) OR (deleted=0)) AND (start_date > ?) AND (status=0) AND (revert_status IS NOT NULL)";
if (sqlite3_prepare_v2(db, sql, -1, &revert_future_statement, NULL) != SQLITE_OK) {
NSAssert1(0, @"Error: failed to prepare update statement with message '%s'.", sqlite3_errmsg(db));
}
}
// Bind NOW to sql statement
NSDate *now = [[NSDate alloc] init];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd"];
NSString *nowString = [formatter stringFromDate:now];
sqlite3_bind_text(revert_future_statement, 1, [nowString UTF8String], -1, SQLITE_TRANSIENT);
[now release];
[formatter release];
// We "step" through the results - once for each row.
while (sqlite3_step(revert_future_statement) == SQLITE_ROW) {
// Do things to each returned row
}
// Reset the statement for future reuse.
sqlite3_reset(revert_future_statement);
}
答案 0 :(得分:8)
我尝试过这两种解决方案,但效果很好。您可以使用关键部分或NSOperationQueue,我更喜欢第一个,这里是两个代码:
定义一些类“DatabaseController”并将此代码添加到其实现中:
static NSString * DatabaseLock = nil;
+ (void)initialize {
[super initialize];
DatabaseLock = [[NSString alloc] initWithString:@"Database-Lock"];
}
+ (NSString *)databaseLock {
return DatabaseLock;
}
- (void)writeToDatabase1 {
@synchronized ([DatabaseController databaseLock]) {
// Code that writes to an sqlite3 database goes here...
}
}
- (void)writeToDatabase2 {
@synchronized ([DatabaseController databaseLock]) {
// Code that writes to an sqlite3 database goes here...
}
}
要使用NSOperationQueue,您可以使用:
static NSOperationQueue * DatabaseQueue = nil;
+ (void)initialize {
[super initialize];
DatabaseQueue = [[NSOperationQueue alloc] init];
[DatabaseQueue setMaxConcurrentOperationCount:1];
}
+ (NSOperationQueue *)databaseQueue {
return DatabaseQueue;
}
- (void)writeToDatabase {
NSInvocationOperation * operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(FUNCTION_THAT_WRITES_TO_DATABASE) object:nil];
[operation setQueuePriority:NSOperationQueuePriorityHigh];
[[DatabaseController databaseQueue] addOperations:[NSArray arrayWithObject:operation] waitUntilFinished:YES];
[operation release];
}
这两个解决方案会阻塞当前线程,直到完成对数据库的写入,在大多数情况下您可以考虑这些。
答案 1 :(得分:6)
该错误消息映射到SQLITE_MISUSE(源代码可在http://www.sqlite.org获得)。
有关从多个线程使用sqlite3 *数据库句柄的限制,请参阅http://www.sqlite.org/faq.html#q6。实际上,您可以跨线程重用数据库句柄和语句,但在另一个线程启动之前必须完全访问数据库(即重叠访问不安全)。这听起来像是你正在发生的事情,并且与SQLITE_MISUSE错误代码一致。
如果您需要从多个线程访问同一个数据库,我建议您从每个线程单独打开数据库,并使用sqlite3_busy_timeout()设置超时。然后,Sqlite将为您处理争用,如果另一个线程正在写入数据,同时仍允许同时读取,则会在一个线程中短时间阻塞。
答案 2 :(得分:1)
SQLite句柄(sqlite3_stmt *
肯定,而sqlite3 *
我认为)是特定于线程的。从多个线程调用它们的正确方法是为每个线程维护一组单独的句柄。
答案 3 :(得分:1)
如果要在没有限制的多个线程上使用SQLite,请在打开连接之前执行以下操作:
sqlite3_shutdown();
sqlite3_config(SQLITE_CONFIG_SERIALIZED);
sqlite3_initialize();
答案 4 :(得分:0)
我会使用NSOperation并在启动期间执行所有操作。 NSOperation岩石。我说过NSOperation有多少岩石?确实如此。摇滚,就是这样。
答案 5 :(得分:0)
如果您仍然没有上述运气,可以尝试使用EnormEGO的这个包装器 https://github.com/jdp-global/egodatabase
他们使用异步回调,可能一举两得。
请查看我的自述文件部分 EGODatabaseRequest - 对db的异步请求
答案 6 :(得分:0)
您最好的选择是使用GCD(Grand Central Dispatch)队列来防止同时访问sqlite数据库。
使用任何形式的锁定(包括多个数据库实例将使用的文件锁定)可能导致繁忙等待,这是浪费。
请参阅my answer类似的问题。
答案 7 :(得分:-1)
我的解决方案是删除设备上的应用程序(我想删除我创建的数据库)。这解决了我的问题。