托管语言是否锁定本机库的刷新和重新加载变量?

Jen*_*nix 7 c c# c++ java locking

当我们使用C#Java之类的托管语言中的锁时,始终可以确保我们正在处理最新数据。

特别是在Java内存模型中,它们具有一个称为Happens-before关系的保证。但是我不确定本机库会发生什么。

说,我有这样的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;
}
Run Code Online (Sandbox Code Playgroud)

我也有这样的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() );
Run Code Online (Sandbox Code Playgroud)

如您所见,如果sharedDataC端未声明为volatile,那么是否仍然可以保证线程2始终可以获取线程1设置的最新值?

使用JNI的Java同样适用吗?

txt*_*elp 1

正如你所看到的,如果sharedData从C端没有声明为易失性,那么是否仍然可以保证线程2始终可以获取线程1设置的最新值?

不,并且标记它对volatile任何线程都没有影响。

这同样适用于使用 JNI 的 Java 吗?

是的,它也适用于 PHP、Lua、Python 以及任何其他可以通过这种方式引入 C 库的语言。

为了详细说明你的第一个问题,volatileC 中的关键字不用于线程,它用于告诉编译器不要优化该变量。

以下面的代码为例:

#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;
}
Run Code Online (Sandbox Code Playgroud)

打开优化后,编译器可能会看到很多区域来删除不必要的代码而不产生副作用,并这样做;例如,编译器可以看到unsigned long v始终ULONG_MAX在函数中do_run,并选择简单地返回ULONG_MAX

事实上,当我运行gcc -O3上面的代码时,这正是发生的情况,函数do_run立即返回并打印v = 18446744073709551615.

如果您要标记runvolatile,则编译器无法优化该变量,这通常意味着它无法以某些方式优化使用该变量的代码区域。

也就是说,当我更改runstatic volatile bool run;然后使用 进行编译时gcc -O3,我的程序现在停止等待循环迭代 18446744073709551615 次。


除此之外,当您调用外部库时,您拥有的唯一线程安全性是由该库中使用的语言提供的。

对于 C,您必须在函数中显式指定线程安全性。因此,对于您的代码,即使您正在利用lock托管代码中的上下文,它也只是对托管代码进行锁定,而 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;
}
Run Code Online (Sandbox Code Playgroud)

C# 代码

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

// Thread 2
while (true) {
    lock (key) {
        i = getData();
    }
}
stopThread();
Run Code Online (Sandbox Code Playgroud)

在此代码中,当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;
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,底层库调用得到了适当的保护,并且共享该库内存的任意数量的进程也将是线程安全的。

我应该注意到,上面使用 POSIX 进行线程同步,但也可以使用 WinAPI 或 C11 标准互斥体,具体取决于您的目标系统。

我希望这能有所帮助。