Поток данных не соответствует сверхмалой tiny модели памяти
Мы ВКонтакте
JS: 2.14.23
CSS: 4.9.13
jQuery: 3.6.0
DataForLocalStorage: 2022-05-10 03:46:02-standard
jQuery
jQuery UI
Bootstrap
Font Awesome
Зенченко Константин Николаевич (Старший модератор)
Assembler
Программирование Компьютеры
Создание программ на языке Assembler.
Здравствуйте, уважаемый посетитель!
Вас приветствует команда экспертов рассылки "Assembler (Ассемблер)".
Мы поможем Вам найти ответ на Ваш вопрос по программированию на языке Ассемблера.
Для того, чтобы у экспертов не возникало дополнительных вопросов,
большая просьба прочитать памятку "Как задать вопрос".
Успехов в программировании на языке ассемблера!
Рекомендуемые ресурсы Интернета:
Очень полезная информация
Ralf Brown's Interrupt List
Книги по Ассемблеру
Assembler для DOS, Windows и UNIX - Зубков С.В.
Assembler. Практикум - Юров В.И.
Assembler. Учебник для вузов - Юров В.И.
Assembler. Учебный курс - Пирогов В.Ю.
Архитектура IBM PC и язык Ассемблера - Митницкий В.Я.
Программирование на языке ассемблера IBM PC - Пильщиков В. Н.
Ассемблер в задачах защиты информации - Абашев А.А.
Ассемблер для процессоров Intel Pentium - Магда С.Ю.
Ассемблер и дизассемблирование - Пирогов В.Ю.
Ассемблер и программирование для IBM PC - Абель Питер
Изучаем ассемблер - Крупник А.Б.
Искусство программирования на Ассемблере. Лекции и упражнения - Голубь Н.Г.
Персональные ЭВМ IBM PC и XT. Программирование на языке ассемблера - Скэнлон Л.
Системное программирование на персональном компьютере - Фельдман С.К.
Язык ассемблера для процессоров Intel - Ирвин, Кип
Язык ассемблера. Уроки программирования - Рудаков П.И.
Ассемблер для Windows - Пирогов В.Ю.
Ассемблер. Разработка и оптимизация Windows-приложений - Магда Ю.С.
Программирование на аппаратном уровне. Специальный справочник - Кулаков В.
Программирование аппаратных средств в Windows - Несвижский В.
Программирование на языке ассемблера для микропроцессоров 8080 и 8085 - Л. Левенталь
Программирование на языке ассемблера для микроконтроллеров семейства i8051 - Каспер Эрни
Книги по WinAPI (на С)
Создание эффективных WIN32-приложений - Рихтер Джеффри
Программирование для Windows 95 - Петцольд Ч.
Памятка: Как задать вопрос
- Какой процессор используется. Если х86-совместимый, то можно не указывать. За исключением случая,
когда необходимы специальные возможности (например, MMX, 3D-NOW!, SSE, SSE2, . );
- Какая платформа (для х86-совместимых: ДОС/Windows/*nix);
- Также укажите это - "Окно" или "Консоль";
- Какой ассемблер предпочителен (TASM, MASM, NASM, FASM, WASM, . );
- Для программ операционной системы ДОС предпочтительная модель памяти (TINY, SMALL, LARGE, . ),
а также тип процессора (от этого зависит использование команд, 32-битных регистров и т.п.).
В случае работы в графическом видеорежиме, не забудьте указать его номер или параметры (напр., 640x480 x 16 цветов);
- Размер переменных (байт, слово, двойное слово), с которыми должна работать программа,
а также являются они числами со знаком или без знака.
- Перед тем, как посылать вопрос, воспользуйтесь поиском, вполне возможно, что подобное уже спрашивали раньше;
- Эксперты с бОльшим удовольствием помогут Вам, если Вы покажете результаты своих проб. Не переживайте насчет ошибок, подправим;
- Если есть дополнительная информация (методичка, описание, . ), дайте ссылку на эту информацию;
- Если для написания программы необходим скриншот, прикрепите картинку к вопросу или разместите её на каком-либо бесплатном хостинге и дайте ссылку. Например, это могут быть ImagesHost.ru, PINPIC.RU, picatom.com, radikal.ru и т.д.;
- НЕЛЬЗЯ использовать красный шрифт в вопросе - этот цвет используют модераторы. Обычно красным цветом отмечают нарушения;
- Не следует также раскрашивать буквы разными цветами - по меньшей мере, такую гирлянду разноцветных букв хуже воспринимать;
- Не рекомендуется ПИСАТЬ ТОЛЬКО ЗАГЛАВНЫМИ БУКВАМИ и использовать много вопросительных и восклицательных знаков: "ЧТО МНЕ ДЕЛАТЬ . ПОМОГИТЕ . " - такие вопросы будут редактироваться в соответствии с общепринятыми правилами;
- Если ответ эксперта Вас не удовлетворяет, не спешите снижать оценку, попросите переделать программу с новыми уточненными параметрами;
- При проблемах с тестированием предложенного решения (программа висит и т.п.) проверьте, в каком формате (COM или EXE) исполняемый файл. Наличие модели памяти TINY или строки ORG 100h обычно предполагает COM-формат.
Программа CompModel некого Жмакина
Задание: Вывод на дисплей трех текстов, хранящихся в памяти, с задержкой
Используемые ВУ: Дисплей, таймер
Пояснение: Первый текст вводится сразу при запуске программы, второй – через 15 с, третий – через 20 с после второго
Два решения
1. Текст программы с программным анализом флагов готовности ВУ.
2. Текст программы с обработчиком прерывания.
Здравствуйте, emicent.lollipop!
Даю программу с обработчиком прерывания.
По адресу 50 необходимо записать блок параметров, который задает, когда, где и что выводить на дисплей
Каждый параметр состоит из 3 полей:
1) времени таймера, через сколько должен сработать следующий параметр
2) позиции на дисплее, вычисляется, как 16*строка+колонка
3) сама строка в коде ASCII, заканчивается нулем
Все параметры идут подряд. Для последнего параметра поле времени таймера можно оставить равным 0, т.к. оно не нужно.
Например, можно задать такой блок параметров:
15,1,49,50,51,52,0,20,17,65,66,0,0,33,57,0
Тогда выведутся:
В первой строке со второй колонки текст "1234" и запустится таймер на 15 секунд
Через 15 секунд во второй строке со второй колонки текст "AB" и запустится таймер на 20 секунд
И наконец, через 20 секунд в третьей строке со второй колонки текст "9"
После чего таймер остановится и произойдет останов
При описании постфиксных операций operator++ или operator-- последний параметр должен описываться с типом int.
Library contains COMDEF recors - extended dictionary not created
Предупреждение утилиты TLIB (библиотека содержит записи COMDEF - расширенный словарь не создается)
Объектная запись, добавляемая к словарю, содержит запись COMDEF. Это несовместимо с параметром расширенного словаря.
Создаваемая библиотека не может быть построена с текущим размером страницы библиотеки.
Спецификации типа компоновки, например extern "C", допустимы только на уровне файла. Перенесите данное объявление функции на уровень файла.
Левая часть операции присваивания должна являться адресуемым выражением. Сюда входят числовые переменные или указатели-переменные, ссылки на поля структур, либо обращение по ссылке через указатель, либо элемент массива, заданный индексом.
Аргумент в определении макрокоманды должен являться идентификатором. Компилятор встретил некоторый символ, который не может являться частью идентификатора, там, где ожидался аргумент.
Размер макрорасширения не может превышать 4096 символов.
Макроопределение не может расширяться более, чем на 4096 знаков. Такая ошибка часто появляется, если осуществляется регулярное макрорасширение. Макроопределение не может правильно расширять самое себя.
Macro substitute text строка is too long
Фатальная ошибка утилиты MAKE (строка текста макроподстановки слишком велика)
Текстовая строка макроподстановки переполняет внутренний буфер MAKE размером 512 байт.
Macro replace text строка is too long
Фатальная ошибка утилиты MAKE (строка текста замены слишком велика)
Текстовая строка замены переполняет внутренний буфер MAKE размером 512 байт.
К функции main предъявляются специальные требования. Одно из них состоит в том, что она не может иметь тип возврата иной, нежели int.
В командной строке найдена недопустимая запись.
Если в производном классе описывается виртуальная динамическая функция (DDVT), то соответствующая функция базового класса должна иметь тот же номер диспетчеризации, что и производная функция.
Если в производном классе описывается виртуальная динамическая функция (DDVT), то соответствующая функция базового класса должна также быть динамической.
Maximum precision used for member pointer type тип
Предупреждение этапа компиляции (для указателя-элемента типа тип используется максимальная точность)
Если при описании типа указателя-элемента его класс описан не полностью, и использован параметр -Vmd, компилятор должен использовать наиболее общее (и наименее эффективное) представление для данного типа указателя-элемента. Это может привести не только к генерации менее эффективного кода (и к получению большего чем нужно элемента-указателя), но привести также к проблемам с раздельной компиляцией. См. описание параметра -Vm в Главе 4 "Компилятор, работающий в режиме командной строки" в "Руководстве пользователя".
Когда в выражении используется функция-элемент, то она должна либо вызываться, либо нужно получать ее адрес с помощью операции &. В этом случае функция-элемент используется в недопустимом контексте.
Здесь предполагается имя элемента или структуры С++, но оно не найдено. Правая часть операции точки (.) или стрелки (->) должна быть именем элемента структуры или класса, указанного в левой части операции.
Это означает, что пользователь записал класс::элемент, где элемент - это обычный (не статический) элемент, и класс, связываемый с данным элементом, отсутствует. Например, допустима запись:
Статический элемент данных, перечислитель, элемент неименованного объединения или вложенный тип не может иметь то же имя, что и его класс. Имя, совпадающее с именем его класса, может иметь только функция-элемент или нестатический элемент.
В конструкторе класса С++, списке инициализации, следующем за заголовком конструктора, одно и то же имя элемента указано несколько раз.
Правая часть операций С++ .* или ->* должна описываться как указатель на элемент класса, задаваемый левой частью операции. В данном случае правая часть не является указателем элемента.
Memory full listing truncated!
Предупреждение утилиты TLIB (полный листинг памяти усечен)
Библиотекарь исчерпал память при создании файла листинга библиотеки. Файл будет создан, но он будет неполным.
Ассемблерный операнд не является ссылкой на память, которая требуется в данном месте. Скорее всего, вы забыли заключить индексный регистр в квадратные скобки, например, MOV AX,BX+SI вместо MOV AX,[BX+SI].
Компилятор обнаружил оператор break вне конструкции оператора switch или цикла.
Компилятор обнаружил оператор continue вне конструкции цикла.
Компилятор обнаружил десятичную точку в показателе степени константы с плавающей точкой.
Директива !elif обнаружена без соответствующей ей директивы !if.
Обнаружена директива !else без соответствующей ей директивы !if.
Была обнаружена директива !endif без соответствующей ей директивы !if.
В секции IMPORTS файла определения модуля имеется ссылка на запись, заданную с помощью имени модуля и порядкового значения. Когда запись задается порядковым значением, определению импорта нужно присвоить внутреннее имя. Это внутреннее имя, которое ваше программа использует для ссылки на импортируемое определение. В файле определения модуля должен использоваться следующий синтаксис:
COMDEF и VIRDEF смешивать нельзя. Чтобы запретить генерацию VIRDEF, включите параметр -Vs.
Mixing pointers to different 'char' types
Предупреждение этапа компиляции (Смешанное использование указателей на char)
Вы преобразовали указатель на char в указатель на unsigned char, либо наоборот без явного приведения типов. (Строго говоря, это допустимо, но на процессоре 8086 часто приводит к ошибочным результатам).
В конструкторе класса С++ каждый вызов конструктора базового класса в заголовке конструктора должен в случае, если имеется более одного непосредственного базового класса, включать имя базового класса.
Идентификатор объявлен более одного раза, что недопустимо. Это может произойти в случае противоречивых объявлений, например int a; double a;, в случае, когда функция объявлена двумя разными способами, либо при повторении некоторого объявления, не являющегося функцией extern или простой переменной.
В приложении определено несколько точек входа. Допускается только одна.
Большинство функций-операций С++ может являться элементами классов или обычными функциями, не входящими в класс, однако некоторые из них обязательно должны быть элементами класса. Это функции operator =, operator ->, operator() и преобразования типов. Данная функция не является функцией-элементом, но должна являться таковой.
Большинство функций-операций С++ должно иметь неявный или явный аргумент типа класса. Данная функция-операция была объявлена вне класса и не имеет явного аргумента типа класса.
Вы пытаетесь объявить идентификатор как базовый класс, тогда как он либо не является классом, либо не был еще полностью определен. Исправьте имя или реорганизуйте объявления.
Данное объявление пытается обратиться к идентификатору, как к тегу типа enum, тогда как он не был объявлен в качестве такового. Исправьте имя или реорганизуйте объявления.
Данная функция-операция С++ была неправильно объявлена с аргументами.
Когда operator++ или operator-- описывается, как функция-элемент, она должна описываться, как не имеющая параметров (например, префиксная версия операции) или с одним параметром типа int (постфиксная версия).
Когда operator++ или operator-- описывается, как функция, не являющаяся элементом, она должна описываться с одним параметром (например, префиксная версия операции) или с двумя параметрами (постфиксная версия).
Данная функция-операция С++ была неправильно объявлена с более чем одним аргументом.
Operator++ или operator-- были описаны как функция-элемент, а они должны описываться без параметров или с одним параметром типа int.
Данная функция С++ была неправильно объявлена.
Ваш исходный файл использовал операцию получения адреса (&)
в выражении, которое не может выполнять адресацию памяти, например, в случае регистровой переменной.
Операции вопросительный знак (?) и двоеточие (:) не соответствуют друг другу в данном выражении. Возможно, отсутствует двоеточие, либо неправильно вложена или отсутствует круглая скобка.
Конструктор класса С++ пытается неявно вызвать конструктор базового класса, тогда как этот класс был объявлен без базовых классов. Проверьте объявления.
В строковом выражении !if или !elif нет закрывающей кавычки.
No declaration for function функция
Предупреждение этапа компиляции (отсутствует объявление функции)
Имя файла в операторе включения !include не содержит закрывающей кавычки или угловой скобки.
Командная строка компилятора Borland C++ режима командной строки (BCC) не содержит имен файлов. Вы обязаны задать имя исходного файла.
Файл .DEF имеет семантическую ошибку. Возможно перед именем модуля вы забыли включить внутреннее имя для импорта.
Перед тем, как присваивать значение, вы должны определить имя макрокоманды.
No module definition file specified: using defaults
Предупреждение утилиты TLINK (не задан файл определения модуля: используются значения по умолчанию)
Компоновщик TLINK вызывался с одним из параметров Windows, но файл определения модуля не задан.
No program starting address defined
Предупреждение утилиты TLINK (не задан стартовый адрес программы)
Это предупреждение означает, что ни в одном модуле не задан начальный стартовый адрес программы. Обычно это происходит из-за того, что вы забываете выполнить компоновку с модулем инициализации C0x.OBJ. При компоновке с динамически компонуемыми библиотеками (DLL.) Windows такое предупреждение выводиться не должно.
No stack
Предупреждение утилиты TLINK (нет стека)
No stub fox fixup at адрес
Предупреждение утилиты TLINK (нет фиктивного модуля для корректировок по данному адресу)
Эта ошибка возникает, когда целью корректировки является оверлейный сегмент, но для внешней цели заглушки не найдено. Обычно это является результатом того, что вы объявляете общедоступным идентификатор, на который имеется ссылка в том же модуле.
No terminator specified for in-line file operator
Фатальная ошибка утилиты MAKE (для встроенной файловой операции не задано завершения)
Формирующий файл содержит операции командной строки && или
Non-const function функция called for const object
Предупреждение этапа компиляции (функция, не имеющая типа константы, вызвана для объекта-константы)
Функция-элемент, не имеющая типа константы (const), вызвана для объекта типа константы. Это ошибка, но выдается только предупреждение, и программа получает шанс заработать.
Nonportable pointer comparison
Предупреждение этапа компиляции (немобильное сравнение указателей)
В вашем исходном файле указатель сравнивается с не указателем, не являющимся нулевой константой. Если сравнение выполняется так, как вам нужно, можно использовать приведение типа, чтобы подавить вывод данного предупреждения.
Ненулевое целочисленное значение используется в контексте, где ожидается указатель или целое значение; размеры указателя и целого значения одинаковы. Если вы действительно намереваетесь использовать данное преобразование, следует выполнить явное приведение типов.
Nonportable pointer conversion
Предупреждение этапа компиляции (немобильное преобразование указателя)
Требуется выполнение неявного преобразования между указателем и интегральным типом, но эти типы имеют разный размер. Такое преобразование не может выполняться без явного приведения типов. Оно может и не иметь смысла, поэтому проверьте, действительно ли это преобразование вам необходимо.
Nonresident Name Table is greater than 64K
Предупреждение утилиты TLINK (нерезидентная таблица имен превышает 64К)
Максимальный размер нерезидентной таблицы имен - 64К. Компоновщик продолжает работу, но игнорирует последующие нерезидентные имена.
Нетипизированный формальный аргумент шаблона должен иметь скалярный тип. Он не может быть целым типом, перечислением или указателем.
Использовано ключевое слово, не соответствующее стандарту ANSI, в то время как задан параметр -A.
"Чистыми" (pure) могут объявляться только виртуальные функции, поскольку производные классы должны иметь возможность их переопределения.
Non-volatile function called for volatile object
Предупреждение этапа компиляции: (для объекта типа volatile вызвана функция, отличная от volatile)
В С++ функция-элемент класса была вызвана для объекта типа volatile, а сама функция после заголовка не была объявлена как "volatile". Для объекта volatile могут использоваться только функции volatile.
Файл, заданный с помощью параметра -f, не является формирующем файлом.
В вашем исходном файле содержится объявление некоторого неразрешенного типа; например, функции, возвращающую функцию или массив.
Not enough memory
Фатальная ошибка утилиты MAKE (недостаточно памяти)
Данная ошибка появляется, если вся рабочая память исчерпана. Вы должны попытаться осуществить то же на вычислительной машине с большой памятью. Если на вашей вычислительной машине имеется уже 640К, то упростите исходный файл.
Для работы компоновщика не хватает памяти. Попытайтесь уменьшить объем моделируемого в оперативной памяти диска или активного дискового буфера. Затем снова запустите TLINK. Если вы работаете в реальном режиме, попробуйте использовать параметр MAKE -S, удалив резидентные задачи и сетевые драйверы. При работе в защищенном режиме MAKE попробуйте уменьшить размер виртуального диска или активного дискового буфера.
Данная ошибка происходит, когда TLIB исчерпывает доступную память.
модуль not found in library
Предупреждение утилиты TLIB (не найден модуль в библиотеке)
При попытке выполнить для библиотеки операцию '-' или '*' указанный объект в библиотеке отсутствует.
Строковые и символьные управляющие последовательности больше шестнадцатиричного значения \xFF или восьмеричного \377 сгенерированы быть не могут. Двухбайтовые символьные константы могут задаваться при помощи второй обратной косой черты. Например, \x0D\x0A представляет собой двухбайтовую константу. Числовой литерал после управляющей последовательности следует разбить: Тем самым будет выведен возврат каретки и затем 12345.
Библиотекарь не может распознать запись заголовка объектного модуля, добавляемого к библиотеке, и предполагает, что это недопустимый модуль.
Обычные структуры Си могут инициализироваться набором значений, заключенных в фигурные скобки. Классы С++ могут инициализироваться с помощью конструкторов (если класс имеет конструкторы, локальных элементов, функций или базовых классов, которые являются виртуальными.
При закрытии временного встроенного файла вы задали что-то отличное от KEEP или NOKEEP.
Как 'const' или 'volatile' описан какой-либо другой объект, отличный от функции-элемента.
Функции С++ по умолчанию являются переопределяемыми, и компилятор присваивает каждой из них новое имя. Если вы хотите переопределить присваивание компилятором нового имени, объявив функцию "C", то можете сделать это только для одного из набора функций с тем же именем (в противном случае компоновщик обнаружит более одной глобальной функции с тем же именем).
С помощью операции delete не допускается использовать указатель-константу.
Функция С++ операция operator-> должна быть объявлена как возвращающая класс или указатель на класс (или структуру либо объединение). В любом случае это должно быть нечто такое, к чему применима операция ->.
Переопределенная операция С++ operator delete была объявлена иначе.
Переопределенная операция С++ operator delete была объявлена иначе. Опишите delete с одним параметром void*, а другим - типа size_t.
Переопределяемая операция была описана с типом, отличным от типа функции.
Операция new может объявляться с произвольным числом параметров, но обязательно должна иметь хотя бы один параметр, в котором будет находиться размер распределяемой памяти.
Переопределенная операция С++ operator new была объявлена иначе.
Переопределенная операция С++ operator new была объявлена иначе.
Out of memory
Фатальная ошибка этапа компиляции (недостаточно памяти)
Исчерпана общая рабочая память. Повторите компиляцию этого файла на машине с большей доступной памятью. Если у вас и так имеется 640К, следует упростить исходный файл.
TLINK исчерпал динамически распределяемую память, необходимую для процесса компоновки. Эта общая ошибка возникает во многих ситуациях, когда утилите TLINK не хватает памяти. Обычно это означает, что в компонуемых объектных файлах определено слишком много модулей, внешних идентификаторов, групп или сегментов. Вы можете попробовать уменьшить объем виртуальных дисков и/или активных дисковых буферов. При работе под Windows, чтобы освободить память, закройте одну или более прикладных программ.
Библиотекарь исчерпал память при создании расширенного словаря библиотеки. Библиотека будет создана, но не будет иметь расширенного словаря.
Библиотекарь пытается прочитать запись данных из объектного модуля, но не может получить достаточно большой блок памяти. Если добавляемый блок содержит большой сегмент или сегменты данных, то возможно добавление этого модуля перед другими модулями позволит решить проблему. Добавив модуль первым, вы получите дополнительную память для записи общедоступных переменных и списка модулей, добавляемых далее.
Библиотекарь исчерпал память выделяя пространство для отладочной информации, связанной с конкретным объектным модулем. Устранение отладочной информации из некоторых добавляемых в библиотеку модулей может устранить проблему.
Устройство вывода переполнено (обычно нет места на диске).
Overlays generated and overlay manager included
Предупреждение утилиты TLINK (сгенерированы оверлеи и включен оверлейный менеджер)
Это предупреждение выводится, если оверлеи создаются, но идентификатор __OVRTRAP__ не определен ни в одном из объектных модулей или компонуемых библиотек. Этот идентификатор определяется стандартной оверлейной библиотекой OVERLAY.LIB.
Оверлеи допустимы только в программах с моделями памяти medium, large и huge).
Единственный случай, когда имя переопределенной функции может использоваться без фактического вызова, это инициализация или присваивание переменной или параметра соответствующего типа. В данном случае переопределяемое имя функции используется в каком-то другом контексте.
Overloaded prefix 'operator операция' used as a postfix operator
Предупреждение этапа компиляции (переопределенный префикс 'operator операция' используется как постфиксная операция)
В последней спецификации С++ теперь можно переопределить префиксную и постфиксную версию операций ++ и --. Чтобы можно было компилировать старый код, когда переопределенной является только префиксная операция, но она используется в постфиксном контексте, Borland C++ использует префиксную операцию и выводит данное предупреждение.
Для написания эффективных и корректных многопоточных приложений очень важно знать какие существуют механизмы синхронизации памяти между потоками исполнения, какие гарантии предоставляют элементы многопоточного программирования, такие как мьютекс, join потока и другие. Особенно это касается модели памяти C++, которая была создана сложной таковой, чтобы обеспечивать оптимальный многопоточный код под множество архитектур процессоров. Кстати, язык программирования Rust, будучи построенным на LLVM, использует модель памяти такую же, как в C++. Поэтому материал в этой статье будет полезен программистам на обоих языках. Но все примеры будут на языке C++. Я буду рассказывать про std::atomic , std::memory_order и на каких трех слонах стоят атомики.
В стандарте C++11 появилась возможность писать многопоточные программы на C++, используя только стандартные средства языка. В то время многоядерные процессоры уже завоевали рынок. Особенность выполнения программы на многоядерном процессоре в том, что инструкции программы из разных потоков физически могут исполняться одновременно. Ранее многопоточность на одном ядре эмулировалась частым переключением контекста исполнения с одного потока на последующие. Для оптимизации работы с памятью у каждого ядра имеется его личный кэш памяти, над ним стоит общий кэш памяти процессора, далее оперативная память. Задача синхронизации памяти между ядрами - поддержка консистентного представления данных на каждом ядре (читай в каждом потоке). Очевидно, что если применить строгую упорядоченность изменений памяти, то операции на разных ядрах уже не будут выполнятся параллельно: остальные ядра будут ожидать, когда одно ядро выполнит инструкции изменения данных. Поэтому процессоры поддерживают работу с памятью с менее строгими гарантиями консистентности памяти. Более того, разработчику программы предоставляется выбор, какие гарантии по доступу к памяти из разных потоков требуются для достижения максимальной корректности и производительности многопоточной программы. Задача предоставить разные гарантии по памяти решалась по-разному для разных архитектур процессоров. Наиболее популярные архитектуры x86-64 и ARM имеют разные представления о том, как синхронизировать память.
Язык C++ компилируется под множество архитектур, поэтому в вопросе синхронизации данных между потоками в С++11 была добавлена модель памяти, которая обобщает механизмы синхронизации различных архитектур, позволяя генерировать для каждого процессора оптимальный код с необходимой степенью синхронизации.
Отсюда следует несколько важных выводов: модель синхронизации памяти C++ — это "искусственные" правила, которые учитывают особенности различных архитектур процессоров. В модели C++ некоторые конструкции, описанные стандартом как undefined behavior (UB), могут корректно работать на одной архитектуре, но приводить к ошибкам работы с памятью на других архитектурах.
Наша задача, как разработчиков на языке C++, состоит в том, чтобы писать корректный с точки зрения стандарта языка код. В этом случае мы можем быть уверены, что для каждой платформы будет сгенерирован корректный машинный код.
Код каждого потока компилируется и выполняется так, как будто он один в программе. Вся синхронизация данных между потоками возложена на плечи атомиков ( std::atomic ), т.к. именно они предоставляют возможность форсировать "передачу" изменений данных в другой поток. Далее я покажу, что мьютексы ( std::mutex ) и другие многопоточные примитивы либо реализованы на атомиках, либо предоставляют гарантии, семантически похожие на атомарные операции. Поэтому ключом к написанию корректных многопоточных программ является понимание того, как конкретно работают атомики.
Семантика acquire/release классов стандартной библиотеки
Механизм acquire/release поможет понять гарантии синхронизации памяти, которые предоставляют классы стандартной библиотеки для работы с потоками. Ниже приведу список наиболее часто используемых операций.
std::thread::(constructor) vs функция потока
Вызов конструктора объекта std::thread ( release ) синхронизирован со стартом работы функции нового потока ( acquire ). Таким образом функция потока будет видеть все изменения памяти, которые произошли до вызова конструктора в исходном потоке.
std::thread::join vs владеющий поток
После успешного вызова join поток, в котором был вызван join, "увидит" все изменения памяти, которые были выполнены завершившимся потоком.
std::mutex::lock vs std::mutex::unlock
успешный lock синхронизирует память, которая была изменена до вызова предыдущего unlock.
set_value синхронизирует память с успешным wait .
И так далее. Полный список можно найти в книге [1].
Три слона
На мой взгляд, основная проблема с атомиками в C++ состоит в том, что они несут сразу три функции. Так на каких же трех слонах держатся атомики?
Атомики позволяют реализовать… атомарные операции.
Атомики накладывают ограничения на порядок выполнения операций с памятью в одном потоке.
Синхронизируют память в двух и более потоках выполнения.
Атомарная операция — это операция, которую невозможно наблюдать в промежуточном состоянии, она либо выполнена либо нет. Атомарные операции могут состоять из нескольких операций. Если говорить про тип std::atomic, то он предоставляет ряд примитивных операций: load , store , fetch_add , compare_exchange_* и другие. Последние две операции — это read-modify-write операции, атомарность которых обеспечивается специальными инструкциями процессора.
Рассмотрим простой пример read-modify-write операции, а именно прибавление к числу единицы. Пример 0, link:
В случае с обычной переменной v1 типа int имеем три отдельных операций: read-modify-write. Нет гарантий, что другое ядро процессора не выполняет другой операции над v1 . Операция над v2 в машинных кодах представлена как одна операция с lock сигналом на уровне процессора, гарантирующим, что к кэш линии, в которой лежит v2 , эксклюзивно имеет доступ только ядро, выполняющее эту инструкцию.
Про ограничения на порядок выполнения операций. Когда мы пишем код программы, то предполагаем, что операторы языка будут выполнены последовательно. В реальности же компилятор и в особенности процессор могут переупорядочить команды программы с целью оптимизации. Они это делают с учетом ограничений на порядок записи и чтения в локацию памяти. Например, чтение из локации памяти должно происходить после записи, эти операции нельзя переупорядочить. Применение атомарных операций может накладывать дополнительные ограничения на возможные переупорядочивания операций с памятью.
Про синхронизацию данных между потоками. Если мы хотим изменить данные в одном потоке и сделать так, чтобы эти изменения были видны в другом потоке, то нам необходимы примитивы многопоточного программирования. Фундаментальным таким примитивом являются атомики, остальные, например мьютексы, либо реализованы на основе атомиков, либо повторяют семантику атомиков. Все попытки записывать и читать одни и те же данные из разных потоков без примитивов синхронизации могут приводить к UB.
Случаи, когда синхронизация памяти не требуется:
Если все потоки, работающие с одним участком памяти, используют ее только на чтение
Если разные потоки используют эксклюзивно разные участки памяти
Далее будет рассмотрены более сложные случаи, когда требуется чтение и запись одного участка памяти из разных потоков. Язык C++ предоставляет три способа синхронизации памяти. По мере возрастания строгости: relaxed , release/acquire и sequential consistency . Рассмотрим их.
Полный порядок
Флаг синхронизации памяти "единая последовательность" (sequential consistency, seq_cst ) дает самые строгие. Его свойства:
порядок модификаций разных атомарных переменных в потоке thread1 сохранится в потоке thread2
все потоки будут видеть один и тот же порядок модификации всех атомарных переменных. Сами модификации могут происходить в разных потоках
все модификации памяти (не только модификации над атомиками) в потоке thread1 , выполняющей store на атомарной переменной, будут видны после выполнения load этой же переменной в потоке thread2
Таким образом можно представить seq_cst операции, как барьеры памяти, в которых состояние памяти синхронизируется между всеми потоками программы.
Этот флаг синхронизации памяти в C++ используется по умолчанию, т.к. с ним меньше всего проблем с точки зрения корректности выполнения программы. Но seq_cst является дорогой операцией для процессоров, в которых вычислительные ядра слабо связаны между собой в плане механизмов обеспечения консистентности памяти. Например, для x86-64 seq_cst дешевле, чем для ARM архитектур.
Продемонстрируем второе свойство. Пример 4, из книги [1], link:
После того, как все четыре потока отработают, значение переменной z будет равно 1 или 2 , потому что потоки thread_read_x_then_y и thread_read_y_then_x "увидят" изменения x и y в одном и том же порядке. От запуска к запуску это могут быть: сначала x = true , потом y = true , или сначала y = true , потом x = true .
Модификатор seq_cst всегда может быть использован вместо relaxed и acquire/release , еще и поэтому он является модификатором по умолчанию. Удобно использовать seq_cst для отладки проблем, связанных с гонкой данных в многопоточной программе: добиваемся корректной работы программы и далее заменяем seq_cst на менее строгие флаги синхронизации памяти. Примеры 1 и 2 также будут корректно работать, если заменить relaxed на seq_cst , а пример 3 начнет работать корректно после такой замены.
Заключение
Сложно представить современную C++ программу, которая была бы однопоточной. Опасно писать многопоточные программы, не имея представления о правилах синхронизации памяти. Я считаю, что нужно знать, как работают атомики в C++. Чтобы не совершать ошибок типа volatile bool , чтобы понимать, какие изменения в каких потоках будут видны после использования того или иного многопоточного примитива, чтобы использовать read-modify-write атомарные операции вместо мьютекса, там где это возможно. Данная статья помогла мне систематизировать материал, который я находил в разных источниках и освежить знания в памяти. Надеюсь, она поможет и вам!
Модели памяти задаются директивой .MODEL
где модель — одно из следующих слов:
TINY — код, данные и стек размещаются в одном и том же сегменте размером до 64 Кб. Эта модель памяти чаще всего используется при написании на ассемблере небольших программ;
SMALL — код размещается в одном сегменте, а данные и стек — в другом (для их описания могут применяться разные сегменты, но объединенные в одну группу). Эту модель памяти также удобно использовать для создания программ на ассемблере;
COMPACT — код размещается в одном сегменте, а для хранения данных могут использоваться несколько сегментов, так что для обращения к данным требуется указывать сегмент и смещение (данные дальнего типа);
MEDIUM — код размещается в нескольких сегментах, а все данные — в одном, поэтому для доступа к данным используется только смещение, а вызовы подпрограмм применяют команды дальнего вызова процедуры;
LARGE и HUGE — и код, и данные могут занимать несколько сегментов;
FLAT — то же, что и TINY, но используются 32-битные сегменты, так что максимальный размер сегмента, содержащего и данные, и код, и стек, — 4 Мб.
Язык — необязательный операнд, принимающий значения C, PASCAL, BASIC, FORTRAN, SYSCALL и STDCALL. Если он указан, подразумевается, что процедуры рассчитаны на вызов из программ на соответствующем языке высокого уровня, следовательно, если указан язык C, все имена ассемблерных процедур, объявленных как PUBLIC, будут изменены так, чтобы начинаться с символа подчеркивания, как это принято в C.
Модификатор — необязательный операнд, принимающий значения NEARSTACK (по умолчанию) или FARSTACK. Во втором случае сегмент стека не будет объединяться в одну группу с сегментами данных.
После того как модель памяти установлена, вступают в силу упрощенные директивы определения сегментов, объединяющие действия директив SEGMENT и ASSUME. Кроме того, сегменты, объявленные упрощенными директивами, не требуется закрывать директивой ENDS — они закрываются автоматически, как только ассемблер обнаруживает новую директиву определения сегмента или конец программы.
Директива .CODE описывает основной сегмент кода
для моделей TINY, SMALL и COMPACT и
для моделей MEDIUM, HUGE и LARGE (name — имя модуля, в котором описан данный сегмент). В этих моделях директива .CODE также допускает необязательный операнд — имя определяемого сегмента, но все сегменты кода, описанные так в одном и том же модуле, объединяются в один сегмент с именем NAME_TEXT.
Директива .STACK описывает сегмент стека и эквивалентна директиве
Необязательный параметр указывает размер стека. По умолчанию он равен 1 Кб.
Описывает обычный сегмент данных и соответствует директиве
Описывает сегмент неинициализированных данных:
Этот сегмент обычно не включается в программу, а располагается за концом памяти, так что все описанные в нем переменные на момент загрузки программы имеют неопределенные значения.
Описывает сегмент неизменяемых данных:
В некоторых операционных системах этот сегмент будет загружен так, что попытка записи в него может привести к ошибке.
Сегмент дальних данных:
Доступ к данным, описанным в этом сегменте, потребует загрузки сегментного регистра. Если не указан операнд, в качестве имени сегмента используется FAR_DATA.
Сегмент дальних неинициализированных данных:
Как и в случае с FARDATA, доступ к данным из этого сегмента потребует загрузки сегментного регистра. Если имя сегмента не указано, используется FAR_BSS.
Во всех моделях памяти сегменты, представленные директивами .DATA, .DATA?, .CONST, .FARDATA и .FARDATA?, а также сегмент, описанный директивой .STACK, если не был указан модификатор FARSTACK, и сегмент .CODE в модели TINY автоматически объединяются в группу с именем FLAT — для модели памяти FLAT или DGROUP — для всех остальных моделей. При этом сегментный регистр DS (и SS, если не было FARSTACK, и CS в модели TINY) настраивается на всю эту группу, как если бы была выполнена команда ASSUME.
Возможно, вы создали поток данных, но у него возникли трудности с получением данных (с помощью Power Query в Power BI Desktop или из других потоков данных). В этой статье описываются некоторые из наиболее распространенных проблем с получением данных из потока данных.
Неделимый, но расслабленный
Самый простой для понимания флаг синхронизации памяти — relaxed . Он гарантирует только свойство атомарности операций, при этом не может участвовать в процессе синхронизации данных между потоками. Свойства:
модификация переменной "появится" в другом потоке не сразу
поток thread2 "увидит" значения одной и той же переменной в том же порядке, в котором происходили её модификации в потоке thread1
порядок модификаций разных переменных в потоке thread1 не сохранится в потоке thread2
Можно использовать relaxed модификатор в качестве счетчика. Пример 1, link:
Использование в качестве флага остановки. Пример 2, link:
В данном примере не важен порядок в котором thread1 увидит изменения из потока, вызывающего stop_thread1 . Также не важно то, чтобы thread1 мгновенно (синхронно) увидел выставление флага stopped в true .
Пример неверного использования relaxed в качестве флага готовности данных. Пример 3, link:
Тут нет гарантий, что поток thread2 увидит изменения data ранее, чем изменение флага ready , т.к. синхронизацию памяти флаг relaxed не обеспечивает.
Не удается установить подключение DirectQuery к потоку данных
Если вы планируете использовать поток данных в качестве источника DirectQuery, может потребоваться сначала включить его.
Причина.
Расширенные параметры вычислительной подсистемы отключены.
Решение.
Включите расширенный модуль вычислений, а затем вы сможете подключиться к потоку данных с помощью DirectQuery.
Поток данных Microsoft Power Platform отсутствует в списке
Иногда у вас есть созданный и обновленный поток данных Microsoft Power Platform, но вы по-прежнему не можете получить к нему доступ с помощью команды "Получить данные ". Это может быть вызвано тем, что у учетной записи, которая пытается получить доступ к потоку данных, нет доступа. Однако если у учетной записи есть доступ к потоку данных, другой причиной может быть тип потока данных, к который вы обращаетесь.
Причина.
В операции получения данных из потока данных можно использовать только аналитические потоки данных.
Решение.
Если вы создали поток данных, в котором хранятся данные в Dataverse, то есть стандартный поток данных, его невозможно увидеть с помощью операции получения данных из потока данных. Однако для доступа к ним можно использовать получение данных из Dataverse . Кроме того, можно создать аналитический поток данных , а затем получить к нему доступ с помощью получения данных из потока данных.
Синхронизация пары. Acquire/Release
Флаг синхронизации памяти acquire/release является более тонким способом синхронизировать данные между парой потоков. Два ключевых слова: memory_order_acquire и memory_order_release работают только в паре над одним атомарным объектом. Рассмотрим их свойства:
модификация атомарной переменной с release будет видна видна в другом потоке, выполняющем чтение этой же атомарной переменной с acquire
все модификации памяти в потоке thread1 , выполняющей запись атомарной переменной с release , будут видны после выполнения чтения той же переменной с acquire в потоке thread2
процессор и компилятор не могут перенести операции записи в память раньше release операции в потоке thread1 , и нельзя перемещать выше операции чтения из памяти позже acquire операции в потоке thread2
Используя release , мы даем инструкцию, что данные в этом потоке готовы для чтения из другого потока. Используя acquire , мы даем инструкцию "подгрузить" все данные, которые подготовил для нас первый поток. Но если мы делаем release и acquire на разных атомарных переменных, то получим UB вместо синхронизации памяти.
Рассмотрим реализацию простейшего мьютекса, который ожидает в цикле сброса флага для того, чтобы получить lock . Такой мьютекс называют spinlock . Это не самый эффективный способ реализации мьютекса, но он обладает всеми нужными свойствами, на которые я хочу обратить внимание. Пример 5, link:
Функция lock() непрерывно пробует сменить значение с false на true с модификатором синхронизации памяти acquire . Разница между compare_exchage_weak и strong незначительна, про нее можно почитать на cppreference. Функция unlock() выставляет значение в false с синхронизацией release . Обратите внимание, что мьютекс не только обеспечивает эксклюзивным доступ к блоку кода, который он защищает. Он также делает доступным те изменения памяти, которые были сделаны до вызова unlock() в коде, который будет работать после вызова lock() . Это важное свойство. Иногда может сложиться ошибочное мнение, что мьютекс в конкретном месте не нужен.
Рассмотрим такой пример, называемый Double Checked Locking Anti-Pattern из [2]. Пример 6, link:
Идея проста: хотим единожды в рантайме инициализировать объект Singleton . Это нужно сделать потокобезопасно, поэтому имеем мьютекс и флаг инициализации. Т.к. создается объект единожды, а используется singleton указатель в read-only режиме всю оставшуюся жизнь программы, то кажется разумным добавить предварительную проверку if (initialized) return . Данный код будет корректно работать на архитектурах процессора с более строгими гарантиями консистентности памяти, например в x86-64. Но данный код неверный с точки зрения стандарта C++. Давайте рассмотрим такой сценарий использования:
Рассмотрим следующую последовательность действий во времени:
1. сначала отрабатывает thread1 -> выполняет инициализацию под мьютексом:
lock мьютекса ( acquire )
unlock мьютекса ( release )
2. далее в игру вступает thread2 :
if(initalized) возвращает true (память, где содержится initialized могла быть неявно синхронизирована между ядрами процессора)
singleton->do_job() приводит к segmentation fault (указатель singleton не обязан был быть синхронизирован с потоком thread1 )
Этот случай интересен тем, что наглядно показывает роль мьютекса не только как примитива синхронизации потока выполнения, но и синхронизации памяти.
Ошибка: эта таблица пуста
Причина.
Данные не были загружены в таблицу.
Решение.
В классических средствах, таких как Power Query в Excel и Power Query в Power BI Desktop, загрузка данных в таблицы происходит автоматически (если вы не отключите их). Это поведение немного отличается в Power Query в потоках данных. В сущностях потока данных данные не будут загружаться, если вы не обновите данные.
Необходимо настроить запланированное обновление для потока данных или ( если требуется только одно обновление) используйте параметр обновления вручную.
После обновления потока данных данные в сущностях будут отображаться в окне навигатора других инструментов и служб.
Читайте также: