Announcement

Collapse
No announcement yet.

multi user dll

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

    multi user dll

    Hi,

    Maybe I'm on a limb here but would it be OK to walk through the steps of 'converting' a dll to be multi-user?

    I've already figured out that I need to get into Thread Local Storage / Support and I've already used Threads in my DLL using Critical Section. What I've learned this far on using threads is this (maybe it's of any use to someone):

    Suppose you have two functions (i.e. thread1 and thread2) that you want to at the same time, both using SQL function using SQL Tools. Both functions bring the same result, but use a different method of getting it.
    First of all:
    - the threads have to be functions
    - the functions have X as long as passthrough vars.

    Steps to build a thread:
    1) include win32api.inc
    2) declare two global values (i.e. Hit1 as double, Hit2 as double) the value is filled when the function provides a result or is finished (e.g. 1 if OK, 2 if exit without result)
    3) make a sub that's gonna call the function by means of threading
    4) declare a var in this sub (say g_CriticalSection1) 'AS CRITICAL_SECTION',
    3) InitializeCriticalSection g_CriticalSection1
    The CS will protect your code
    4) right the next line: EnterCriticalSection g_CriticalSection1
    5) when using SQL Tools, use a wrapper function (i.e. thread1_wrap(x as long), thread2_wrap(x as long)
    6) start the threads (THREAD CREATE ... see PB help) using the wrapper functions, use a do while loop to wait for the return values, like this:
    Code:
        local hThread1 as long
        local hThread2 as long
        local result1 as long, result2 as long
    
        ' create threads
        sleep 1
        THREAD CREATE THREAD1_WRAP(50) TO hThread1
        sleep 1
        THREAD CREATE THREAD2_WRAP(50) TO hThread2
    
        ' wait for result
        Do While Hit1 = 0 and Hit2 = 0
            Sleep 1  ' to provide the time slice to the other function
        Loop
    
        ' parse the result and do action
        if Hit1 = 1 Then
          ' thread1 has got result so thread2 can stop: pass a counter value
          Hit2 = -1  ' in thread2 there's a lot of "if Hit2 = -1 then exit function" to make sure it exist as fast as possible
          
        end if
    
        ' wait for possible other running thread to finish
        do while hit1 <= 0 or hit2 <= 0 
            sleep 1
        loop
    
        ' when all is finished: close threads
        SLEEP 1
        THREAD CLOSE hThread1 TO Result1
        SLEEP 1
        THREAD CLOSE hThread2 TO Result2
        SLEEP 1
    7) when done, use LeaveCriticalSection g_CriticalSection1 and DeleteCriticalSection g_CriticalSection1

    The wrapper functions are important for SQL Tools and make it easier for yourself :
    Code:
    function Thread1_Wrap(x as long) as long
    
        ' set the return value to zero
        Hit1 = 0
        ' start the thread for SQL tools also (SQL data also have to be part of the thread so inititalise the SQL thread)
        SQL_THREAD %THREAD_START, 1
    
        ' call you actual worker function
        result& = Thread1(X)
    
        ' cancel possible opened statement number
        SQL_STATEMENTCANCEL <db nr>, <fixed stmt Nr>
    
        ' close the SQL thread
        SQL_THREAD %THREAD_STOP, 1
    
        ' to prevent a loop above
        if Hit1 <= 0 Then
           Hit1 = 2
        end if
    
        exit function
    end function

    - In the function itself: USE FIXED STATEMENTNUMBERS for each thread. Do not try to get a free statement number because it will possibly crash.
    - The wrapper function with the counter value make sure each SQL Thread is closed. If you fail to do so and start another SQL Thread using the same number, it will wait till the running thread is finished, i.e. the thread number is free. This is exactly NOT what you want.
    - The StatementCancel command is also very important: use fixed statementnumbers for each thread.


    OK, that's how I do it (and after quite some time, I got it to work). Of course, any comment for improvements are welcome!


    Now my current problem: this DLL of mine is called from a VB app and is part of the installation (i.e. present on each workstation). We are working towards getting it all on the internet.
    Then there's suddenly the problem of having the DLL on the server which is called from a VB COM. This still works great in multi-user when the Com is compiled as 'single threaded'. What IIS does in this case is just queue the requests, so one after another, where request 2 starts when request 1 is finished.
    So the solution for the Com is simple: use 'apartment threaded' but then my DLL is still not safe! The same behavior is seen when I start my app two time and both have it doing some work using the (same) dll. The results are all mixed up.

    So from reading Rector and Newcoming, as well as the forums, I have to use Thread Local Storage. So I included LibMain to my DLL.

    My question is: then what? My C++ experience is VERY limited, so it's hard to read the code example for R&C or MSDN....
    Do I have to assign all global vars in a UDT with a counter for each thread? Can LibMain keep track of the number of threads and how ?

    Hope you guys can shed some light here.

    Regards

    Jeroen




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

    #2
    hi jerome

    tls is useful when retrofitting code to make it thread-safe.

    remember that if your application only uses stack variables
    (no static or global variables) then it will automatically
    be thread safe without having to resort to using tls.

    tls is quite simple to use. see
    http://www.powerbasic.com/support/pb...ad.php?t=22648 for an example.

    if you want to pass a lot of information arounf the best
    way to use tls would be to create a type containing the info,
    you want to pass around, allocating memory for the type on the
    heap and storing the pointer to the type in the tls index.

    there are at least tls_minimum_available (64) tls indexes available
    per process but it's best to minimize the amount of indexes
    you use since dlls your application may link to may also be using
    tls.

    cheers

    florent




    [this message has been edited by florent heyworth (edited january 07, 2001).]

    Comment


      #3
      Thanks Florent,

      Is it also thread safe to keep passing variables around between subs and functions (instead of making them global) ?

      I'll have to look into your example. It'll take some time.... not as easy as you say

      Cheers
      Jeroen

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

      Comment


        #4
        Hi Jeroen

        (sorry for misspelling your name in my previous post)

        As long as you pass variables between subs and functions
        these variables are passed on the stack and this is
        inherently thread safe as long as you don't mix this
        with global/static variables.

        Cheers

        Florent

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

        Comment


          #5
          OK Florent,
          I checked your example from the forum, but I don't really get it.

          Humour me if you will: Just a simple example

          The SessionInfo is passed from a VB app. I would like multiple apps to use this DLL.
          I already included the LibMain and adapted it according to your example.
          And then what?

          Here's the code:
          Code:
          #COMPILE DLL "C:\PBDLL\TestMU\test1.dll"
          $INCLUDE "C:\PBDLL\TESTMU\SQLTools\WIN32API.INC"
          
          
          ' UDT
          TYPE SessionInfo
          
              InputString         AS STRING * 200
              OutputString        AS STRING * 700
              TestInfo            AS STRING * 250
              debugger            AS STRING * 50
              PrintInfo           AS STRING * 10
              UserInfo            AS STRING * 100
              Root                AS STRING * 100
          
          END TYPE
          
          ' DECLARES
          DECLARE SUB ENTRY ALIAS "ENTRY" (BYREF VBSession AS SessionInfo)
          DECLARE SUB TestSub(VBSession AS SessionInfo)
          DECLARE SUB ParseInput()
              
          ' GLOBAL / CONSTANTS
          GLOBAL TLSINDEX         AS DWORD
          
          GLOBAL InputString      AS STRING
          GLOBAL OutputString     AS STRING
          GLOBAL TestInfo         AS STRING
          GLOBAL Debugger         AS DOUBLE
          GLOBAL PrintInfo        AS DOUBLE
          GLOBAL UserInfo         AS STRING
          GLOBAL Root             AS STRING
          
          GLOBAL FreeFile1&
          
          
          '------------------------------------------------
          '------------------------------------------------
          '------------------------------------------------
          FUNCTION LIBMAIN(BYVAL hInstance   AS LONG, _
                           BYVAL fwdReason   AS LONG, _
                           BYVAL lpvReserved AS LONG) EXPORT AS LONG
          
            SELECT CASE fwdReason
          
              CASE %DLL_PROCESS_ATTACH
          
                  TLSINDEX = TLSALLOC()    ' allocate TLS
                  IF TLSINDEX = %TLS_OUT_OF_INDEXES THEN
                      FUNCTION = %FALSE
                  ELSE
                      CALL TLSSETVALUE(TLSINDEX, BYVAL %NULL)
                      FUNCTION = %TRUE
                  END IF
          
              CASE %DLL_PROCESS_DETACH
          
                  CALL TLSFREE(TLSINDEX)
                  FUNCTION = %TRUE
          
              CASE %DLL_THREAD_ATTACH
          
                  CALL TLSSETVALUE(TLSINDEX, BYVAL %NULL)
                  FUNCTION = %TRUE
          
              CASE %DLL_THREAD_DETACH
          
                 FUNCTION = %TRUE
          
            END SELECT
          
            EXIT FUNCTION
          
          END FUNCTION
          
          SUB Entry ALIAS "ENTRY" (BYREF VBSession AS SessionInfo) EXPORT
          
              ' make it thread safe for multi-user
              ' ????
          
              ' do work
              CALL testSub(VBSession)
              
          END SUB
          
          SUB TestSub(VBSession AS SessionInfo)
              
              ' convert values
              InputString = " " + TRIM$(VBSession.InputString) + " "
              OutputString = ""
              TestInfo = TRIM$(VBSession.InputString)
              Debugger = VAL(TRIM$(VBSession.InputString))
              PrintInfo = VAL(TRIM$(VBSession.InputString))
              Root = TRIM$(VBSession.InputString)
          
              ' open log
              IF PrintInfo >= 5 THEN
                  FreeFile1& = FREEFILE
              
                  IF RIGHT$(Root, 1) = "\" THEN
                      Root = MID$(Root, 1, LEN(Root) - 1)
                  END IF
                  OPEN Root + "\test.log" FOR OUTPUT AS #FreeFile1&
              END IF
              
              ' do some work
              CALL ParseInput()
              
              
              ' convert values back
              VBSession.OutputString = TRIM$(OutputString)
              IF TRIM$(OutputString) <> "" AND TestInfo = "example" THEN
                  VBSession.Debugger = "OK"
              END IF
              
              ' close log
              IF PrintInfo >= 5 THEN
                  CLOSE #FreeFile1&
              END IF
              
              EXIT SUB
          END SUB
          
          
          SUB ParseInput()
              
              IF INSTR(1, InputString, " hello ") <> 0 THEN
                  IF TestInfo <> "example" THEN
                      OutputString = "hello to you too."
                  ELSE
                      OutputString = "OK1"
                  END IF
                  
              ELSEIF INSTR(1, InputString, " byebye ") <> 0 THEN
                  IF TestInfo <> "example" THEN
                      OutputString = "CU."
                  ELSE
                      OutputString = "OK2"
                  END IF
              END IF
                  
          END SUB
          Hope someone can help,

          Sincerely
          Jeroen



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

          Comment


            #6
            Hi Jeroen

            you're initialiing the TLS index in LibMain but you are not using
            it to store/retrieve information. In the example you show you
            are outputting information to file.

            FREEFILE is not thread-safe. If you're going to do disk
            access across sessions you'll need to encapsulate the FILE access
            functions using mutexes.

            You are using a lot of globals to store information and write to it
            which you should not do with a thread safe app. I'm not quite
            sure what you want since in the test code you include you
            could directly modify the passed in VBSession and then call
            ParseInput() passing it the VBSession as a parameter (which would
            make it thread safe) instead of using global variables to access
            the info.

            Remember that if you pass variables around on the stack your app
            is automatically thread-safe and you don't need to use TLS at all.

            In the example shown you do not need TLS to make it thread-safe.
            Just get rid of all Globals, get rid of the TLS index and modify
            the VBSession and pass it from function to function as needed.

            What you'd need to do is to make sure that Disk Access and writes
            are guarded by Named Mutexes if you want to make sure that writes to
            file are synchronised accross processes and threads. Named mutexes
            are valid across processes whereas Critical Sections are valid
            for all threads in a process.

            I take it this is not a real life example - could you tell me what
            you are trying to do? Do you want to write a log of all VB apps
            that access your DLL? Would all the apps write to the same disk
            file? Should your app be synchronised across processes or
            just within the threads of one process?

            Cheers

            Florent


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

            Comment


              #7
              Hi Florent, thanks for responding.

              You're right: this is not a real program. BTW: I thought freefile was ALWAYS thread safe (I read that somewhere).
              The log file is not really important, but in my dll I use it for debugging.

              I know now that passing all local vars is thread-safe. But I want to learn TLS too.
              That's why I put the code there: it looks a bit like what I have : incoming, parsing, outgoing.
              How would I make this piece of code (no matter how redicule) thread-safe??

              Can't i use globals at all or do I have to put them in an array somehow?
              The answers to those question are important to me (just forget about the writing of the log file).

              Hope this provides more info on what I would like.

              Greetings
              Jeroen


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


              [This message has been edited by jeroen brouwers (edited January 08, 2001).]

              Comment


                #8
                using global variables in threads is very risky if the target variable is larger than 32-bits, since two or more threads may try to read and write to the variable's memory at the same time (actually it involves a context-switch when the data is only partially written and therefore contains undefined data). accessing global and static variables requires the use of a synchronization object, such as a critical section, mutex, etc.

                if the variable is 32-bits (or smaller), then the read/write cannot be interrupted by a context-switch, so the variable is "guaranteed" to contain legitimate data.

                for more information on writing multi-threaded app's, get yourself a copy of rector/newcomer's "win32 programming". see the faq forum posting "win32 reference books" for isbn numbers, etc. http://www.powerbasic.com/support/pb...hread.php?t=39

                regarding freefile in threads - rather than go to long lengths to deal with mutexes, etc, it is much easier to just restrict each thread to use a unique range of file numbers, say, based upon the thread id value. this placed the file number management in the hands of the programmer, but it is a simple and effective solution.

                for example, if each thread requires a maximum of 50 files, do something like this:
                Code:
                %maxthreadfiles = 50        ' maximum per thread
                %primarythreadhandles = 100 ' reserved for primary thread
                ...
                function mythread(byval id&) as long
                  local hfile1&, hfile2&
                  hfile1& = id& * %maxthreadfiles  + %primarythreadhandles
                  hfile2& = hfile1 + 1
                  ...
                end function

                ------------------
                lance
                powerbasic support
                mailto:[email protected][email protected]</a>
                Lance
                mailto:[email protected]

                Comment


                  #9
                  Hi,

                  Hope someone can help me al little further.
                  What I've done: I've put all global variables in a type and I already had one type that serves as input/output. Then I've rewritten my DLL to pass these types from function to function. Working fine, up to the point where I start my (internal) threading. This is where I get stuck.
                  I can only send the paramater value to the thread functions. How do I pass my variables then? It's got to do with this pointer to the type like in Florent's example but I don't really get it. Reading Rector & Newcomer doesn't do the trick. I'm missing something basic but don't know what.

                  So I have the two types:
                  Code:
                  Type Session
                     Inputstring as string * 200
                     Outputstring as string * 200
                  end type
                  
                  Type InternalInfo
                     PrintInfo as double
                     testString as string * 200
                     TempString as String * 200
                  end type
                  Then what?

                  Furthermore, I don't start the dll instance like in Florent's example myself. The Dll is called from a VB COM from the internet. My problem is to get this thread safe with multi-user.

                  Hope you guys can help

                  Regards
                  Jeroen

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

                  Comment


                    #10
                    If I'm reading your question correctly, simply pass the address of the target UDT to the thread function. This UDT would contain any necessary "startup" values that the thread function will need to use.


                    ------------------
                    Lance
                    PowerBASIC Support
                    mailto:[email protected][email protected]</A>
                    Lance
                    mailto:[email protected]

                    Comment


                      #11
                      Thanks Lance,
                      but just to be a bit more specific:

                      How do I do this??
                      Is this getting in the right direction?

                      I declare two vars before starting the threads:
                      LOCAL SessionPtr as SESSIONINFO PTR
                      LOCAL InternalPtr as INTERNALINFO PTR

                      Then create the threads (that only read the sessioninfo and fill different values in the IternalInfo)

                      And then - in each thread - declare the same vars and use

                      SessionPtr = HeapAlloc( GetProcessHeap(), %HEAP_ZERO_MEMORY, LEN(SessionInfo) )
                      InternalPtr = HeapAlloc( GetProcessHeap(), %HEAP_ZERO_MEMORY, LEN(InternalInfo) )

                      Something like this??

                      regards
                      Jeroen



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

                      Comment


                        #12
                        A song about iMalloc (by MSDN)

                        The OLE Memory Allocator
                        The COM Library provides an implementation of a memory allocator that is thread-safe (cannot cause problems in multi-threaded situations). Whenever ownership of an allocated chunk of memory is passed through a COM interface or between a client and the COM library, you must use this allocator to allocate the memory. Allocation internal to an object can use any allocation scheme desired, but the COM memory allocator is a handy, efficient, and thread-safe allocator.
                        A call to the API function CoGetMalloc provides a pointer to the OLE allocator, which is an implementation of the IMalloc interface. Rather than doing this, it is more efficient to call the helper functions CoTaskMemAlloc, CoTaskMemRealloc, and CoTaskMemFree, which wrap getting a pointer to the task memory allocator, calling the corresponding IMalloc method, and then releasing the pointer to the allocator.



                        ------------------
                        E-MAIL: [email protected]

                        Comment


                          #13
                          Thanks Semen,
                          Could you look at this shortly and tell me what I'm doing wrong here?

                          I tried using your code example from the forum and apply it to my example.
                          I need to pass the info to my thread. As you can see from the remmed out stuff


                          Code:
                          #COMPILE EXE "MUTEST1.EXE"
                          $INCLUDE "WIN32API.INC"
                          
                          DECLARE FUNCTION CoTaskMemAlloc LIB "ole32.dll" ALIAS "CoTaskMemAlloc" (BYVAL DWORD) AS DWORD
                          DECLARE SUB CoTaskMemFree LIB "ole32.dll" ALIAS "CoTaskMemFree" (BYVAL DWORD)
                          
                          TYPE SessionInfo
                              InputString       AS STRING * 200
                              OutputString       AS STRING * 200
                          END TYPE
                          
                          TYPE MyInfo
                              PtrToNext AS DWORD
                              Info AS STRING * 1000
                          END TYPE
                          
                          
                          SUB RunThrough_Order ALIAS "RUNTHROUGH_ORDER" (BYREF Session AS SessionInfo) EXPORT
                          
                              LOCAL PtrToRoot AS DWORD, PtrToEl AS DWORD
                              DIM pMyInfo AS MyInfo PTR
                          
                              LOCAL hThread AS LONG
                              LOCAL hResult AS LONG
                              LOCAL lResult AS LONG
                          
                              PtrToEl = CoTaskMemAlloc(LEN(MyInfo))
                                  IF PtrToRoot = 0 THEN
                                      PtrToRoot = PtrToEl
                                  ELSE
                                      @pMyInfo.PtrToNext = PtrToEl
                                  END IF
                          
                                  pMyInfo = PtrToEl
                                  @pMyInfo.Info = TRIM$(Session.InputString)
                          
                                      THREAD CREATE MyThread(pMyInfo) TO hThread
                                      'THREAD CREATE MyThread(PtrToEl) TO hThread
                          
                                      lResult = WaitForSingleObject( pMyInfo, %INFINITE )
                                      'lResult = WaitForSingleObject( PtrToEl, %INFINITE )
                          
                                      THREAD CLOSE hThread TO hResult
                                      
                                      Session.OutputString = TRIM$(@pMyInfo.Info)
                                      
                              CoTaskMemFree pMyInfo
                          
                          
                          END SUB
                          
                          FUNCTION MyThread(pMyInfo1 AS MyInfo PTR) AS LONG
                          'FUNCTION MyThread(PtrToEl AS DWORD) AS LONG
                          
                              DIM pMyInfo AS MyInfo PTR
                              
                              pMyInfo = pMyInfo1
                              'pMyInfo = PtrToEl
                              
                              MSGBOX "@pMyInfo.Info In Thread = " + @pMyInfo.Info
                              @pMyInfo.Info = TRIM$(@pMyInfo.Info) + " - delta!!"
                          
                          END FUNCTION
                          
                          
                          FUNCTION PBMAIN() AS LONG
                          
                              LOCAL Session AS SessionInfo
                          
                              Session.InputString = "1e input"
                              CALL RunThrough_Order(Session)
                          
                              SLEEP 3000
                              MSGBOX "Ending : " + TRIM$(Session.OutputString)
                          END FUNCTION

                          Any ideas?

                          Hope you can help

                          Regards

                          Jeroen

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

                          Comment


                            #14
                            Hi Jeroen

                            You were very close, I took the liberty of modifying your code.
                            Here's what it could look like:

                            Code:
                            #COMPILE EXE
                            $INCLUDE "WIN32API.INC"
                             
                            DECLARE FUNCTION CoTaskMemAlloc LIB "ole32.dll" ALIAS "CoTaskMemAlloc" (BYVAL DWORD) AS DWORD
                            DECLARE SUB CoTaskMemFree LIB "ole32.dll" ALIAS "CoTaskMemFree" (BYVAL DWORD)
                             
                            TYPE SessionInfo
                                InputString       AS STRING * 200
                                OutputString       AS STRING * 200
                            END TYPE
                             
                            TYPE MyInfo
                                PtrToNext AS DWORD
                                Info AS STRING * 1000
                            END TYPE
                            
                             
                            SUB RunThrough_Order ALIAS "RUNTHROUGH_ORDER" (BYREF Session AS SessionInfo) EXPORT
                                DIM pMyInfo AS MyInfo PTR
                             
                                LOCAL hThread AS LONG
                                LOCAL hResult AS LONG
                                LOCAL dwResult AS DWORD
                             
                                pMyInfo = CoTaskMemAlloc(SIZEOF(@pMyInfo))
                                IF pMyInfo THEN
                                    @pMyInfo.Info = TRIM$(Session.InputString)
                             
                                    THREAD CREATE MyThread(pMyInfo) TO hThread
                             
                                    dwResult = WaitForSingleObject( hThread, %INFINITE ) 'wait for thread completion
                                    SELECT CASE dwResult
                                        CASE %WAIT_OBJECT_0
                                            Session.OutputString = TRIM$(@pMyInfo.Info) + CHR$(0) 'terminate the string
                                            MSGBOX Session.OutputString
                                        CASE %WAIT_TIMEOUT
                                            MSGBOX "WaitForSingleObject returned and hThread is not signaled"
                                    END SELECT
                                     
                                    THREAD CLOSE hThread TO hResult
                                    CoTaskMemFree pMyInfo
                                END IF
                            
                             
                            END SUB
                             
                            FUNCTION MyThread(pMyInfo AS MyInfo PTR) AS LONG
                             
                                IF pMyInfo THEN
                                    @pMyInfo.Info = TRIM$(@pMyInfo.Info) + " - delta!!"
                                END IF
                             
                            END FUNCTION
                            
                             
                            FUNCTION PBMAIN() AS LONG
                             
                                LOCAL Session AS SessionInfo
                             
                                Session.InputString = "1e input"
                                CALL RunThrough_Order(Session)
                             
                            END FUNCTION
                            Cheers

                            Florent

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

                            Comment


                              #15
                              Florent,

                              I've got to do some rewriting but If this works!!

                              Thanks a lot already, I'll let you know!

                              Cheers
                              jeroen

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

                              Comment

                              Working...
                              X
                              😀
                              🥰
                              🤢
                              😎
                              😡
                              👍
                              👎