我在我的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 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); }
Mousa.. 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]; }
这两个解决方案阻塞当前线程,直到写入数据库完成,您可以在大多数情况下考虑.
我尝试过这两种解决方案,但效果很好.您可以使用关键部分或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]; }
这两个解决方案阻塞当前线程,直到写入数据库完成,您可以在大多数情况下考虑.
该错误消息映射到SQLITE_MISUSE(源代码可从http://www.sqlite.org获得).
有关从多个线程使用sqlite3*数据库句柄的限制,请参阅http://www.sqlite.org/faq.html#q6.实际上,您可以跨线程重用数据库句柄和语句,但在另一个线程启动之前必须完全访问数据库(即重叠访问不安全).这听起来像你正在发生的事情,并与SQLITE_MISUSE错误代码一致.
如果您需要从多个线程访问同一个数据库,我建议您从每个线程单独打开数据库并使用sqlite3_busy_timeout()设置超时.然后,Sqlite将为您处理争用,如果另一个线程正在写入数据同时仍然允许同时读取,则在一个线程中短时间阻塞.