Hi @mp5gosu
Correct, it was just to demonstrate a possible usage.
The next code would not have the same sense since .end will be checked only the next TestDBreak is called, while End(False) will force the thread to be ended right now.
GlobalThread.end = True # 1
GlobalThread.End(False) # 2
@lasselauch Here an implementation example of an execute_on_own_thread decorator.
import c4d
import functools
import time
import random
global globalThreadStorage
globalThreadStorage = []
class BGThread(c4d.threading.C4DThread):
def __init__(self, func, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
def Main(self):
self.func(*self.args, **self.kwargs)
return
class CleanupThread(c4d.threading.C4DThread):
def Main(self):
while True:
time.sleep(3)
c4d.threading.GeThreadLock()
global globalThreadStorage
# If there is only 1 thread in the global list and its self, leave since there is nothing else to clean.
if len(globalThreadStorage) == 1 and globalThreadStorage[0] == self:
c4d.threading.GeThreadUnlock()
return
# Removes all threads that are finished, so the garbage collector can delete them.
for thread in reversed(globalThreadStorage):
if not thread.IsRunning():
thread.End(False)
globalThreadStorage.remove(thread)
c4d.threading.GeThreadUnlock()
if self.TestBreak():
return
return
# Creates the decorator with argument
def execute_on_own_thread(wait=False):
# The decorated function
def decorated(func):
@functools.wraps(func)
# The wrapper
def wrapper(*args, **kwargs):
thread = BGThread(func, *args, **kwargs)
thread.Start()
# If we don't wait for the thread to be finalized, that means the created thread should live even after
# this function is executed. So we store a thread reference in a global list and we also initialize
# the cleanup thread, which as its name suggest will have the role to delete other thread.
if not wait:
c4d.threading.GeThreadLock()
# Adds the thread that encapsulates the function to the global list
global globalThreadStorage
globalThreadStorage.append(thread)
# Checks if there is already a cleanup thread, and if not create it
cleanupThreadPresent = False
for thread in globalThreadStorage:
if isinstance(thread, CleanupThread):
cleanupThreadPresent = True
break
if not cleanupThreadPresent:
cleanupThread = CleanupThread()
cleanupThread.Start()
globalThreadStorage.append(cleanupThread)
c4d.threading.GeThreadUnlock()
if wait:
thread.End(wait)
del thread
return
return wrapper
return decorated
@execute_on_own_thread(False)
def randomAction(a):
rTime = random.random() * 5
time.sleep(rTime)
print(a, rTime)
def main():
for x in xrange(10):
randomAction(x)
print("main done")
# Execute main()
if __name__=='__main__':
main()
@pim Here a safer and appropriate version for a GeDialog background thread, this does not rely on a global variable but only live as long as the dialog object lives while my previous example was something genric.
import c4d
import time, random, weakref
PLUGIN_ID_COMMAND_DATA = 1000000
# Background Thread is driven from Thread class to make it run in the background.
class BGThread(c4d.threading.C4DThread):
def __init__(self, dlg):
self.dlg = weakref.ref(dlg)
def GenerateBitmap(self):
# Generate a basebitmap with a random color"
clipMap = c4d.bitmaps.GeClipMap()
clipMap.Init(100, 100)
clipMap.BeginDraw()
clipMap.SetColor(int(random.random() * 255), int(random.random() * 255), int(random.random() * 255))
clipMap.FillRect(0, 0, 100, 100)
clipMap.EndDraw()
# It's important to return a clone since the original basebtimap is owned by the GeClipMap.
# So if the GeClipMap is destructed (and it will be the case after the return statement), the BaseBtimap will also be destructed.
return clipMap.GetBitmap().GetClone()
# Starts the thread to start listing to the port.
def Main(self):
while True:
# Creates a global try/except block since at any time the dlg can be destructed.
try:
# Checks if the thread needs to be closed e.g. a call of GlobalThread.End()
if self.TestBreak():
return
# Sleeps to not bother too much the CPU
time.sleep(0.1)
if not self.dlg().IsOpen():
continue
# Simulates a random update
randomEvent = bool(random.randint(1, 100) <= 10)
if randomEvent:
bmp = self.GenerateBitmap()
self.dlg().UpdatePicture(bmp)
# Catch the case self.dlg() return None, because the dlg object is destructed.
# This should not happen because in the dialog.__del__ method, this thread is ended.
# This also why a weakref is used, cause if the thread store directly a ref to the GeDialog,
# Due to Python refcounting, the Dialog will never be deleted, so __end__ will never be called.
except AttributeError:
return
# The Dialog class
class DialogWithBitmap(c4d.gui.GeDialog):
def __init__(self):
self.thread = BGThread(self)
self.thread.Start()
self.bmp = None
self.customUiBmp = None
def __del__(self):
self.thread.End()
# Define our Layout
def CreateLayout(self):
w = c4d.gui.SizePix(100)
h = c4d.gui.SizePix(100)
bcBitmapButton = c4d.BaseContainer()
bcBitmapButton[c4d.BITMAPBUTTON_BUTTON] = True
bcBitmapButton[c4d.BITMAPBUTTON_BORDER] = c4d.BORDER_IN
self.customUiBmp = self.AddCustomGui(1000, c4d.CUSTOMGUI_BITMAPBUTTON, "", c4d.BFH_FIT | c4d.BFV_TOP, w, h,
bcBitmapButton)
return True
def UpdatePicture(self, bmp):
# Called from the thread
self.bmp = bmp
# Sanity checks if the dialog is opened or eitehr the customUI is not defined
if self.IsOpen() and self.customUiBmp is not None:
self.customUiBmp.SetImage(self.bmp)
if c4d.threading.GeIsMainThread():
self.customUiBmp.Redraw()
else:
# If we are not on the main thread, redraw is not allowed, so add a special event with a unique ID
c4d.SpecialEventAdd(PLUGIN_ID_COMMAND_DATA, 0, 0)
return True
def CoreMessage(self, id, msg):
if id == PLUGIN_ID_COMMAND_DATA:
self.customUiBmp.Redraw()
return True
# The CommandData plugin which host the Dialog object alive
class CommandDataDlg(c4d.plugins.CommandData):
dialog = None
def Execute(self, doc):
if self.dialog is None:
self.dialog = DialogWithBitmap()
return self.dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=PLUGIN_ID_COMMAND_DATA, xpos=-1, ypos=-1,
defaultw=200, defaulth=150)
def RestoreLayout(self, sec_ref):
if self.dialog is None:
self.dialog = DialogWithBitmap()
return self.dialog.Restore(pluginid=PLUGIN_ID_COMMAND_DATA, secret=sec_ref)
# Main function
def main():
c4d.plugins.RegisterCommandPlugin(PLUGIN_ID_COMMAND_DATA, "GeDialog Example", 0, None, "GeDialog Example",
CommandDataDlg())
# Execute main()
if __name__ == '__main__':
main()
Regarding async, I can only forward you to Async IO in Python: A Complete Walkthrough. The main idea is Python does not support MultiThreading, this is the nature of the GIL so only one instruction can be executed in the Python VM machine at a given time. So as an example I write to a file a very very long string of few Gb, The python call is pretty short, what takes the most time is the write into the file (hardware limitation) so instead to wait until the file is written with async stuff you tell, to the python VM it can execute some stuff during this lost time of waiting for hardware (this is a very rough explanation). However, note this is not well supported in Python2.7 but this is in Python3.
Cheers,
Maxime.