Зачем существует GLFWProvider.CheckForMainThread?
Перевод поста NogginBops "Why does GLFWProvider.CheckForMainThread exist?"
Пост не является авторским и/или официальным
Ссылка на оригинал: Why does GLFWProvider.CheckForMainThread exist?
Автор оригинала: NogginBops
В этой статье мы рассмотрим функцию GLFWProvider.CheckForMainThread, выясним, зачем она нужна и для чего её можно использовать.
OpenTK 4 использует GLFW для создания и управления окнами. Для корректного использования GLFW необходимо вызвать метод glfwInit перед использованием большинства функций GLFW. Для этого OpenTK использует GLFWProvider, который мы рассматривали в двух предыдущих статьях. Этот класс отвечает за инициализацию GLFW с соответствующим обработчиком ошибок перед инициализацией (на случай возникновения ошибок во время инициализации) и фактический вызов glfwInit. OpenTK вызывает glfwInit, вызывая GLFWProvider.EnsureInitialized() перед использованием любых функций GLFW, в частности, в конструкторе NativeWindow.
Если создание первого окна выполняется в отдельном потоке, вы получите исключение с сообщением "GLFW can only be called from the main thread!". Это связано с тем, что glfwInit можно вызвать только из основного потока, а GLFWProvider выполняет проверку при первой инициализации GLFW, чтобы убедиться, что она действительно выполняется в основном потоке.
Большинство функций GLFW необходимо вызывать в основном потоке, но почему? Есть ли в основном потоке что-то особенное? Ответ, как это обычно и бывает, — «зависит от обстоятельств». macOS делает основной поток особенным и предполагает, что все вызовы функций окон и пользовательского интерфейса будут выполняться в основном потоке. Это означает, что попытка создать окно в macOS в каком-либо другом потоке, кроме основного, скорее всего, приведет к сбою или другим странным проблемам.
В Windows ситуация несколько иная, поскольку у каждого потока есть своя очередь событий, и если окно создается в определенном потоке, сообщения, получаемые этим окном, будут отправляться только в очередь событий этого потока. Таким образом, создание окон в разных потоках нарушит способность GLFW обрабатывать все события окна одним вызовом glfwPollEvents.
В Linux ситуация иная. X11 использует единую очередь событий для всего процесса и может обрабатывать большинство функций, вызываемых из разных потоков, поскольку весь API по своей природе асинхронен. Wayland очень явно использует очереди событий и может перенаправлять события из одного объекта в любую созданную пользователем очередь событий, поэтому то, что происходит при создании окон в разных потоках, зависит от внутренних механизмов GLFW.
Как мы видим, использование только одного потока при вызове GLFW, а следовательно, и OpenTK, устраняет все эти платформенно-зависимые различия. Поэтому для обеспечения кроссплатформенной совместимости GLFW требует, чтобы большинство функций1 вызывались из основного потока.
Следующий вопрос, на который я хочу ответить, — как GLFWProvider определяет, какой поток является основным. Кажется, это легко выяснить, но на самом деле это оказывается удивительно сложно, поскольку в C# нет API для получения информации об “основном” потоке. Это связано с тем, что существует множество способов запуска C#, при которых может отсутствовать основной поток (запуск C# в COM-компоненте) или среда выполнения не имеет хорошего способа определить основной поток (вызов C# из неуправляемого кода). Итак, как OpenTK проверяет основной поток? В порядке “максимальных усилий”.
1
2
3
4
5
6
7
8
9
10
11
12
MethodInfo correctEntryMethod = Assembly.GetEntryAssembly()?.EntryPoint;
StackTrace trace = new StackTrace();
StackFrame[] frames = trace.GetFrames();
for (int i = frames.Length - 1; i >= 0; i--)
{
MethodBase method = frames[i].GetMethod();
if (correctEntryMethod == method)
{
_mainThread = Thread.CurrentThread;
break;
}
}
Основная идея заключается в получении MethodInfo для метода Main, а затем в исследовании стека вызовов текущего потока, чтобы определить, находится ли поток Main в стеке вызовов текущего потока. Если он не является частью стека вызовов, мы предполагаем, что это не основной поток, а это значит, что мы не можем инициализировать GLFW в этом потоке, поэтому мы выбрасываем исключение.
Это плавно подводит нас к ответу на вопрос, с которого началась эта статья. Зачем существует GLFWProvider.CheckForMainThread? Он существует потому, что могут быть сценарии, когда OpenTK не может корректно определить основной поток (например, вызов C# из неуправляемого кода). Без возможности установить GLFWProvider.CheckForMainThread = false было бы невозможно инициализировать OpenTK в таких сценариях, но с этой функцией мы позволяем вам обойти эту проверку на ваш страх и риск.