Как правильно вызывать функции SQLite из фонового потока на iPhone? - PullRequest
6 голосов
/ 14 апреля 2009

Я использую базу данных SQLite в своем приложении для iPhone. При запуске есть некоторые действия с базой данных, которые я хочу выполнить в отдельном потоке. (Я делаю это в основном для минимизации времени запуска.)

Иногда / случайно, когда эти вызовы базы данных выполняются из фонового потока, приложение вылетает с такими ошибками:

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);
}

Ответы [ 8 ]

8 голосов
/ 22 августа 2011

Я пробовал эти два решения, и они отлично работали. Вы можете использовать критические разделы или 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];
}

эти два решения блокируют текущий поток до завершения записи в базу данных, что вы можете рассмотреть в большинстве случаев.

6 голосов
/ 14 апреля 2009

Это сообщение об ошибке отображается на SQLITE_MISUSE (исходный код доступен по адресу http://www.sqlite.org).

См. http://www.sqlite.org/faq.html#q6 об ограничениях на использование дескриптора базы данных sqlite3 * из более чем одного потока. По сути, вам разрешено повторно использовать дескриптор базы данных и операторы в разных потоках, но один поток должен полностью завершить доступ к базе данных до запуска другого потока (то есть перекрывающийся доступ не является безопасным). Это похоже на то, что происходит для вас, и согласуется с кодом ошибки SQLITE_MISUSE.

Если вам нужен доступ к одной и той же базе данных из нескольких потоков, я рекомендую вместо этого открывать базу данных отдельно от каждого потока и устанавливать таймаут с помощью sqlite3_busy_timeout (). Затем Sqlite обработает для вас конфликт, блокируя на короткое время в одном потоке, если другой поток записывает данные, и в то же время разрешает одновременное чтение.

1 голос
/ 02 апреля 2013

Если вы хотите использовать SQLite в нескольких потоках без ограничений, сделайте следующее перед открытием соединения:

sqlite3_shutdown ();
sqlite3_config (SQLITE_CONFIG_SERIALIZED);
sqlite3_initialize ();

http://www.sqlite.org/threadsafe.html

1 голос
/ 14 апреля 2009

дескрипторы SQLite (наверняка, sqlite3_stmt * и sqlite3 *, я думаю) зависят от потока. Правильный способ вызывать их из нескольких потоков - поддерживать отдельный набор дескрипторов для каждого потока.

0 голосов
/ 20 ноября 2013

Лучше всего использовать очереди GCD (Grand Central Dispatch) для предотвращения одновременного доступа к базе данных sqlite.

Использование любой формы блокировки (включая блокировку файлов, которая будет использоваться несколькими экземплярами базы данных) может привести к занятому ожиданию, которое расточительно.

См. мой ответ на аналогичный вопрос.

0 голосов
/ 24 февраля 2012

Мое решение - удалить приложение на моем устройстве (я хочу удалить базу данных, которую я создал). Это решает проблему для меня.

0 голосов
/ 11 октября 2011

Если вам все еще не повезло с вышеупомянутым, вы можете попробовать использовать эту обертку от EnormEGO https://github.com/jdp-global/egodatabase

Они используют асинхронные обратные вызовы, которые могут убить двух зайцев одним выстрелом.

Взгляните на мой раздел Readme для EGODatabaseRequest - асинхронные запросы к БД

0 голосов
/ 14 апреля 2009

Я бы использовал NSOperation и просто делал все там во время запуска. NSOperation рок. Я сказал, сколько NSOperation качается? Оно делает. Рок, то есть.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...