W poprzednim artykule • Funkcje API - Jedna instancja bazy MS Access przedstawiłem kilka rozwiązań dotyczących zabezpieczenia bazy danych MS przed otwarciem przez użytkownika drugiej instancji otwartej bazy danych. Przedstawione metody działają w oparciu o opcje startowe MS Access tj. makro AutoExec lub właściwość StartupForm = „Formularz startowy”. Akcja makra AutoExec (Action ="RunCode") lub zdarzenie OnLoad() formularza wywoływały funkcję własną CountAccessInstances(...) przedstawioną w art. • Funkcje API - ile uruchomiono instancji MS Access •. Funkcja własna CountAccessInstances w zależności od przekazanych argumentów wyszukiwała okna MS Access klasy OMain o tytule zgodnym z tekstem przekazanym w opcjonalnym argumencie sAccessTitle.
' poglądowy kod
If CountAccessInstances([opconalnie: JakisTytulOkna MS Access]) > 1 Then
MsgBox "Nie możesz uruchomić następnej bazy danych, [instancji bazy danych] "
' zamknij otwieraną bazę
Application.Quit
End If
Uniemożliwienia otwarcie drugiej instancji bazy danych wymagało przekazania w argumencie funkcji własnej CountAccessInstances(...) tytułu okna MS Access. Wymogiem poprawnego działania funkcji było, by bieżąca baza danych miała ustawioną właściwość CurrentDb.Properties("AppTitle"), gdyż przy braku tej właściwośći, nie można było jednoznacznie określić tytułu okna MS Access, gdyż tytuł okna MS Access zmienia się w zależności od jego wersji. Umożliwiało to otwarcie drugiej instancji bieżącej bazy danych za pomocą innej wersji MS Access.
• Mutex - synchronizacja procesów.
• Co to jest mutex?
Tak ogólnie można napisać, że mutex jest obiektem służącym do synchronizacji wątków i dostępny jest dla wszystkich wątków uruchomionych aktualnie w systemie. Bardziej szczegółowe dane dotyczące mutexu możemy uzyskać z Wikipedii w artykule Problem wzajemnego wykluczania. Poniżej cytuję fragment z tego artykułu:
Algorytmy wzajemnego wykluczania (w skrócie często nazywane mutex, z ang. mutual exclusion) są używane w przetwarzaniu współbieżnym w celu uniknięcia równoczesnego użycia wspólnego zasobu (np. zmiennej globalnej) przez różne wątki/procesy w częściach kodu zwanych sekcjami krytycznymi. Sekcja krytyczna jest fragmentem kodu, w którym wątki (lub procesy) odwołują się do wspólnego zasobu. Sama w sobie nie jest ani mechanizmem, ani algorytmem wzajemnego wykluczania. Program, proces lub wątek może posiadać sekcje krytyczne bez mechanizmów czy algorytmów implementujących wzajemne wykluczanie.
Aby stworzyć mutex należy wywołać funkcję CreateMutex(...)
, która po stwierdzeniu braku
mutexu o podanej nazwie w systemie tworzy go, a funkcja GetLastError
zwraca wartość ERROR_SUCCESS=0. Jeśli mutex już istnieje, funkcja CreateMutex(...)
tworzy nowy uchwyt, a funkcja GetLastError
zwraca wartość ERROR_ALREADY_EXISTS.
W chwili tworzenia mutexu wątek żąda natychmiastowego prawa własności do niego.
Inne wątki moga otworzyć mutex za pomocą funkcji OpenMutex()
i czekać na objęcie
mutexu w posiadanie. Aby uwolnić mutex należy wywołć funkcję ReleaseMutex()
.
Jeśli wątek zakończy się i nie uwolni mutexu, to taki mutex uważany jest za porzucony
i każdy czekający wątek może objąć go w posiadanie.
Teoretycznie jawne zamykanie uchwytów nie jest niezbędne, gdyż system zamyka wszystkie uchwyty w chwili zakończenia procesu, jednak zalecane jest, kiedy obiekt jest niepotrzebny.
• Mutex - jedna instancja bazy.
W celu uniemożliwienia otwarcie drugiej instancji bieżącej bazy danych wykorzystamy trzy funkcje API:• CreateMutex(...)
- tworzy, bądź otwiera nazwany lub nienazwany mutex i zwraca uchwyt do niego• ReleaseMutex(...)
- zwalnia mutex, proces przestaje być właścicielem mutexu,
co pozwala przejąć go przez inny proces.• CloseHandle(...)
- zamyka obiekt identyfikowany przez uchwyt.
Option Compare Database Option Explicit ' • Function OneInstanceDb() As Long ' -------------------------------------------------------------------------------------- ' autor: Zbigniew Bratko - 02.2016 ' Tworzy nazwany muteks, uniemożliwiający otwarcie drugiej instancji bieżącej bazy danych ' [Out] - Przy powodzeniu tworzy nazwany muteks i zwraca liczbę różną od 0 (Zero) ' Przy niepowodzeniu zwraca 0 (Zero) ' #If VBA7 Then Private Declare PtrSafe Function CreateMutex Lib "kernel32" _ Alias "CreateMutexA" _ (lpMutexAttributes As SECURITY_ATTRIBUTES, _ ByVal bInitialOwner As Long, _ ByVal lpName As String) As LongPtr Private Declare PtrSafe Function ReleaseMutex Lib "kernel32" _ (ByVal hMutex As LongPtr) As Long Private Declare PtrSafe Function CloseHandle Lib "kernel32" _ (ByVal hObject As LongPtr) As Long Private Type SECURITY_ATTRIBUTES nLength As Long lpSecurityDescriptor As LongPtr bInheritHandle As Long End Type Private m_hMutex As LongPtr #Else Private Declare Function CreateMutex Lib "kernel32" _ Alias "CreateMutexA" _ (lpMutexAttributes As SECURITY_ATTRIBUTES, _ ByVal bInitialOwner As Long, _ ByVal lpName As String) As Long Private Declare Function ReleaseMutex Lib "kernel32" _ (ByVal hMutex As Long) As Long Private Declare Function CloseHandle Lib "kernel32" _ (ByVal hObject As Long) As Long Private Type SECURITY_ATTRIBUTES nLength As Long lpSecurityDescriptor As Long bInheritHandle As Long End Type Private m_hMutex As Long #End If Private Const ERROR_ALREADY_EXISTS = &HB7 Private Const ERROR_SUCCESS = 0& ' Funkcja własna OneInstanceDb() - uruchomienie: ' makro AutoExec: ' Action = RunCode ' FunctionName = OneInstanceDb () ' lub ' Zdarzenie OnLoad formularza startowego (StartUpForm) Public Function OneInstanceDb() As Long #If VBA7 Then Dim hMutex As LongPtr #Else Dim hMutex As Long #End If Dim sa As SECURITY_ATTRIBUTES sa.nLength = Len(sa) ' utwórz nazwany muteks i zostań jego właścicielem (bInitialOwner=1) hMutex = CreateMutex(sa, 1, "NazwaMuteksu") ' sprawdź, czy nie wystąpił błąd podczas tworzenia muteksu If (Err.LastDllError = ERROR_SUCCESS) Then ' zapisz uchwyt muteksu w zmiennej prywatnej na poziomie modułu m_hMutex = hMutex OneInstanceDb = 1 ' ... inne instrukcje (np. DoCmd.OpenForm "StartUpForm") Else If (Err.LastDllError = ERROR_ALREADY_EXISTS) Then ' muteks już istnieje, zamknij uchwyt Call CloseHandle(hMutex) MsgBox "Nie możesz uruchomić drugiej instancji bieżącej bazy danych!" Else ' (*) - patrz uwagi MsgBox "Nieprzewidziany błąd nr " & Err.LastDllError End If Application.Quit End If End Function ' • Function RemoveMutex() As Long ' ------------------------------------------------------- ' autor: Zbigniew Bratko - 02.2016 ' Zwalnia muteks i zamyka uchwyt muteksu ' [Out] - Przy powodzeniu zwraca liczbę różną od 0 (Zero) ' Przy niepowodzeniu zwraca 0 (Zero) ' Funkcja własna RemoveMutex() ' funkcję zwalniającą muteks należy uruchomić przy zamykaniu bazy, lub gdy chcemy ' zezwolić użytkownikowi na otwarcie drugiej instancji bazy. Public Function RemoveMutex() As Long Dim lRet As Long If m_hMutex <> 0 Then ' zwolnij muteks If ReleaseMutex(m_hMutex) <> 0 Then RemoveMutex = CloseHandle(m_hMutex) End If End If End Function
Uwagi:
(*) komunikat typu:
MsgBox "Nieprzewidziany błąd nr " & Err.LastDllError
jest dość zdawkowy i nie daje końcowemu użytkownikowi zbyt wiele informacji o przyczynie błędu.
W następnym artykule (być może) postaram się przedstawić rozwiązanie, by komunikaty o błędach były bardziej przyjazne dla użytkownika.
• Inne rozwiązania
Nasuwa mi się jeszcze jedno (dość nietypowe) rozwiązanie oparte o utworzenie, przez zabezpieczaną bazę, swojego własnego, prywatnego okna o indywidualnym tytule, którego istnienie świadczyć będzie, że baza została juz uruchomiona. Dodatkowo, utworzone okno będzie mogło spełniać rolę "malutkiego" komunikatora pomiędzy bazami. Ale o tym będzie w którymś kolejnym artykule. Gdy artykuł będzie gotowy, podam tutaj link do niego.
• Funkcje API - Mutex, jedna instancja bazy MS Access • Przykładowa baza MS Access do pobrania