Блокирует ли управляемые языки сброс и перезагрузку переменных собственных библиотек? - PullRequest
7 голосов
/ 27 июня 2019

Когда мы используем блокировки в управляемых языках, таких как C # и Java , мы всегда можем быть уверены, что имеем дело с последними данными.

В частности, в модели памяти Java они имеют гарантию под названием Отношение «до и после» . Но я не уверен, что произойдет с нативными библиотеками.

Скажем, у меня есть C функции, подобные этой:

static int sharedData;      // I'm not declaring this as volatile on purpose here.

void setData(int data) {
    sharedData = data;      // Not using any mutex or the like.
}

int getData() {
    return sharedData;
}

У меня также есть C # код, подобный этому:

// Thread 1
while( true )
    lock( key )
        setData( ++i );     // Calling a native C function using P/Invoke.

// Thread 2
while( true )
    lock( key )
        DoSomeJob( getData() );

Как видите, если сторона sharedData со стороны C не объявлена ​​как volatile, то есть ли гарантия, что Thread 2 всегда сможет получить последний установленный набор значений по Тема 1 ?

То же самое относится к Java , используя JNI тоже?

1 Ответ

1 голос
/ 09 июля 2019

Как вы видите, если sharedData со стороны C не объявлен как энергозависимый, то есть ли еще гарантия, что поток 2 всегда может получить последнее значение, установленное потоком 1?

Нет, и пометить его volatile никак не влияет на многопоточность.

То же самое относится и к Java с использованием JNI?

Да, и этотакже применимо к PHP, Lua, Python и любым другим языкам, которые могут использовать библиотеку C таким образом.

Для уточнения вашего первого вопроса ключевое слово volatile в C не используется для многопоточности,он используется для указания компилятору , а не для оптимизации этой переменной.

Возьмем, например, следующий код:

#include <stdio.h>
#include <stdbool.h>
#include <limits.h>

static bool run; // = false

void do_run(void)
{
    unsigned long v = 1;
    while (run) {
        if (++v == ULONG_MAX) run = false;
    }
    printf("v = %lu\n", v);
}

void set_run(bool value)
{
    run = value;
}

int main(int argc, char** argv)
{
    set_run(true);
    do_run();
    return 0;
}

При включенной оптимизации компилятор можетувидеть много областей, чтобы удалить ненужный код без побочных эффектов, и сделайте это;например, компилятор может видеть, что unsigned long v всегда будет ULONG_MAX в функции do_run, и вместо этого выбрать просто возвращать ULONG_MAX.

И фактически, когда я запускаю gcc -O3 наПриведенный выше код, это именно то, что происходит, с функцией do_run, возвращающейся немедленно и печатающей v = 18446744073709551615.

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

С другой стороны, когда я изменяю run на static volatile bool run; и затем компилирую с использованием gcc -O3, моя программа теперь останавливается, ожидаячтобы цикл повторялся 18446744073709551615 раз.


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

Для C вы должны явно указать безопасность потоков в функциях.Таким образом, для вашего кода, даже если вы используете контекст lock в управляемом коде, это only блокировка для управляемого кода, а сам код C все еще не является поточно-ориентированным.

Возьмем, к примеру, следующий код:

Код C

static volatile int sharedData;
static volatile bool doRun;
static pthread_t addThread;

void* runThread(void* data)
{
    while (doRun) {
        ++sharedData;
    }
    return NULL;
}

void startThread(void)
{
    doRun = true;
    pthread_create(&addThread, NULL, &runThread, NULL);
}

void stopThread(void)
{
    doRun = false;
}

void setData(int data)
{
    sharedData = data;
}

int getData(void)
{
    return sharedData;
}

Код C #

// Thread 1
startThread();
while (true) {
    lock (key) {
        setData(++i);
    }
}

// Thread 2
while (true) {
    lock (key) {
        i = getData();
    }
}
stopThread();

В этом коде, когда вызывается lock (key), единственная гарантияу вас есть то, что i будет защищено в коде C #.Однако, поскольку код C также выполняет поток (поскольку поток 1 называется startThread), у вас нет никаких гарантий, что код C# будет правильно синхронизирован.

Чтобы сделатьПоточно-ориентированный код C, вам нужно будет специально добавить мьютекс или семафор в соответствии с вашими потребностями:

static int sharedData;
static volatile bool doRun;
static pthread_t addThread;
static pthread_mutex_t key;

void* runThread(void* data)
{
    while (doRun) {
        pthread_mutex_lock(&key);
        ++sharedData;
        pthread_mutex_unlock(&key);
    }
    return NULL;
}

void startThread(void)
{
    doRun = true;
    pthread_mutex_init(&key, NULL);
    pthread_create(&addThread, NULL, &runThread, NULL);
}

void stopThread(void)
{
    doRun = false;
    pthread_mutex_lock(&key);
    pthread_mutex_unlock(&key);
    pthread_mutex_destroy(&key);
}

void setData(int data)
{
    pthread_mutex_lock(&key);
    sharedData = data;
    pthread_mutex_unlock(&key);
}

int getData(void)
{
    int ret = 0;
    pthread_mutex_lock(&key);
    ret = sharedData;
    pthread_mutex_unlock(&key);
    return ret;
}

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

Следует отметить, что в приведенном выше примере для синхронизации потоков используется POSIX, но в зависимости от вашей целевой системы может также использоваться WinAPI или стандартный мьютекс C11.

Надеюсь, это поможет.

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