Lua yield allows waiting for threads in Lua?
#1
Just posting this here for future reference, I want to test some things with this.

http://stackoverflow.com/questions/72064...o-routines

As it is right now a web plugin (or any other plugins) can deadlock the server because they receive callbacks from multiple threads. When receiving a callback from one thread Lua has access to all MCServer's functions, but some code areas may be locked by another thread which is currently waiting for the Lua state to unlock.

I want to create a multi-threaded test case that uses lua_newthread, yielding and returning.

lua_newthread creates a "Lua thread" that shares global plugin data but has its own call stack. This still does not allow multiple threads to run a plugin at once, because of the shared global data. However it does allow a plugin to yield and stop execution in one thread while another thread can call whatever callback they want. When the callback is done executing (or any other requirements are met) the old thread can kick back in and resume execution where it left off.

An example of how I see this working:

function BlockChangedCallback()
	--blabla do something heavy that takes a long time making sure stuff is blocking and possibly deadlocking
end

function WebCallback()
	local BlockID = GetBlockIDYield(x, y, z)
end

List< Messages > m_MessageList; // In the case of Lua this should probably be a single message instead of a list, or at least linked with the Lua thread it belongs to

int LUA_GetBlockIDYield(int x, int y, int z)
{
	m_MessageList.push_back( new Runnable()
	{
		void Run()
		{
			cWorld::GetBlockID(x, y, z);
		}
	} );
    return lua_yield(L,0); // I _think_ this immediately returns LUA_YIELD in main()
}


int main() {
	lua_register(L, "GetBlockIDYield", LUA_GetBlockIDYield);

    lua_State* luaThread = lua_newthread(luaState);
    
	lua_getglobal(luaThread, "WebCallback");
	lua_resume(luaThread, 0); // Should execute WebCallback??

    while (running) {
        int status;
        status = lua_resume(cL,0);
        if (status == LUA_YIELD) {
			// Lua has returned, we should unlock the CriticalSection so other threads can run BlockChangedCallback()
			// Handle all messages here??
			foreach( message in m_MessageList )
			{
				message->Run();
			}
			// Somehow fill some variables in the Lua script with results from the messages
			// I think it should be possible to push some objects on the stack before calling lua_resume which results in returning the results directly into Lua
        } else {
            running=false; // It did not yield which means execution was completed (error or not is not relevant)
        }
    }
    return 0;
}

I haven't really thought this out, and the example is horribly incomplete and wrong.. but I'm tired I'll look at it later again.

Maybe you guys can look at this with a fresh non-fried brain and let me know whether this sounds reasonable.
Reply
Thanks given by:
#2
Great news! It works!
I still have to test if global vars are shared like they should. If this works all plugins should be able to be deadlock safeBig Grin
Reply
Thanks given by:
#3
Global vars work, even the "local" vars in a global scope (outside of a function)
Reply
Thanks given by:
#4
This is the test case I cooked up so far.
Two threads are trying to access a plugin simultaneously and each time the plugin calls a function in C++ the "lua thread" yields and does its thing which allows the other thread to execute the plugin.
Each time a "lua thread" yields, all locks for the plugin are released making it impossible to create a deadlock (even though a deadlock cannot even exist in this setting, even if I didn't use yielding)

I also figured out some cool template magic which makes it really easy to wrap plugins to support yielding.

main.cpp
cCriticalSection LuaStateLock;
cCriticalSection LuaMessageLock;
cCriticalSection LogLock;


extern int tolua_StringSplit(lua_State* tolua_S);
static int tolua_GetNumber(lua_State* a_pLuaState)
{
	tolua_pushnumber( a_pLuaState, 1.234f );
	return 1;
}

class cBaseMessage
{
public:
	virtual int Run() = 0;
	lua_State* pLuaState;
};

typedef std::map<lua_State*, cBaseMessage*> LuaMessageMap;
LuaMessageMap m_LuaMessages;

template< int (*Func1)(lua_State *L) >
static int Lua_Yield_Template( lua_State* a_pLuaState )
{
	class cLuaMessage : public cBaseMessage
	{
	public:
		virtual int Run() override
		{
			return Func1( pLuaState );
		}
	};
	cBaseMessage* pLuaMessage = new cLuaMessage();
	pLuaMessage->pLuaState = a_pLuaState;
	LuaMessageLock.Lock();
	m_LuaMessages[a_pLuaState] = pLuaMessage;
	LuaMessageLock.Unlock();
	
	printf("top: %i\n", lua_gettop(a_pLuaState) );
	return lua_yield(a_pLuaState, lua_gettop(a_pLuaState));
}

// A function to make sure two threads cannot log at the same time (C++ vs Lua)
static int LUA_SafeLog(lua_State* tolua_S)
{
	cCSLock Lock( LogLock );
	const char* str = tolua_tocppstring(tolua_S,1,0);
	printf("%s\n", str);
	return 0;
}


class cTestThread : public cIsThread
{
public:
	cTestThread( const AString & a_ThreadName ) : cIsThread( a_ThreadName ) {}
	void Execute(void) override
	{
		for (int i = 0; i < 400; ++i)
		{
			lua_getglobal(pLuaThread, FunctionToCall.c_str());
			int res = LUA_YIELD;
			do 
			{
				int nargs = 0;
				LuaMessageLock.Lock();
				LuaMessageMap::iterator itr = m_LuaMessages.find(pLuaThread);
				if (itr != m_LuaMessages.end())
				{
					cBaseMessage* pMessage = itr->second;
					m_LuaMessages.erase(itr);
					LuaMessageLock.Unlock();
					
					LuaStateLock.Lock();
					nargs = pMessage->Run();
					LuaStateLock.Unlock();
					delete pMessage;
				}
				else
				{
					LuaMessageLock.Unlock();
				}
				

				LuaStateLock.Lock();
				res = lua_resume(pLuaThread, nargs);
				LuaStateLock.Unlock();
				if (res != LUA_YIELD && res != 0)
				{
					report_errors( pLuaThread, res );
				}
				else if (res == 0) // no error, function returned
				{
					//cCSLock Lock(LogLock);
					//printf("returned type: %s %s\n", luaL_typename(pThread, -1) );
				}
			} while (res == LUA_YIELD);
		}
	}
	lua_State* pLuaThread;
	int LuaThreadReferenceIdx;
	AString FunctionToCall;
};

int main( int argc, char **argv )
{
	cTestThread TestThread1("TestThread1");
	TestThread1.FunctionToCall = "fromOtherThread";
	cTestThread TestThread2("TestThread2");
	TestThread2.FunctionToCall = "myFunc";

	lua_State* pLuaState = lua_open();
	luaL_openlibs( pLuaState );

	lua_register(pLuaState, "SafeLog", LUA_SafeLog);
	lua_register(pLuaState, "YieldGetNumber", Lua_Yield_Template<tolua_GetNumber> );
	lua_register(pLuaState, "YieldStringSplit", Lua_Yield_Template<tolua_StringSplit> );

	int s = luaL_loadfile(pLuaState, "Plugins/test.lua" );
	s = lua_pcall(pLuaState, 0, LUA_MULTRET, 0);

	for (int i = 0; i < 5; ++i)
	{
		LuaStateLock.Lock();
		TestThread1.pLuaThread = lua_newthread(pLuaState);
		TestThread1.LuaThreadReferenceIdx = luaL_ref(pLuaState, LUA_REGISTRYINDEX);
		TestThread2.pLuaThread = lua_newthread(pLuaState);
		TestThread2.LuaThreadReferenceIdx = luaL_ref(pLuaState, LUA_REGISTRYINDEX);
		LuaStateLock.Unlock();

		TestThread1.Start();
		TestThread2.Start();

		TestThread1.Wait();
		TestThread2.Wait();

		luaL_unref(pLuaState,LUA_REGISTRYINDEX,TestThread1.LuaThreadReferenceIdx ); 
		luaL_unref(pLuaState,LUA_REGISTRYINDEX,TestThread2.LuaThreadReferenceIdx ); 
	}

	printf("Lua stack size: %i\n", lua_gettop(pLuaState));
	lua_close( pLuaState );
}

test.lua
local localVar = 0
function GetLocalVar()
	localVar = localVar + 1
	return localVar;
end

function myFunc()
	SafeLog("in myfunc " .. GetLocalVar());
	local blaa = YieldGetNumber()
	SafeLog("blaa=" .. blaa)
	SafeLog("after yielding " .. GetLocalVar())
	return "num: " .. blaa;
end

function fromOtherThread()
	SafeLog("in other thread " .. GetLocalVar())
	local split = YieldStringSplit("one,2,three", ",")
	SafeLog("split: [" .. split[1] .. " - " .. split[2] .. " - " .. split[3] .. "]" )
end

SafeLog("lulwut")
Reply
Thanks given by:
#5
Sounds really cool Smile
Reply
Thanks given by:
#6
soo MCServer now supports C++ and lua for plugins or am i misunderstanding?
Reply
Thanks given by:
#7
STR, this is offtopic, but here's the reply Wink
Only Lua. C++ is used in the underlying framework to get Lua working.
Reply
Thanks given by: NiLSPACE
#8
(02-02-2013, 11:45 PM)STR_Warrior Wrote: soo MCServer now supports C++ and lua for plugins or am i misunderstanding?

You are misunderstandingTongue

In MCServer the Lua plugins are accessed by many different threads and through the plugins each thread has access to data that is owned by other threads.

For example you can say world data is owned by the tick thread, but the WebAdmin also has access to this data even though it is being executed from another thread.

The problem with this is that Lua plugins can only be accessed by a single thread at a time. When a certain situation happens, this can cause MCServer to hang (a deadlock). What I found out is a way to allow multiple threads to access plugins at the same time (well not exactly but close enough) which should eliminate all possibilities of deadlocks created by plugins
Reply
Thanks given by: xoft , Taugeshtu , bearbin , NiLSPACE
#9
Nice description Smile
Reply
Thanks given by:
#10
I've hit a bit of a roadbump.. GCC does not seem to allow passing a template function as a template function's template argumentTongue (template template template)

For example this compiles fine in visual studio, but gives me an error in GCC
tolua_function(tolua_S, "ForEachPlayer",         tolua_YieldTemplate< tolua_ForEach<cWorld, cPlayer, &cWorld::ForEachPlayer> >);

jni/../../source/ManualBindings.cpp: In static member function 'static void ManualBindings::Bind(lua_State*)':
jni/../../source/ManualBindings.cpp:1072: error: 'tolua_ForEach<cWorld, cPlayer, &cWorld::ForEachPlayer>' is not a valid template argument for type 'int (*)(lua_State*)' because function 'int tolua_ForEach(lua_State*) [with Ty1 = cWorld, Ty2 = cPlayer, bool (Ty1::* Func1)(cItemCallback<Ty2>&) = &cWorld::ForEachPlayer]' has not external linkage
jni/../../source/ManualBindings.cpp:1072: error: 'tolua_ForEach<cWorld, cPlayer, &cWorld::ForEachPlayer>' is not a valid template argument for type 'int (*)(lua_State*)' because function 'int tolua_ForEach(lua_State*) [with Ty1 = cWorld, Ty2 = cPlayer, bool (Ty1::* Func1)(cItemCallback<Ty2>&) = &cWorld::ForEachPlayer]' has not external linkage
jni/../../source/ManualBindings.cpp:1072: error: 'tolua_ForEach<cWorld, cPlayer, &cWorld::ForEachPlayer>' is not a valid template argument for type 'int (*)(lua_State*)' because function 'int tolua_ForEach(lua_State*) [with Ty1 = cWorld, Ty2 = cPlayer, bool (Ty1::* Func1)(cItemCallback<Ty2>&) = &cWorld::ForEachPlayer]' has not external linkage
jni/../../source/ManualBindings.cpp:1072: error: no matches converting function 'tolua_YieldTemplate' to type 'int (*)(struct lua_State*)'
jni/../../source/ManualBindings.cpp:1027: error: candidates are: template<int (* Func1)(lua_State*)> int tolua_YieldTemplate(lua_State*)
/cygdrive/d/android-ndk-r8/build/core/build-binary.mk:243: recipe for target `obj/local/armeabi/objs/mcserver/__/__/source/ManualBindings.o' failed
make: *** [obj/local/armeabi/objs/mcserver/__/__/source/ManualBindings.o] Error 1
Reply
Thanks given by:




Users browsing this thread: 5 Guest(s)