Announcement

Collapse
No announcement yet.

re-entrance, threading and multi-user

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • re-entrance, threading and multi-user

    Hi,
    I've been working on a DLL for some time now and - as a beginner - not realized the consequences of programming for multi-user applications.
    As soon as I link my dll to an ASP page using a COM (apartment threaded), I'm running into problems (i.e. crash of the web server).

    I've read Rector & Newcomer on synchronization, so I skipped all the globals from my DLL and put them into a type which I pass around from one function to another. I only use one global array to store a value that I use for statement numbers. The function that handles all changes and reading is starting with a critical section.
    It still didn't work. I've left out the threading that I use within my dll, no change.
    I've tried to copy the DLL (trick I found on this BBS), no real change.

    Now I've found this : http://www.unet.univie.ac.at/aix/aix..._safe_code.htm
    Basically, they tell in a simple matters what you should and shouldn't do (finally found one!).

    Anyway, just some questions about this (assuming the 'rules' for 'perfect' re-entrance):


    1) It is said that it's better to change the interface to have different incoming and outgoing variables.
    Am I correct if this goes as far as : as soon as a var changes in a function, I should store the values in a differnt variable?

    2) What about a Type then? Is it applying to the Type as a whole or just a member? To put it differently : When Type1 is passed to a function that changes member1, can I store it in member2 or should I store it in a differnt Type alltogetter.

    3) The use of local vars is - if I'm not mistaken - not really recommended if you want to have a perfect re-entrance situation. The initialization of the local is blocking the next thread.
    How much is this affecting the performance? Is it really that bad?
    How do you do this then? I can see it for one function but you have to initialise the variables somewhere! Is there a 'best practice' in this regard?


    Hope someone can help.

    Sincerely

    Jeroen Brouwers



    ------------------

  • #2
    "Apartment threaded", I am not familiar with. If this involves VB, please
    be advised that Visual Basic provides only limited support for threading,
    which may be the root of your problem.

    1) and 2) seem to involve some confusion? What do you mean by "the
    interface"? I can think of no point to storing a value in a different
    variable. Certainly, there should never be any need to change the type
    of a value.

    3) No, local variables are ideal for reentrant routines. You just need
    to avoid global or STATIC variables unless you fully understand all the
    implications of threading. There is no automatic blocking implied here.

    Keep studying! It sounds like you're making progress. I will be glad to
    help with any specific questions.

    ------------------
    Tom Hanlin
    PowerBASIC Staff

    Comment


    • #3
      Thanks Tom,

      'Apartment threaded' is the ActiveX variation that handles some form of multi-threading for the web via CoMarshalling in IIS.
      We tested it with a smaller component which works just fine (WCAT : 250 user simultanious).

      The thing is: the final goal is to have 8 interlinked DLLs (response time ~ 1 sec) linked to a web page. So speed is of great important (reason for PB in the first place).
      This will not work (somehow). I wrote a script engine which is called about 700 times per request. What we see in IIS is - besides the crash - huge context switches (we're talking more than 75.000!).
      If you start reading on context switches you will mostly find that they are caused by doing i/o work (so I cut it out) or some other form of 'shared resource'. So I thought: like database statement numbers. Solved it (each request gets its unique statement number).
      Tried the trick with copying the DLL(s) for each request, it got even worse!
      So then I found that a context switch is - in basis - a switch between threads because one thread has to wait for the other, so it provides control to the next one in the queuing list. Which causes a context switch. Like critical sections (skipped as many globals - that have to be protected - as possible), no change.

      The thing is: there's a strong correlation between context switches and the likelyness of IIS to crash!


      So back to the drawing table.

      The idea that they (the people from the mentioned website) pose is this:

      Code:
      'Code examples (re-write of the given functions in C++ to PB)
      
      ' non re-entrant
      ' reason1: input to function = output of function
      ' reason2: local initialization of i
      FUNCTION Test1 ( InputString() AS STRING ) AS LONG
      
          IF UBOUND (InputString) = -1 THEN EXIT FUNCTION
      
          LOCAL i AS INTEGER
      
          FOR i = 0 TO UBOUND(InputString)
              IF UCASE$(InputString(i)) <> InputString(i) THEN
                  InputString(i) = UCASE$(InputString)
              END IF
          NEXT i
          
      END FUNCTION
      
      ' perfectly re-entrant
      ' solution by a change of 'interface', i.e. design
      FUNCTION Test2 (InputString() AS STRING, OutputString() AS STRING, i AS INTEGER) AS LONG
      
          IF UBOUND (InputString) = -1 THEN EXIT FUNCTION
          
          FOR i = 0 TO UBOUND(InputString)
              IF UCASE$(InputString(i)) <> InputString(i) THEN
                  OutputString(i) = UCASE$(InputString)
              END IF
          NEXT i
              
      END FUNCTION

      I would like to now if this is true (before I start recoding about 1 Mb of compiled code).
      If so, what nuances can be made regarding storing values in a type: does the storage principle apply to members of a type or to the type as a whole?

      Regards

      Jeroen

      ------------------


      [This message has been edited by jeroen brouwers (edited February 07, 2001).]

      Comment


      • #4
        Jeroen

        the code examples you posted are incorrect - neither one nor the
        other is more or less thread safe than the other however your test2
        function will not compile since you should not use a stack
        parameter as a loop counter....

        Additionally you are using string arrays and attempt to assign
        the entire array to the array subscript which will not work.

        This code will compile:

        Code:
        FUNCTION Test1 ( InputString() AS STRING ) AS LONG
        
            IF UBOUND (InputString) = -1 THEN EXIT FUNCTION
        
            LOCAL i AS INTEGER
        
            FOR i = 0 TO UBOUND(InputString)
                IF UCASE$(InputString(i)) <> InputString(i) THEN
                    InputString(i) = UCASE$(InputString(i))
                END IF
            NEXT i
        
        END FUNCTION
        
        FUNCTION Test2 (InputString() AS STRING, OutputString() AS STRING) AS LONG
        
            IF UBOUND (InputString) = -1 THEN EXIT FUNCTION
            LOCAL i AS LONG
            FOR i = 0 TO UBOUND(InputString)
                IF UCASE$(InputString(i)) <> InputString(i) THEN
                    OutputString(i) = UCASE$(InputString(i))
                END IF
            NEXT i
        
        END FUNCTION
        The whole issue here is that you are passing string arrays to a
        function - how are the arrays declared - is there any risk in your
        code that the arrays will get overwritten, does each array point
        to separate memory, etc...

        Having different input/output parameters will not make your code
        any more or less thread safe. You need to remember: stack and local
        variables are automatically thread-safe.

        The whole issue of writing thread safe code is making sure memory
        / variables do not get overwritten/corrupted from thread to thread.
        That means essentially no globals or static variables unless you
        synchronize those variables.

        A type passed on the stack is no different from any other stack
        variable.

        This is essentially a design issue and without knowing the design
        of your app it's difficult to give any specific advice. The GPFs
        you're seeing could just as well be provoked by invalid memory
        access (for example accessing an array past its bounds, etc).

        Cheers

        Florent


        [This message has been edited by Florent Heyworth (edited February 07, 2001).]

        Comment


        • #5
          Thanks Florent,

          I was almost rewriting my code already ...

          I can't really put all of my code on the web (is a lot already), but I'm getting stuck on the things to look for.
          Fact is the IIS crashes as soon as I link my script engine to it. That is, if I call it one hundred times, it's OK; make it 500 and it crashes.
          The more I add, the higher the context switches / sec. and the lower the request executed / sec.

          So I've stripped all i/o, I've made as many variables local as possible, I've protected the 2 arrays that are still global by critical section, I've left out the internal threading for testing purposes, I've checked that at all times arrays are checked on their ubound (being -1).
          If it is a call to a non existing dimension of an array, why would it appear in multi-threading and not in a single request format?

          I don't know what to look for anymore. Getting lost here.

          Any tips / suggestions what to check for a appreciated.


          Hope you can help

          Sincerely
          Jeroen



          ------------------

          Comment


          • #6
            Hi Jeroen

            you mentioned you used a TYPE to pass information around - is the
            type declared using dynamic memory? Where is the type declared from?

            You also mention using a Global array to hold statement number
            information. How do use use it? Do you Redim this Global array?
            You mentioned that access to the array is serialized with a critical
            section - how do you know which statement belongs to which thread, etc?

            I take it you are using VB and that the DLL is written in PB. Did
            you encapsulate the DLL using a VB Class?

            As I said difficult to really offer concrete advice in this situation.
            I'm pressed for time at the moment but if you get really stuck you
            could email me your code and I could throw a quick look at it to
            see if something stands out.

            My mail is mailto:[email protected][email protected]</A>

            Cheers

            Florent


            ------------------

            Comment


            • #7
              Thanks Florent,

              If you could spare a little time later on, I would be most happy.

              At my Entry Function :
              FUNCTION RunThrough_Order ALIAS "RUNTHROUGH_ORDER" (BYREF VBSession AS SessionInfo) EXPORT AS LONG

              right in the beginning I declare two types
              LOCAL Session AS SessionInfo
              LOCAL INTERNAL AS INTERNALINFO

              I pass the incoming info
              session = VBSession

              and fill the internal with general values
              result1& = GetGlobalVars(Internal)

              Then I call the function that handles the one global array ThrStmt :
              LOCAL St AS DOUBLE
              result& = StmtNrAdd(St, 1)
              Internal.FileNr = St
              Internal.ThrStmt = St


              The function StmtNrAdd is used to add, delete and get the value from the array:
              Code:
              FUNCTION StmtNrAdd(StmtNr AS DOUBLE, Code AS DOUBLE) AS LONG
              
                  LOCAL GSC1 AS CRITICAL_SECTION
                  InitializeCriticalSection GSC1
                  EnterCriticalSection GSC1
              
                  IF Code = 1 THEN
                      ' add
                      IF UBOUND(ThrStmt) = -1 THEN    ' 1e keer
                          REDIM ThrStmt(1,1)
                          ThrStmt(0,0) = 0
                          ThrStmt(1,0) = TIMER
                      END IF
              
                      REDIM PRESERVE ThrStmt(1, UBOUND(ThrStmt, 2) + 1)
                      ThrStmt(0,UBOUND(ThrStmt,2)) = ThrStmt(0,UBOUND(ThrStmt, 2) - 1) + 2
                      ThrStmt(1,UBOUND(ThrStmt,2)) = TIMER
                      StmtNr = UBOUND(ThrStmt,2)
              
                      IF ThrStmt(0,StmtNr) >= 256 THEN
                         '??
                      END IF
              
                  ELSEIF Code = 0 THEN
                      ' set to nul zetten
                      ThrStmt(0,StmtNr) = 0
              
                      IF StmtNr = UBOUND(ThrStmt,2) THEN
                          ' delete if possible
                          LOCAL i AS INTEGER, Hit&
                          FOR i = UBOUND(ThrStmt,2) TO 1 STEP -1
                              IF ThrStmt(0,i) <> 0 THEN
                                  IF TIMER - ThrStmt(1,i) > 15 THEN
                                      ThrStmt(0,i) = 0
                                  ELSE
                                      Hit& = 1
                                      EXIT FOR
                                  END IF
                              END IF
                          NEXT i
              
                          IF Hit& = 1 THEN
                              REDIM PRESERVE ThrStmt(1,i)
                          ELSE
                              REDIM ThrStmt(1,0)
                          END IF
                      END IF
                  ELSE
                      ' get value
                      FUNCTION = ThrStmt(0, StmtNr)
                  END IF
              
                  LeaveCriticalSection GSC1
                  DeleteCriticalSection GSC1
              
              END FUNCTION
              So in my DLL , I don't use 'var& = ThrStmt(0, Internal.ThrStmt)' but I use 'StmtNr = StmtNrAdd(Internal.ThrStmt, 2)' to make sure the global data are protected.


              About the VB Class:
              Yes, I used and ActiveX DLL that is called from an ASP webpage using
              Set VBObj = Server.CreateObject(<project name>, <class name> )

              as to create the object using
              Temp = VbObj.<function name> ( <inputvalues> )
              to activate the function.

              The ActiveX DLL also has a module where the type is set.
              The actual declaration is done inside the function, i.e. local. The incoming values in the function are passed to the Type, which is passed to the PBDLL. The results of the PB DLL are stored in this type and come back in the ActiveX which - in turn - passes the values from the Type to the original values that came in. The ASP can then read these and correspond accordingly. Simple, no?

              This works OK (as far as I can oversee).

              The ActiveX is apartment threaded (not single) which should allow for the IIS to create a thread for each instance, i.e. web request for the page.
              COM uses only 1 thread per page, which make it difficult to test manually. So we use WCAT which generates users and uses a GET to get the results from the page that calls the object.
              If you get into that stuff, it gets kind of confusing (to me at least).

              I cleared the registy and recompiled to check for version issues and properties.

              Does this still make sense?

              Regards
              Jeroen



              ------------------

              Comment


              • #8
                Jeroen --

                I have not looked at your code in detail, but your use of Critical Sections is clearly not correct.

                Since your Critical Section is LOCAL, every time you call your function a new Critical Section will be created. The function with then "claim" it, do its work, "release" it, and then destroy it. That's useless because no other thread can possibly attempt to enter the Critical Section, since it is LOCAL. It will never block any thread from doing anything at all. In other words, if a second thread calls that function, it will get its own LOCAL critical section. Critical Sections must be GLOBAL in order to work. (Ok, I suppose you could use a STATIC CRITICAL_SECTION, but it would be hard to write, and why do it? The whole idea is to create a structure that all threads can access.)

                Here's a thumbnail sketch of the way you should use a Critical Section...

                1) In WinMain, DIM gtCritSect AS GLOBAL CRITICAL_SECTION then, also in WinMain, InitializeCriticalSection gtCritSect

                2) In the sub or function you want to protect, use EnterCriticalSection gtCritSect at the very beginning and LeaveCriticalSection gtCritSect at the very end. Make sure there is no way to exit from from the sub/function without using LeaveCriticalSection, or the next time ANY thread calls that function it will lock up forever.

                3) In WinMain, at the very end of your program, DestroyCriticalSection gtCritSect.

                An arrangement like that will allow a thread to call that function, but if another thread tries to call it while the first thread is still using it, the second thread will be "paused" until the first thread executes LeaveCriticalSection. At that moment, the second thread will be un-paused and allowed to continue.

                -- Eric


                ------------------
                Perfect Sync: Perfect Sync Development Tools
                Email: mailto:[email protected][email protected]</A>



                [This message has been edited by Eric Pearson (edited February 07, 2001).]
                "Not my circus, not my monkeys."

                Comment


                • #9
                  OK!

                  I was already doubting that and have tried something similar (without results then) but I'll give it another try. Thanks.

                  I'll let you know how it goes


                  Sincerely
                  Jeroen



                  ------------------

                  Comment


                  • #10
                    Hi,

                    Thanks for the explanation Eric, it works better already.
                    I'm stripping my code to see where the further problems are.

                    One of the things I encounter is that as soon as start doing actual SQL statements (mostly select) the context switches go up and the request executed go down.
                    So I stripped all of them and it provided quite an improvement (from an average of 6,5 request executed / sec. to about 25).

                    Still there are context switches. So I'm stripping my code to see where the problem is.

                    This brings me to my question: what general code design considerations do I have to apply to optimize my code for multi-threading. I can't find much info on this on the net (maybe I'm not searching right..)?
                    What can I do / shouldn't do? For instance, if I leave a recursive function out of the code, it improves the requests executed / sec. Does this means that recursion techniques are better not to be used in multi-threading applications?

                    Is there a good book on this? I know I learned most of the stuff myself but it seems I'm missing some basics.

                    Hope someone can help a little,

                    Regards
                    Jeroen

                    ------------------


                    [This message has been edited by jeroen brouwers (edited February 09, 2001).]

                    Comment

                    Working...
                    X