PythonProxyNotificationHandler::OnEventGenericHook acquires the Python GIL before executing (#3904)

- Protected python execution of OnEventGenericHook by trying to lock the GIL and show a descriptive error when it was not possible to lock.
- Improved mechanism to lock python mutex and GIL in PhytonSystemComponent. Acquiring/releasing GIL once per thread.
- Added unit test to verify errors are fired when trying to execute OnEventGenericHook from another thread (as it should not able to acquire the GIL).
- Improved python threading tests to actually using python buses.

Signed-off-by: moraaar moraaar@amazon.com
This commit is contained in:
moraaar
2021-09-07 15:35:04 +01:00
committed by GitHub
parent 33299399af
commit f551e69b2b
16 changed files with 259 additions and 67 deletions
@@ -20,6 +20,7 @@
#include <AzCore/RTTI/AttributeReader.h>
#include <AzCore/std/optional.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
namespace EditorPythonBindings
{
@@ -261,19 +262,39 @@ namespace EditorPythonBindings
static void OnEventGenericHook(void* userData, const char* eventName, int eventIndex, AZ::BehaviorValueParameter* result, int numParameters, AZ::BehaviorValueParameter* parameters)
{
reinterpret_cast<PythonProxyNotificationHandler*>(userData)->OnEventGenericHook(eventName, eventIndex, result, numParameters, parameters);
}
void OnEventGenericHook(const char* eventName, [[maybe_unused]] int eventIndex, AZ::BehaviorValueParameter* result, int numParameters, AZ::BehaviorValueParameter* parameters)
{
// find the callback for the event
const auto& callbackEntry = m_callbackMap.find(eventName);
if (callbackEntry == m_callbackMap.end())
auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
if (!editorPythonEventsInterface)
{
return;
}
pybind11::function callback = callbackEntry->second;
// find the callback for the event
auto* handler = reinterpret_cast<PythonProxyNotificationHandler*>(userData);
const auto& callbackEntry = handler->m_callbackMap.find(eventName);
if (callbackEntry == handler->m_callbackMap.end())
{
return;
}
// This function can reach from multiple threads, which means OnEventGenericHook
// will require to acquire the Python GIL, make sure it tries to lock it using TryExecuteWithLock.
[[maybe_unused]] const bool executed = editorPythonEventsInterface->TryExecuteWithLock(
[handler, eventName, callback = callbackEntry->second, eventIndex, result, numParameters, parameters]()
{
handler->OnEventGenericHook(eventName, callback, eventIndex, result, numParameters, parameters);
});
AZ_Error("python", executed,
"Ebus(%s) event(%s) could not be executed because it could not acquire the Python GIL. "
"This occurs when there is already another thread executing python, which has the GIL locked, "
"making it not possible for this thread to callback python at the same time. "
"This is a limitation of python interpreter. Python scripts executions and event callbacks "
"from EBuses need be designed to avoid this scenario.",
handler->m_ebus->m_name.c_str(), eventName);
}
void OnEventGenericHook(const char* eventName, pybind11::function callback, [[maybe_unused]] int eventIndex, AZ::BehaviorValueParameter* result, int numParameters, AZ::BehaviorValueParameter* parameters)
{
// build the parameters to send to callback
Convert::StackVariableAllocator stackVariableAllocator;
pybind11::tuple pythonParamters(numParameters);
@@ -258,6 +258,85 @@ namespace EditorPythonBindings
void Finalize() override {}
};
// Manages the acquisition and release of the Python GIL (Global Interpreter Lock).
// Used by PythonSystemComponent to lock the GIL when executing python.
class PythonSystemComponent::PythonGILScopedLock final
{
public:
PythonGILScopedLock(AZStd::recursive_mutex& lock, int& lockRecursiveCounter, bool tryLock = false);
~PythonGILScopedLock();
bool IsLocked() const;
protected:
void Lock(bool tryLock);
void Unlock();
AZStd::recursive_mutex& m_lock;
int& m_lockRecursiveCounter;
bool m_locked = false;
AZStd::unique_ptr<pybind11::gil_scoped_release> m_releaseGIL;
AZStd::unique_ptr<pybind11::gil_scoped_acquire> m_acquireGIL;
};
PythonSystemComponent::PythonGILScopedLock::PythonGILScopedLock(AZStd::recursive_mutex& lock, int& lockRecursiveCounter, bool tryLock)
: m_lock(lock)
, m_lockRecursiveCounter(lockRecursiveCounter)
{
Lock(tryLock);
}
PythonSystemComponent::PythonGILScopedLock::~PythonGILScopedLock()
{
Unlock();
}
bool PythonSystemComponent::PythonGILScopedLock::IsLocked() const
{
return m_locked;
}
void PythonSystemComponent::PythonGILScopedLock::Lock(bool tryLock)
{
if (tryLock)
{
if (!m_lock.try_lock())
{
return;
}
}
else
{
m_lock.lock();
}
m_locked = true;
m_lockRecursiveCounter++;
// Only Acquire the GIL when there is no recursion. If there is
// recursion that means it's the same thread (because the mutex was able
// to be locked) and therefore it's already got the GIL acquired.
if (m_lockRecursiveCounter == 1)
{
m_releaseGIL = AZStd::make_unique<pybind11::gil_scoped_release>();
m_acquireGIL = AZStd::make_unique<pybind11::gil_scoped_acquire>();
}
}
void PythonSystemComponent::PythonGILScopedLock::Unlock()
{
if (!m_locked)
{
return;
}
m_acquireGIL.reset();
m_releaseGIL.reset();
m_lockRecursiveCounter--;
m_locked = false;
m_lock.unlock();
}
void PythonSystemComponent::Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
@@ -296,9 +375,9 @@ namespace EditorPythonBindings
void PythonSystemComponent::Deactivate()
{
StopPython(true);
AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusDisconnect();
AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Unregister(this);
StopPython(true);
}
bool PythonSystemComponent::StartPython([[maybe_unused]] bool silenceWarnings)
@@ -354,7 +433,6 @@ namespace EditorPythonBindings
bool result = false;
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPreFinalize);
AzToolsFramework::EditorPythonRunnerRequestBus::Handler::BusDisconnect();
result = StopPythonInterpreter();
EditorPythonBindingsNotificationBus::Broadcast(&EditorPythonBindingsNotificationBus::Events::OnPostFinalize);
@@ -374,12 +452,21 @@ namespace EditorPythonBindings
void PythonSystemComponent::ExecuteWithLock(AZStd::function<void()> executionCallback)
{
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_release release;
pybind11::gil_scoped_acquire acquire;
PythonGILScopedLock lock(m_lock, m_lockRecursiveCounter);
executionCallback();
}
bool PythonSystemComponent::TryExecuteWithLock(AZStd::function<void()> executionCallback)
{
PythonGILScopedLock lock(m_lock, m_lockRecursiveCounter, true /*tryLock*/);
if (lock.IsLocked())
{
executionCallback();
return true;
}
return false;
}
void PythonSystemComponent::DiscoverPythonPaths(PythonPathStack& pythonPathStack)
{
// the order of the Python paths is the order the Python bootstrap scripts will execute
@@ -548,8 +635,7 @@ namespace EditorPythonBindings
RedirectOutput::Intialize(PyImport_ImportModule("azlmbr_redirect"));
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
PythonGILScopedLock lock(m_lock, m_lockRecursiveCounter);
if (EditorPythonBindings::PythonSymbolEventBus::GetTotalNumOfEventHandlers() == 0)
{
@@ -622,8 +708,7 @@ namespace EditorPythonBindings
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByString, script);
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
PythonGILScopedLock lock(m_lock, m_lockRecursiveCounter);
// Acquire scope for __main__ for executing our script
pybind11::object scope = pybind11::module::import("__main__").attr("__dict__");
@@ -741,8 +826,7 @@ namespace EditorPythonBindings
try
{
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
PythonGILScopedLock lock(m_lock, m_lockRecursiveCounter);
// Create standard "argc" / "argv" command-line parameters to pass in to the Python script via sys.argv.
// argc = number of parameters. This will always be at least 1, since the first parameter is the script name.
@@ -48,6 +48,7 @@ namespace EditorPythonBindings
bool IsPythonActive() override;
void WaitForInitialization() override;
void ExecuteWithLock(AZStd::function<void()> executionCallback) override;
bool TryExecuteWithLock(AZStd::function<void()> executionCallback) override;
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
@@ -60,11 +61,13 @@ namespace EditorPythonBindings
private:
class SymbolLogHelper;
class PythonGILScopedLock;
// handle multiple Python initializers and threads
AZStd::atomic_int m_initalizeWaiterCount {0};
AZStd::semaphore m_initalizeWaiter;
AZStd::recursive_mutex m_lock;
int m_lockRecursiveCounter = 0;
AZStd::shared_ptr<SymbolLogHelper> m_symbolLogHelper;
enum class Result