简体   繁体   English

C——克服原子操作的使用

[英]C - overcoming the use of atomic operations

I was wondering.我想知道。 If I have an int variable which I want to be synced across all my threads - wouldn't I be able to reserve one bit to know whether the value is being updated or not?如果我有一个想要在所有线程之间同步的 int 变量 - 我不能保留一位来知道该值是否正在更新吗?
To avoid the write operation being executed in chunks, which would mean threads could potentially be accessing mid-written value, which is not correct, or even worse, overwrite it, causing it to be totally wrong, I want the threads to first be informed that the variable is being written to.为了避免分块执行写操作,这意味着线程可能正在访问中间写入的值,这是不正确的,或者更糟糕的是,覆盖它,导致它完全错误,我希望首先通知线程变量正在被写入。 I could simply use an atomic operation to write the new value so that the other threads don't interfere, but this idea does not seem that dumb and I would like to use the basic tools first.我可以简单地使用原子操作来写入新值,这样其他线程就不会干扰,但是这个想法似乎并不愚蠢,我想先使用基本工具。
What if I just make one operation, which is small enough to keep it in one chunk, an operation like changing a single bit (which will still result in the whole byte(s) changing, but it's not the whole value changing, right), and let the bit indicate the variable is being written to or not?如果我只进行一个操作,该操作足够小以将其保持在一个块中,就像更改单个位之类的操作(这仍会导致整个字节更改,但不是整个值更改,对) ,并让该位指示变量是否正在写入? Would that even work, or would the whole int be written to?这甚至会起作用,还是会写入整个 int ?
I mean, even if the whole int was to change, this still would have a chance of working - if the bit indicating if the value is changing was written first.我的意思是,即使整个 int 要改变,这仍然有机会工作 - 如果首先写入指示值是否正在改变的位。

Any thoughts on this?对此有什么想法吗?

EDIT: I feel like I did not specify what I am actually planning to do, and why I thought of this in the first place.编辑:我觉得我没有具体说明我实际打算做什么,以及为什么我首先想到这个。
I am trying to implement a timeout function, similarly to setTimeout in JavaScript.我正在尝试实现超时 function,类似于 JavaScript 中的 setTimeout。 It is pretty straightforward for a timeout that you don't want to ever cancel - you create a new thread, tell it to sleep for given amount of time, then give it a function to execute, eventually with some data.对于您不想取消的超时,这非常简单——您创建一个新线程,告诉它休眠给定的时间,然后给它一个 function 来执行,最终得到一些数据。 Piece of cake.小菜一碟。 Finished writing it in maybe half an hour, while being totally new to C.大概半个小时就写完了,对 C 完全陌生。
The hard part comes when you want to set a timeout which might be canceled in the future.当您想要设置将来可能会取消的超时时,困难的部分就来了。 So you do exactly the same as a timeout without canceling, but when the thread wakes up and the CPU's scheduler puts it on, the thread must check if a value in the memory it was given when it started does not say 'you should stop executing'.因此,您在不取消的情况下执行与超时完全相同的操作,但是当线程唤醒并且 CPU 的调度程序将其启动时,线程必须检查 memory 中的值是否在它启动时给出并没有说'你应该停止执行'。 The value could potentially be modified by other thread, but it would only be done once, at least in the best case scenario.该值可能会被其他线程修改,但它只会被修改一次,至少在最好的情况下是这样。 I will worry about different solutions when it comes down to trying to modify the value from multiple threads at the same time.当涉及到尝试同时修改多个线程的值时,我会担心不同的解决方案。 The base assumption right now is that only the main thread, or one of other threads, can modify the value, and it will happen only once.现在的基本假设是只有主线程或其他线程之一可以修改该值,并且只会发生一次。 Control of it happening only once can be by setting up other variable, which might change multiple times, but always to the same value (that is, initial value is 0 and it means not-yet-canceled, but then when it must be canceled, the value changes to 1, so there is no worrying about the value being fragmented into multiple write operations and only chunk of it being updated at the time of reading it by different thread).可以通过设置其他变量来控制它只发生一次,该变量可能会更改多次,但始终为相同的值(即初始值为 0,表示尚未取消,但随后必须取消) ,值更改为 1,因此无需担心该值被分成多个写入操作,并且在不同线程读取它时仅更新其中的一部分)。
Given this assumption, I think the text I initially wrote at the beginning of this post should be more clear.鉴于这个假设,我认为我最初在这篇文章开头写的文字应该更清楚。 In a nutshell, no need to worry about the value being written multiple times, only once, but by any thread, and the value must be available to be read by any other thread, or it must be indicated that it cannot be read.简而言之,无需担心该值被多次写入,仅一次,但被任何线程写入,并且该值必须可供任何其他线程读取,或者必须指示它无法读取。
Now as I am thinking of it, since the value itself will only ever be 0 or 1, the trick with knowing when it's already been canceled should work too, shouldn't it?现在我正在考虑它,因为值本身只会是 0 或 1,所以知道它何时被取消的技巧也应该起作用,不是吗? Since the 0 or 1 will always be in one operation, so there is no need to worry about it being fragmented and read incorrectly.由于 0 或 1 总是在一个操作中,所以不用担心它会被碎片化和读取错误。 Please correct me if I'm wrong.如果我错了,请纠正我。
On the other hand, what if the value is being written from the end, not the beginning?另一方面,如果值是从末尾而不是开头写入的呢? If it's not possible then no need to worry and the post will be resolved, but I would like to know of every danger that might come with overcoming atomic operations like this, in this specific context.如果不可能,那么无需担心,帖子将得到解决,但我想知道在这种特定情况下克服这样的原子操作可能带来的每一个危险。 In case it is being written from the end, and a thread wants to access the variable to know if it should continue executing, it will notice that it indeed should, while the expected behaviour would be to stop executing.如果它是从最后写入的,并且线程想要访问变量以知道它是否应该继续执行,它会注意到它确实应该执行,而预期的行为是停止执行。 This should have completely minimal chance of being possible, but still is, which means it is dangerous, and I want it to be 100% predictable.这应该有完全最小的可能性,但仍然是,这意味着它是危险的,我希望它是 100% 可预测的。

Another edit to explain what steps I imagine the program to make.另一个编辑来解释我想象程序要执行的步骤。
Main thread spawns a new thread, aka 'cancelable timeout'.主线程产生一个新线程,又名“可取消超时”。 It passes a function to execute along with data, time to sleep, and memory address, pointing to a value.它传递一个 function 与数据、睡眠时间和 memory 地址一起执行,指向一个值。 After the thread wakes up after given time, it must check the value to see if it should execute the function it has been given.在给定时间后线程唤醒后,它必须检查该值是否应该执行给定的 function。 0 means it should continue, 1 means it should stop and exit. 0 表示它应该继续,1 表示它应该停止并退出。 The value (thread's 'state', canceled or not canceled) can be manipulated by either the main thread, or any other thread, 'timeout', which's job is to cancel the first thread.该值(线程的“状态”,已取消或未取消)可以由主线程或任何其他线程“超时”操作,其工作是取消第一个线程。
Sample code:示例代码:

struct Timeout {
  void (*function)(void* data);
  void* data;
  int milliseconds;
  int** base;
  int cancelID;
};
DWORD WINAPI CTimeout(const struct Timeout* data) {
  Sleep(data->milliseconds);
  if(*(*(data->base) + sizeof(int) * data->cancelID) == 0) {
    data->function(data->data);
  }
  free(data);
  return 0;
}

Where CTimeout is a function provided to the newly-spawned thread.其中 CTimeout 是提供给新生成的线程的 function。 Please note that I have written some of this code on go and haven't tested it.请注意,我已经在 go 上编写了一些此代码,但尚未对其进行测试。 Ignore any potential errors.忽略任何潜在的错误。
Timeout.base is pointer to a pointer to an array of ints, since many timeouts can exists at the same time. Timeout.base 是指向整数数组的指针,因为可以同时存在许多超时。 Timeout.cancelID is the ID of current thread on the list of timeouts. Timeout.cancelID 是超时列表中当前线程的 ID。 The same ID has a value if treated as index in the base array.如果将相同的 ID 视为基本数组中的索引,则该 ID 具有值。 If the value is 0, the thread should execute its function, else, clean up the data it has been given and nicely return.如果值为 0,线程应该执行它的 function,否则,清理它已经给出的数据并很好地返回。 The reason behind base being pointer to a pointer, is because at any time, the array of states of timeouts can be resized. base 之所以是指向指针的指针,是因为在任何时候,超时状态数组都可以调整大小。 In case place of the array changes, there is no option to pass its initial place.如果数组的位置发生变化,则无法传递其初始位置。 It might potentially cause a segmentation fault (if not, correct me please), for accessing memory which does not belong to us anymore.访问不再属于我们的 memory 可能会导致分段错误(如果不是,请纠正我)。
Base can be accessed from the main thread or other threads if necessary, and the state of our thread can be changed to cancel its execution.如有必要,可以从主线程或其他线程访问base,并且可以更改我们线程的state以取消其执行。
If any thread wants to change the state (the state as state of the timeout we spawned at the beginning and want to cancel), it should change the value in the 'base' array.如果任何线程想要更改我们在开始时产生的超时的 state(state 为 state 并想要取消),它应该更改“base”数组中的值。 I think this is pretty straightforward so far.到目前为止,我认为这非常简单。
There would be a huge problem if the values for continuing and stopping would be something bigger than just 1 byte.如果继续和停止的值大于 1 个字节,那将是一个巨大的问题。 Operation to write to the memory could actually take multiple operations, and thus, accessing the memory too early would cause unexpected results to occur, which is not what I am fond of.写入memory的操作实际上可能需要多次操作,因此,过早访问memory会导致出现意外结果,这不是我喜欢的。 Though, as I earlier mentioned out, what if the value is very small, 0 or 1?但是,正如我之前提到的,如果值非常小,0 或 1 怎么办? Wouldn't it matter at all at what time the value is accessed at?在什么时候访问该值根本不重要吗? We are interested only in 1 byte, or even 2 or 4 bytes or the whole number, even 8 bytes wouldn't make any difference in this case, would they?我们只对 1 个字节,甚至 2 个或 4 个字节或整数感兴趣,在这种情况下,即使 8 个字节也没有任何区别,不是吗? In the end, there is no worry about receiving an invalid value, since we don't care about 32bit value, but just 1 bit, no matter how many bytes we would be reading.最后,不用担心接收到无效值,因为我们不关心 32 位值,而只关心 1 位,无论我们要读取多少字节。
Maybe it isn't exactly understandable what I mean.也许我的意思并不完全可以理解。 Write/read operations do not consist of reading single bits, but byte(s).写/读操作不包括读取单个位,而是读取字节。 That is, if our value is not bigger than 255, or 65535, or 4 million million, whatever the amount of bytes we are writing/reading is, we shouldn't worry about reading it in middle of it being written.也就是说,如果我们的值不大于 255、65535 或 400 万,无论我们正在写入/读取的字节数是多少,我们都不应该担心在写入过程中读取它。 What we care about is only one chunk of what is being written, the last or the first byte(s).我们关心的只是正在写入的内容的一部分,即最后一个或第一个字节。 The rest is completely useless to us, so no need to worry about it all being synced at the time we access the value. rest 对我们来说完全没用,所以不用担心在我们访问值的时候会同步。 The real problem starts when the value is being written to, but the first byte written to is at the end, which is useless to us.真正的问题是在写入值时开始,但写入的第一个字节在末尾,这对我们来说毫无用处。 If we read the value at that moment, we will receive what we shouldn't - no cancel state instead of cancel.如果我们此时读取该值,我们将收到我们不应该收到的信息 - 不取消 state 而不是取消。 If the first byte, given little endian, was to be read first, we would receive valid value even if reading in the middle of write.如果给定小端序的第一个字节首先被读取,即使在写入中间读取,我们也会收到有效值。
Perhaps I am mangling and mistaking everything.也许我正在破坏和误解一切。 I am not a pro, you know.我不是专业人士,你知道的。 Perhaps I have been reading trashy articles, whatever.也许我一直在阅读垃圾文章,无论如何。 If I am wrong about anything at all, please correct me.如果我有任何错误,请纠正我。

Except for some specialised embedded environments with dedicated hardware, there is no such thing as "one operation, which is small enough to keep it in one chunk, an operation like changing a single bit".除了一些具有专用硬件的专用嵌入式环境外,没有所谓的“一次操作,小到可以将其保持在一个块中,就像改变一个比特一样的操作”。 You need to keep in mind that you do not want to simply overwrite the special bit with "1" (or "0").您需要记住,您不想简单地用“1”(或“0”)覆盖特殊位。 Because even if you could do that, it might just coincide with some other thread doing the same.因为即使你能做到这一点,它也可能与其他一些做同样的线程相吻合。 What you need in fact to do is to check whrther it is already 1 and ONLY if it is NOT write a 1 yourself and KNOW that you did not overwrite an existing 1 (or that writing your 1 failed because of a 1 already being there).实际上你需要做的是检查它是否已经是 1 并且只有当它不是你自己写一个 1 并且知道你没有覆盖现有的 1 (或者写你的 1 因为 1 已经存在而失败) .

This is called the critical section.这称为临界区。 And this problem can only be solved by the OS, which happens to know or be able to prevent about other parallel threads.而这个问题只能由操作系统来解决,操作系统恰好知道或能够阻止其他并行线程。 This is the reason for the existence of the OS-supported synchronisation methods.这就是存在操作系统支持的同步方法的原因。

There is no easy way around this.没有简单的方法可以解决这个问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM