Blogs

重要的编程概念(即使在嵌入式系统上)部分III:波动率

杰森萨赫斯2014年10月10日

1vol·a·tile adjective \ˈvä-lə-tə我,特别是英国 - ˌtī(-ə)l\
:可能会以非常突然或极端的方式改变
:有或表现出极端或突然的情感变化
:可能会变得危险或失控

merriam-webster在线词典

本系列的其他文章:

相信它与否,当我第一次想到写这群文章时,我会写的 五篇关于五个概念的文章。我想,嘿,基础知识很简单,我’我要保持简短而甜蜜。基础知识 pretty simple —但是,一些有趣的微妙之处已经挡路了,我认为我’d更好地保持每个概念分开,这是一件好事,因为不可变节必须是一个相当长的文章。一世 承诺 我自己波动率将会更短,因为那里的刚刚没有’要说太多了。它会更短—不是因为我最初想到的原因。

当您达到核心理念时,对编程波动性的重要性实际上只是识别某些数据可以不可预测和没有警告的情况。如果你是 当然 ,我的意思是真的肯定的是,你了解这一切的所有含义,前进,标记它 TL;博士 并继续前进。

好的,所以最后一篇文章是不可变的,这篇文章是波动性的… isn’这只是不可变的相反吗?出色地…也许是核心概念,但不是含义。

我在不可变的文章中讨论了一些落在功能规划主题的不可变性文章中。让’思考作为一种塔的编程领域,塔中的位置对应于涉及概念性视角的一些评估。使用嵌入式微控制器的软件工程师靠近地板;做游戏编程的工程师有点高;在中间的文字处理器和税务应用程序和其他桌面应用程序上工作的工程师;使用大规模分布式系统的人进一步;正在进行编程语言研究的功能向导位于顶部。 (而且家伙写作编译器在整个地方遍布。)但是你’在地下室和底下地下室的情况下,在那里的电动开关设备和水和下水道管道,以及锅炉发火和所有的锅炉。我们每天都依靠他们,但我们’重复害怕他们和唐’非常相信他们。所以他们是谁,无论如何?

波动率101:不关注幕后的那个男人!

让’现在忽略了地下室的POSSE。忘了我甚至提到了他们。

不同的类比时间:假装你是一个学士学位,孤独之王。你拥有自己的房子,在一个大,安静,树木繁茂的地段的中间,距离道路和你的任何邻居几百英尺。你自己住在那里,没有女朋友,没有宠物。

你决定有一天在沙发的一端的一堆旧账单中间涂上一摞狗耳信包的背面。也许它’我想到了一个库存提示,也许是它’关于啤酒酿造。它’s your house, so it’s your prerogative.

六个月后,桩仍然存在,在沙发的一端,你决定拿起狗耳信封并阅读纸条。它’就像你离开一样。

你的朋友杰伊有一个类似的想法,他在沙发上写了一天。但杰伊和他的妻子住在一起,他在几个小时后整理起居室时拿起信封,并把它扔掉了。第二天杰伊去寻找信封,而且已经消失了,他和他的妻子争辩说。布拉拉等我可以’找出你不做的事情’尊重我总是把这个地方留在这个地方清洁Blah Blah Blah。

简单地说:杰伊住在一个比你挥发的世界里。

好的,我希望你有比喻。编程是一种操纵计算环境的方式,如果是的话’唯一的实体,你可以做很多假设。台式电脑上的单线程编程是一块蛋糕。它’s like your bachelor house. Set a variable called doodad in your program to the value 367 一 day, and as long as the program is still running, and you don’t change it yourself, it will remain 367 forever. In fact, you don’t even have to look at it. You know it will be 367.

当您的计算环境中有其他实体时,波动性进入游戏。

波动源#1:其他线程

其中一个可能是显而易见的:并发线程和流程。让’s say you’re writing an application which has two threads, a main thread and a worker thread, which share the variable doodad. Just because the main thread writes 367 to doodad doesn’t意味着工人线程是’T将在那里写一些其他价值。事实上,为了让这些线程共存,他们需要有一些基础规则。它’不像你的朋友杰伊’房子。杰伊看着沙发上的纸条,并认识到它不是’那里。软件程序不’T有那种奢侈品。如果杰伊是一个软件计划,他就会看着沙发,位于他的女儿’他的青少年杂志在完全相同的地方,而不是在啤酒酿造成分上寻找他的注意事项,他阅读了文本 Miley Cyrus发现放射性Canteloupe,用错误的成分酿造一批啤酒,最终与其他人一起去世。

在并发线程之间没有单一的分享数据的最佳方法。最简单和最安全的方法是 独家访问:只有一个控制线程必须一次访问数据。 (杰伊可以使用沙发并在信封后面写一个注释,但当他’s done, he has to put it somewhere safe. Same with his daughter and the teen magazine. Leave the sofa, and you have to put your stuff away.) In software, we use locks or mutexes to control exclusive access. Two or more processes must decide to follow the same rules that in order to access the variable doodad, they will have to acquire the lock doodad_lock, perform any reads and writes, and then release doodad_lock.

还有其他方法可以提高性能,就像 单作家多读器锁 或者 锁定技术 但它们更复杂,更难保证正确性。一般来说,如果你’没有流利的编程,唐’使用这些技术“real”程序,并将其局限于您自己的私人实验。这篇文章不是一般的并发,所以我’不打算详细介绍更复杂的技术。如果你’在Java中编程,经典参考是 实践中的Java并发 By Brian Goetz,Tim Peierls,Joshua Bloch,Joseph Bowbeer,Doug Lea和David Holmes。无论如何,如果你’刚刚进入并发,好运。它可以是一个可怕的世界。

波动源#2:机器中的幽灵

其他波动源是由于底层计算环境中的软件和硬件功能。例如,软件源与操作系统相关联,并且很少直接访问,除非您决定不遵守规则并开始黑客入围OS管理的私人数据。

硬件波动源可能更熟悉嵌入式软件工程师。这些包括与I / O端口或外围设备交互的微控制器中的任何特殊功能寄存器。您可能熟悉其中至少一些:计时器,计数器,状态寄存器,那种东西。在没有操作系统的微控制器中,它’由您记住,许多这些寄存器可以随时更改。

这个领域的经典领域是 读取修改 - 写问题。让’S表示您希望更改输出端口的一个引脚的状态。但是,您的微处理器是8位或16位处理器,以改变您必须写入整个内存位置的输出。要更改一个输出引脚,您必须读取旧值,更改适当的位,然后写回来。在一些8位PIC微处理器中,读取旧值意味着访问引脚上存在的实际逻辑电平,这可以随时改变:输入受到连接到它们的任何硬件,并且可以通过瞬时改变输出短路故障。因此,在这些处理器上处理I / O端口的安全方法是维护所需输出引脚状态的私有副本。如果要更改输出引脚,请修改副本,然后写入输出端口寄存器。这样,数据只能以单向流动,并且您只能从副本写入输出端口。较新的处理器对每个方向具有单独的寄存器,用于写入输出的一组数据锁存寄存器以及用于读取PIN状态的另一组端口输入寄存器。或者它们具有用于设置/清除/切换位的专用寄存器:将1个写入适当的位为这些特殊寄存器之一,并且PIN状态将改变;写一个0离开比特不变。 TI.’S C2000处理器具有此类寄存器。

编程语言支持波动性

In the immutability article, I talked about the const keyword in C/C++ and the final keyword in Java. Both languages have a volatile keyword, which I’请描述,但我对此做了一些犹豫。你’ll occasionally need to use volatile in C and C++, but in Java, you should not be using volatile unless you really know what you are doing. (Ironically the semantics of volatile in Java are more useful and are more clearly defined than they are in C and C++.)

In both languages, the volatile keyword is not really for your benefit; instead, it tells the compiler that it needs to expect that another source of control (another thread, the operating system, or the hardware itself) may be modifying data asynchronously, and therefore the compiler cannot make certain optimizations. The classic example in C is a blocking loop:

volatile bool ready_flag;

void wait_flag()
{
    ready_flag = false;
    while (!ready_flag)
       pause_for_an_instant();
}

void signal_flag()
{
    ready_flag = true;
}

Here, one thread can call wait_flag() 和 execute the while loop until a second thread calls signal_flag(). If the volatile qualifier is not there, then the compiler is free to optimize access to ready_flag 和 decide that since it has just set ready_flag = false; at the beginning of the loop, it can assume that ready_flag is always false 和 the while loop will never exit. Including volatile tells the compiler it must not make that assumption, and that each evaluation of the contents of the C variable ready_flag requires a memory read to its storage location. Similarly, any assignment to ready_flag actually requires a memory write. For example, suppose you have this in C:

volatile int answer_to_the_universe;

void something_or_other()
{
    answer_to_the_universe = 54;
    answer_to_the_universe = 42;
}

In this case, the compiler is required to write 54 to memory and then 42 to memory. If answer_to_the_universe weren’t declared as volatile, the compiler could look at this program and decide, “嘿,看,Moron程序员再次做蠢事。他’■写两次到相同的变量。第一次分配54次从未被使用过,所以我可以优化它。” But with volatile, it does exactly what you ask. This example isn’实际上是那么远的;一些嵌入式处理器需要某些背对背序列写入同一系统寄存器以解锁内存区域并允许它们更改。

后退 to our wait_flag()signal_flag() example:

In a system with an OS, pause_for_an_instant() should call an appropriate sleep() 或者 wait() function to release control to the OS. In low-level embedded systems without an OS, if signal_flag() is called in an interrupt service routine, and there’没有别的是主线代码,但等等,它’SOK使用旋转循环:

void wait_flag()
{
    ready_flag = false;
    while (!ready_flag)
       ;
}

Also, the volatile qualifier acts similarly to the const qualifier, in that it is a constraint. Variables marked volatile cannot be passed by reference to a function, unless that function’s argument is a volatile * 或者 volatile & (in C++). If we want to rewrite the wait_flag()signal_flag() functions so they don’T访问全局变量,我们’D必须使用此语法:

void wait_flag(volatile bool *pready_flag)
{
    *pready_flag = false;
    while (!*pready_flag)
       pause_for_an_instant();
}

void signal_flag(volatile bool *pready_flag)
{
    *pready_flag = true;
}

Unless we use the volatile qualifier in the function signature, we cannot pass in the address to a volatile variable to a function. If we remove the volatile, the compiler will report an error:

/* Wrong! This version of wait_flag()
 * allows the compiler to optimize out
 * the read access to pready_flag
 * at the top of the while() loop 
 * incorrectly, and it will cause
 * a compilation error if you call
 * wait_flag() with a volatile pointer.
 */
void wait_flag_bad(bool *pready_flag)
{
    *pready_flag = false;
    while (!*pready_flag)
       pause_for_an_instant();
}

这是你的关键概念 必须 know if you are going to be working with peripheral registers in embedded systems, even if you have no intention of mucking around with concurrency. If you want to write functions that accept an address of a volatile peripheral register, such as a routine that can write bytes to one of several serial ports, the function parameter has to include a volatile qualifier. Also, like the const keyword, the compiler will automatically promote non-volatile pointers and references to volatile, but to go the other way is not safe. (In other words, wait_flag() can be called with a non-volatile pointer, but wait_flag_bad() can’t be called with a volatile pointer.)

Note that constvolatile 是 not exclusive: const just tells the compiler that you promise not to change the data, whereas volatile tells the compiler that something else might be changing the data. So functions that read peripheral registers by address, but do not write them, should look like this:

bool data_available (const volatile uint16_t *uart_status_ptr)
{
    return (*uart_status_ptr & DATA_AVAILABLE_BIT) != 0;
}

The const keyword is optional; it helps the compiler check that you are faithful to your promise not to write to the pointer. The volatile keyword is required if you pass in the address to a volatile register.

When else should you use volatile in C and C++?

在这里,我们留下了那里愉快的明亮的公路,进入阴暗的返回胡同,愚蠢地匆匆忙忙,天使恐惧地踩踏。那’s right, it’s 未定义的行为 车道。这就是C标准有效地说,任何事情都会发生。从技术上讲,如果您编写包含未定义行为的C代码,则编译器可以创建任何对象代码。 任何事物! 它可以删除你的硬盘。实际上是’发生,但编译器可以以你的方式行事’t expect because it’允许做任何事情。实际上,标准正在告诉编译器它不起作用’T必须负责,处理未定义行为所需的任何操作都可以,因为程序员永远不会这样做。正确的?如果源代码包含未定义的行为,则编译器可以删除所有符号q‘s in them, it doesn’T必须让堆栈访问正确,它可以冷静下来并创建一个无限的循环,占据0xCafeBabe到控制台。无论编译器所做的还可以,因为它’案例之一,永远不会发生。 C中的程序永远不会尝试取消引用空指针,因为它’不确定的行为。它’s the program writer’责任确保它不起作用’t happen.

远离未定义的行为通道的短距离是另一个通道,实现定义的行为方式,其中编译器可以做任何事情,但它必须记录该行为。所以至少它必须告诉你它’S将是不负责任的…在实践中意味着它’至少在编译器作者的意见中尝试做正确的事情。看看附录A XC16编译器用户’s Guide, 例如— here’s 12 pages covering everything from what happens if you right-shift a negative signed integer, to what the value of the character constant '\n' is, to what happens when you cast an integer to a pointer or a pointer to an integer.

记住 这个图表?未定义的行为车道和实施定义的行为方式分散在语言的条纹之间,如果您想在世界的这一部分地区徘徊,您’D最好有您的语言标准和编译器手册的副本,并确切地理解它们’re saying.

The volatile keyword leads to these fringes… here’s的东西xc16用户’s Guide has to say about volatile:

Another use of the volatile keyword is to prevent variables being removed if they are not used in the C source. If a non-volatile variable is never used, or used in a 对程序没有影响的方式’s功能,然后可以在代码之前删除 由编译器生成。

让’s表示我想创建一个运行时确定的延迟,例如:

function delay(uint16_t n)
{
    uint16_t i;
    for (i = 0; i < n; ++i)
        ;
}

The compiler is free to optimize out the loop because the variable i is never used. So one way that will fix this —至少对于XC16编译器;一世’不确定此技术是便携式的— is as follows:

function delay(uint16_t n)
{
    volatile uint16_t i;
    for (i = 0; i < n; ++i)
        ;
}

Another use of volatile in C is that it may help you prevent the compiler from reordering memory accesses among volatile variables. Maybe. From what I’读这是C标准HASN的那些东西之一’T清楚地记录了它’达到编译器的解释。

In Java, volatile has some 非常具体的语义: it essentially guarantees certain ordering constraints of memory reads and writes among threads. This is really hard to use correctly, and since there are plenty of built-in concurrency features, you should be using them rather than getting your fingers dirty with volatile.

和这里’我们在地下室进入了Posse的地方。大学教师’t worry, we won’这是很长的,因为它’那里的可怕。

(顺便说一下,其他计算机语言有一系列方法: Fortran. has a C-like volatile keyword; Python, Ruby, Go, and Rust do not, preferring to keep this behavior out of the core language and leave it up to standard libraries, which is probably the better choice.)

波动率304:尤达,内存模型和地下室的POSSE

你准备好了吗?什么知道你准备好了?— Yoda, 帝国反击战

哦,我喜欢借口浏览Web的星球大战报价。你还记得yoda和他奇怪的说话方式吗?为什么奇怪?部分原因是他从您听到母语扬声器的方式不同地订购单词。我们仍然可以理解他,因为这些词有相同的含义。

重新排序也可能发生在计算机程序中。我们倾向于了解计算机程序作为指定一系列步骤的方式。我们编写这些序列,编译器将它们直接转换为汇编语言,对吧?您在计算机程序中写入语句的顺序有两个原因与计算机上执行的顺序不同。

想象一下你’在炎热的夏日结束时工作,你的室友电话要求你在杂货店拿起一些冰淇淋和副本 幻影tollbooth. 在图书馆。如果你’聪明,你可能会先着一本书然后又是冰淇淋,让冰淇淋不太可能融化。但这涉及判断它’不好让冰淇淋融化,所以让’忘了这一刻。另一方面,让’如果你去图书馆,那就有一个较短的路线,得到冰淇淋,然后回家,而不是如果你先得到冰淇淋然后去图书馆。无论如何,那里’真的没有限制你可以先得到的限制;两者都是等同的选择,但有一个选择的订购更加优化。

以同样的方式,编译器可以重新排序程序语句以优化程序大小或执行时间,只要它将它们从源文件减少到编译输出时’t更改程序语义。

即使编译器也没有’t重新订购您的内存访问’另一件事要思考。再次,当我们了解计算机编程时,我们倾向于想到存储器访问,尽管计算机有一组大型Cubbyhole,在某种序列中发生了读取和写入。在一个线程的程序中,我们认为这将作为一系列访问序列,如学生在他们的大学宿舍的大厅工作,一次将事物从一个古宝孔移动到另一个袖子。在一个多线程的程序中,我们认为这并行发生,就像许多宿舍工作者都在大厅里移动。

问题是这一问题’t match what’实际上在现代的多核处理器中进行。因为在许多处理器中,那里’s每个处理器核心和内存之间的缓存。 (所以’更像每个宿舍工作者都有自己的一套古比,而在那里’S一些神奇的气动管,使Cubbyholes同步,并交换进出不同的存储位置。),如果你要从每个核心看内存 ’S的角度(通过缓存),事件的顺序可能看起来不同。不是因为编译器正在做一些不同的东西,而是因为处理器本身必须优化内存访问,并且这样做,它被允许重新排序存储器访问以及其他核心所可见的方式,只要他们不起’T更改每个控制线程内的操作的语义。

地下室中的人是处理器建筑师和设计师和图书馆作家,他们必须处理这个缓存和内存模型和障碍的世界。他们’重新专业人士,他们知道他们是什么’re doing. But really don’想留在这种思维水平;相反,您应该使用有适当的内存语义的库,以便您可以在更高级别的情况下思考事物。地下室POSSE使用记忆障碍和对Java这样的事情的担忧 发生在之前 relationship.

请注意c’s volatile qualifier does not necessarily guarantee memory access ordering. If you need to ensure that thread A writes variable foo after variable bar changes, and you want thread B to see foo change first before bar, you need to use the library functions and intrinsics properly. Java’s volatile is specified with certain memory ordering semantics, but again, use the concurrency libraries rather than trying to build something yourself just using volatile.

I’除了指出一些关于进一步阅读的文章,不再对这个问题说更多有关这个问题。让’离开地下室的工作,唐’忘记了我们依靠它们很多。

包起来

重申:

  • 计算机编程中的波动性是数据可以从程序之外的外部代理更改’S控制线程。该代理可以是另一个控制线程,或操作系统或计算硬件本身。 (特别是嵌入式微处理器中的控制寄存器。)
  • The volatile qualifier in C has a couple of important aspects:
    • 它用于表示受外部变化的程序变量。
    • 它要求编译器在评估或分配程序变量时执行内存读取和写入。
    • 它还可以防止编译器显然优化“useless”至少在某些编译器中计算。
    • Functions accepting arguments that are pointers (or references in C++) that are not qualified with volatile can accept only non-volatile inputs. If the function signature has an argument qualified with volatile, it can accept pointers to either volatile 或者 non-volatile variables.
  • The volatile keyword in Java is used to denote certain types of synchronization of memory accesses between threads. Don’你自己使用它;相反,使用更高级别的设施 java.lang.concurrent.
  • 在C或Java程序中以特定顺序编写语句’t意味着编译器和处理器可以’只要它没有重新排序它们’t更改结果计算。了解内存访问重新排序中涉及的所有机制都很棘手,它’留给专家并更容易将其留给专家,而是使用以规定的方式设计和测试的库。

进一步阅读地下室

如果您确实想了解有关地下室中的POSSE的更多信息,那么这里有一些您可能会发现有用的资源。

没有参考Jeff Suphing就没有对现代记忆模型的讨论’s blog:

接下来的是: 单身人士


©2014年杰森M. Sachs,保留所有权利。


要发布回复评论,请单击连接到每个注释的“回复”按钮。发布新的评论(不是回复评论),请在评论的顶部查看“写评论”选项卡。

注册将允许您参加所有相关网站的论坛,并为您提供所有PDF下载。

注册

我同意 使用条款隐私政策.

尝试我们偶尔但流行的时事通讯。非常容易取消订阅。
或登录