Ignition event scripts generally do not hold state--that is, any variables/functions/classes created in the script are discarded at the end of the script.
Ignition project library scripts, on the other hand, are run once at the top level the first time they are referenced elsewhere, and then persist until the project is edited/saved (or an parent project is edited/saved).
Put your import and your initialization of pid in the top level (unindented) in a project library script. Put the call to execute the instance in a function within the project library script. That function would use system.tag.readBlocking() to retrieve the process value into a local python variable for the pid instance to use, and would use system.tag.writeBlocking() to send the control value to an output tag.
Call that function from a gateway timer event at the desired pace.
I can use the gateway timer script to trigger the PID calculation, it works out well.
Also I tried to use a tag now(1000), and perform the PID calculation every 1 second. It also works out well.
To compare these two method, the second method is better in terms of modularizing the code, but I am not sure whether it will impact the gateway performance if I have dozens of the PID running, all at the same 1 second scan rate?
Probably won't matter for dozens or even hundreds of tags, as long as the events execute to completion quickly. But keep in mind that jython has substantial startup overhead--it is best to use a single timer script with all of your loops at a given pace.