何时在编程中使用原子操作?

huangapple go评论70阅读模式
英文:

When to use atomic operations while programming?

问题

我知道CPU提供原子指令来原子访问指定的内存地址。我很好奇我们应该在什么时候使用原子指令。更确切地说,一些专家告诉我"当超过两个线程访问共享变量时,我们应该使用原子操作"。我的问题是,如果我有一个读线程和一个写线程怎么办?例如,我有下面的非原子代码片段:

#define READY 1
#define NOTREADY 0

int flag;
int msg;

// 线程1
void reader() {
    while (flag == NOTREADY) ;  // 非原子操作

    process(msg);

    flag = NOTREADY;
}

// 线程2
void writer() {
    while (1) {
        while (flag == READY) ;
        msg = send_msg();
        flag = READY;  // 非原子操作
    }
}

在这个例子中,线程1进入自旋循环,直到flagNOTREADY,而线程2将send_msg并将flag设置为READY。如果我不将对flag的访问设为原子操作会怎么样?这些代码会正常运行吗?

我尝试运行下面的代码,结果是正常的。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

#define READY 1
#define NOTREADY 0

int msg, flag = NOTREADY, cnt = 0;

void process(int msg) {
  printf("Processing msg: %d\n", msg);
}

void *reader() {
  while (cnt < 200000) {
    while (flag == NOTREADY) ;

    process(msg);

    flag = NOTREADY;
  }
}

int send_msg() {
  cnt ++;
  printf("Sending msg: %d\n", cnt);
  return cnt;
}

void *writer() {
  while (cnt < 200000) {
    while (flag == READY) ;

    msg = send_msg();

    flag = READY;
  }
}

int main() {
  pthread_t r_id, w_id;
  pthread_create(&r_id, NULL, reader, NULL);
  pthread_create(&w_id, NULL, writer, NULL);

  pthread_join(r_id, NULL);
  pthread_join(w_id, NULL);

  return 0;
}

那么,如果程序只有一个读线程和一个写线程,我们是否可以省略原子操作?如果不能,为什么不能呢?

英文:

I knew that cpu provides atomic instructions to atomically access a specified memory address. I'm curious about when should we use atomic instructions. More extractly, some experts told me that "we should use atomic when more than two threads access the shared variable". My question is that what if I have a reading thread with a writing thread? For example, I have non-atomic code snippets below:

#define READY 1
#define NOTREADY 0

int flag;
int msg;

// thread 1
void reader() {
    while (flag == NOTREADY) ;  // not-atomic operation

    process(msg);

    flag = NOTREADY;
}

// thread 2
void writer() {
    while (1) {
        while (flag == READY) ;
        msg = send_msg();
        flag = READY;  // not-atomic operation
    }
}

In this example, thread 1 enters spinloop until flag is NOTREADY, while thread 2 will send_msg and set flag to READY. What if I don't make accesses to flag atomic operation? Would these codes run well?

I tried to run code below and the result is ok.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

#define READY 1
#define NOTREADY 0

int msg, flag = NOTREADY, cnt = 0;

void process(int msg) {
  printf("Processing msg: %d\n", msg);
}

void *reader() {
  while (cnt < 200000) {
    while (flag == NOTREADY) ;

    process(msg);

    flag = NOTREADY;
  }
}

int send_msg() {
  cnt ++;
  printf("Sending msg: %d\n", cnt);
  return cnt;
}

void *writer() {
  while (cnt < 200000) {
    while (flag == READY) ;

    msg = send_msg();

    flag = READY;
  }
}

int main() {
  pthread_t r_id, w_id;
  pthread_create(&r_id, NULL, reader, NULL);
  pthread_create(&w_id, NULL, writer, NULL);

  pthread_join(r_id, NULL);
  pthread_join(w_id, NULL);

  return 0;
}

So could we omit atomic operation when the program has only one thread for reading and one thread for writing to the shared variable? If we can't, why not?

答案1

得分: 3

在这种情况下,省略原子操作是有效的,但不能保证始终有效。

原因是编译器和CPU以某种方式优化代码,这在这个简单示例中起作用,但不是保证的行为。

潜在的问题包括:

指令重排序 - CPU可能会重新排列线程内的指令,以影响其他线程。例如,它可能会将flag = READY语句移到msg = send_msg()语句之前。如果发生这种情况,读取线程可能会在实际更新msg之前将flag视为READY。

缓存 - CPU可能会在寄存器中缓存flag的值以提高性能。因此,写入线程可能会更新flag,但读取线程继续看到旧的缓存值。

通过使用原子操作,您可以禁用这些优化并确保:

指令按程序顺序执行
对共享内存的写入立即对其他线程可见。
因此,总之,对于像这样的简单示例,省略原子操作可能有效。但对于能在所有平台上使用的健壮、可移植的代码,访问多个线程的共享变量时,应使用原子操作。

英文:

In this case, omitting atomic operations is working, but it is not guaranteed to always work.

The reason is that the compiler and CPU are optimizing your code in a way that happens to work in this simple example, but it is not guaranteed behavior.

The potential issues are:

Instruction reordering - The CPU may reorder the instructions within a thread in a way that affects the other thread. For example, it may move the flag = READY statement before the msg = send_msg() statement. If this happens, the reader thread could see flag as READY before msg is actually updated.

Caching - The CPU may cache the value of flag in a register for performance. So the writer thread may update flag, but the reader thread continues to see the old cached value.

By using atomic operations, you disable these optimizations and ensure that:

Instructions are executed in program order
Writes to shared memory are visible to other threads immediately.
So in summary, for simple examples like this, omitting atomics may work. But for robust, portable code that works on all platforms, you should use atomic operations when accessing shared variables from multiple threads.

huangapple
  • 本文由 发表于 2023年6月13日 15:49:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76462737.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定