К счастью, 32-разрядные программы более устойчивы к определенным проблемам, включая проблемы с потоками, чем 16-разрядные программы. Например, предположим, что один поток выполняет простое действие:
lCount++ ;
где ICount – 32-разрядная глобальная переменная типа длинное целое, используемая другими потоками. В 16-разрядной программе, в которой такой оператор языка С транслируется в две инструкции машинного кода (сначала инкрементируется младшие 16 разрядов, а затем добавляется перенос в старшие 16 разрядов). Допустим, что операционная система прервала поток между этими двумя инструкциями машинного кода. Если переменная ICount имела значение $0000FFFF, то после выполнения первой инструкции машинного кода ICount будет иметь нулевое значение. Если в этот момент произойдет прерывание потока, то другой поток получит нулевое значение переменной ICount. Только после окончания этого потока значение ICount будет увеличено на единицу до своего истинного значения $00010000.
Такого рода ошибка может быть никогда не выявлена, поскольку довольно редко приводит к проблемам во время выполнения. Для 16-разрядных программ наилучший путь предотвратить такую ошибку – это поместить данное выражение в критический раздел, в рамках которого поток не может быть прерван. В 32-разрядной программе, однако, приведенное выражение является абсолютно корректным, поскольку оно компилируется в одну инструкцию машинного кода.
Преимущества Windows
Операционные системы Windows 95 и Windows NT не имеют последовательной очереди сообщений. Такое решение кажется очень хорошим: если программа выполняет длительную обработку сообщения, то курсор мыши принимает форму песочных часов при расположении над окном этой программы, и изменяется на обычную стрелку, если он располагается над окном другой программы. Простым щелчком кнопкой мыши можно перевести другое окно на передний план.
Однако, пользователь по-прежнему не может работать с приложением, выполняющим длительную операцию, поскольку выполнение длительной операции предотвращает получение сообщений программой. А это нежелательно. Программа должна быть всегда открыта для сообщений, а это требует использования вторичных потоков.
В Windows 95 и Windows NT не существует различия между потоками, имеющими очередь сообщений, и потоками без очереди сообщений. При создании каждый поток получает свою собственную очередь сообщений. Это снижает число ограничений, существующих для потоков в РМ-программе. (Однако, в большинстве случаев все еще обработка ввода и сообщений осуществляется в одном потоке, а протяженные во времени задачи передаются другим потокам, которые не создают окон.) Такая схема организации приложения, как мы увидим, почти всегда является наиболее разумной.
В Windows 95 и Windows NT есть функция, которая позволяет одному потоку уничтожить другой поток, принадлежащий тому же процессу. Как вы обнаружите, когда начнете писать многопоточные приложения под Windows, иногда это очень удобно. Ранние версии операционной системы OS/2 не содержали функции для уничтожения потоков.
Windows 95 и Windows NT поддерживают так называемую локальную память потока (thread local storage, TLS). Для того чтобы понять, что это такое, вспомним о том, что статические переменные, как глобальные так и локальные по отношению к функциям, разделяются между потоками, поскольку они расположены в зоне памяти данных процесса. Автоматические переменные (которые являются всегда локальными по отношению к функции) – уникальны для каждого потока, т. к. они располагаются в стеке, а каждый поток имеет свой стек.
Иногда бывает удобно использовать для двух и более потоков одну и ту же функцию, а статические данные использовать уникальные для каждого потока. Это и есть пример использования локальной памяти потока. Существует несколько вызовов функций Windows для работы с локальной памятью потока. Фирма Microsoft ввела расширение в компилятор С, которое позволяет использовать локальную память потока более прозрачным для программиста образом.
Новая усовершенствованная многопоточная программа
Иногда имеет место тенденция использовать в программе каждую возможность, предлагаемую операционной системой. Нет смысла использовать множество потоков в программе, которая в этом не нуждается. Если программа выводит на экран курсор в виде песочных часов на достаточно долгий период времени, или, если она использует функцию PeekMessage для того, чтобы избежать появления курсора в виде песочных часов, то тогда идея реструктуризации программы в многопоточную, вероятно, может оказаться хорошей. В противном случае, вы только усложните себе работу и, возможно, внесете в программу новые ошибки.
Есть даже некоторые ситуации, когда появление курсора мыши в виде песочных часов, может быть совершенно подходящим. Выше уже упоминалось "правило 1/10 секунды". Так вот, загрузка большого файла в память может потребовать больше времени, чем 1/10 секунды. Значит ли это, что функции загрузки файла должны были быть реализованы с использованием разделения на потоки? Совсем необязательно. Когда пользователь дает программе команду открыть файл, то он или она обычно хочет, чтобы операционная система выполнила ее немедленно. Выделение процесса загрузки файла в отдельный поток просто приведет к усложнению программы. Не стоит это делать даже ради того, чтобы похвастаться перед друзьями, что вы пишите многопоточные приложения.
О использовании функции Sleep
Выше было показано, как лучше организовать архитектуру программы, использующей многопоточность, а именно, чтобы первичный поток создавал все окна в программе, содержал все оконные процедуры этих окон и обрабатывал все сообщения. Вторичные потоки выполняют фоновые задачи или задачи, протяженные во времени.
Однако, предположим, что требуется реализовать анимацию во вторичном потоке. Обычно анимация в Windows осуществляется с использованием сообщения WM_TIMER. Но если вторичный поток не создает окно, то он не может получить это сообщение. А без задания определенного темпа анимация могла бы осуществляться слишком быстро.
Решение состоит в использовании функции Sleep. Поток вызывает функцию Sleep для того, чтобы добровольно отложить свое выполнение. Единственный параметр этой функции – время, задаваемое в миллисекундах. Функция Sleep не осуществляет возврата до тех пор, пока не истечет указанное время. В течение него выполнение потока приостанавливается и выделения для него процессорного времени не происходит (хотя очевидно, что для потока все-таки требуется какое-то незначительное время, за которое система должна определить, пора возобновлять выполнение потока или нет).
Если параметр функции Sleep задан равным нулю, то поток будет лишен остатка выделенного ему кванта процессорного времени.
Когда поток вызывает функцию Sleep, задержка на заданное время относится к этому потоку. Система продолжает выполнять другие потоки этого и других процессов
В однозадачной операционной системе обычные программы не нуждаются в "светофорах" для координации их действий. Они выполняются так, как будто они являются хозяевами дороги, по которой они следуют. Не существует ничего, что могло бы вмешаться в то, что они делают.
Даже в многозадачной операционной системе большинство программ выполняются независимо друг от друга. Но некоторые проблемы все же могут возникнуть. Например, двум программам может понадобиться читать и писать в один файл в одно и то же время. Для таких случаев операционная система поддерживает механизм разделения файлов (shared files) и блокирования отдельных фрагментов файла (record locking).
Однако, в операционной системе, поддерживающей многопоточность, такое решение может внести путаницу и создать потенциальную опасность. Разделение
данных между двумя и более потоками является общим случаем. Например, один поток может обновлять одну или более переменных, а другой может использовать эти переменные. Иногда в этой ситуации может возникнуть проблема, а иногда – нет. (Помните, что операционная система может переключать управление потоками только между инструкциями машинного кода. Если простое целое число разделяется между двумя потоками, то изменение этой переменной обычно осуществляется одной инструкцией машинного кода, и потенциальные проблемы сводятся к минимуму.)
Однако, предположим, что потоки разделяют несколько переменных или структуру данных. Часто эти сложные переменные или поля структур данных должны быть согласованными между собой. Операционная система может прерывать поток в середине процесса обновления этих переменных. В этом случае поток, который затем использует эти переменные, будет иметь дело с несогласованными данными.
В результате бы возникла коллизия, и нетрудно представить себе, как такого рода ошибка может привести к краху программы. В этой ситуации нам необходимо нечто похожее на светофор, который мог бы синхронизировать и координировать работу потоков. Таким средством и является критический раздел. Критический раздел – это блок кода, при выполнении которого поток не может быть прерван.
Имеется четыре функции для работы с критическими разделами. Чтобы их использовать, вам необходимо определить объект типа критический раздел, который является глобальной переменной типа CRITICAL_SECTION. Например,
CRITICAL_SECTION CS ;
Тип данных CRITICAL_SECTION является структурой, но ее поля используются только внутри Windows. Объект типа критический раздел сначала должен быть инициализирован одним из потоков программы с помощью функции: