Thử viết code lập trình đa luồng bằng TwinBasic và khả năng ứng dụng vào COM add-in chạy trong Excel (1 người xem)

Liên hệ QC

Người dùng đang xem chủ đề này

Tôi tuân thủ nội quy khi đăng bài

nguyendang95

Thành viên chính thức
Tham gia
25/5/22
Bài viết
95
Được thích
92
TwinBasic, chắc nhiều đã từng nghe nói đến, là ngôn ngữ lập trình được nhiều tín đồ VB đánh giá là ứng viên rất sáng giá thay thế cho VB6/VBA hiện nay. Mặc dù qua nhiều năm vẫn còn đang ở giai đoạn beta, tuy nhiên TwinBasic tương thích khá tốt với VB6/VBA giúp lập trình viên có thể dễ dàng chuyển đổi, ngoài ra nó còn bổ sung thêm nhiều tính năng đáng giá mà VB6/VBA còn thiếu.
Nói về khả năng lập trình đa luồng, hiện tại TwinBasic vẫn chưa có cú pháp giúp lập trình viên dễ dàng triển khai tính năng này và tác giả của ngôn ngữ lập trình này cũng hứa hẹn bổ sung cú pháp trong Quý 2 năm nay theo như ghi nhận trong Roadmap (lịch trình) đăng tải trên Github của tác giả, tuy nhiên với sự trợ giúp của các hàm Win32 API, chúng ta có thể dễ dàng viết một ứng dụng chạy đa luồng và tốn chút sức lực để gỡ lỗi và chạy thử hoàn thiện.
Code dưới đây lần lượt tạo thread, gửi http request đến các URL trong danh sách và nhận lại phản hồi từ các URL này.

Mã:
Module MainModule
    Private Declare PtrSafe Function CreateThread Lib "Kernel32.dll" (ByVal lpThreadAttributes As LongPtr, ByVal dwStackSize As Long, ByVal lpStartAddress As LongPtr, ByVal lpParameter As LongPtr, ByVal dwCreationFlags As Long, ByRef lpThreadId As Long) As LongPtr
    Private Declare PtrSafe Function CloseHandle Lib "Kernel32.dll" (ByVal hObject As LongPtr) As Long
    Private Declare PtrSafe Function WaitForSingleObject Lib "Kernel32.dll" (ByVal hHandle As LongPtr, ByVal dwMiliseconds As Long) As Long
    Private Declare PtrSafe Sub CoUninitialize Lib "Ole32.dll" ()
    Private Declare PtrSafe Function CreateMutex Lib "Kernel32.dll" Alias "CreateMutexW" (ByVal lpMutexAttributes As LongPtr, ByVal bInitialOwner As Long, ByVal lpName As String) As LongPtr
    Private Declare PtrSafe Function ReleaseMutex Lib "Kernel32.dll" (ByVal hMutex As LongPtr) As Long
    Private Declare PtrSafe Function CoInitializeEx Lib "ole32.dll" (ByVal pvReserved As LongPtr, ByVal dwCoInit As Long) As Long
    Private Declare PtrSafe Function GetCurrentThreadId Lib "Kernel32.dll" () As Long
    Private Mutex As LongPtr
    Private Const COINIT_MULTITHREADED = &H0
    Private Const COINIT_DISABLE_OLE1DDE = &H4
    Private Const WAIT_OBJECT_0 = &H0
    Private Const INFINITE = -1&
    Private Const WAIT_ABANDONED = &H80
    Private HttpResponses As New Collection(Of String)
    Public Sub Main()
        Mutex = CreateMutex(0, 0, vbNullString) 'Khởi tạo Mutex để sử dụng cho mục đích đồng bộ giữa các thread
        If Mutex = 0 Then
            Console.WriteLine("Failed to create mutex")
            Exit Sub
        Else: Console.WriteLine("Mutex has been created")
        End If
        Dim Thread As LongPtr, ThreadId As Long, i As Long
        Dim Urls As Variant = Array("https://google.com.vn", "https://youtube.com", "https://x.com", "https://facebook.com")
        Dim Threads(0 To 3) As LongPtr
        For i = LBound(Urls) To UBound(Urls)
            Thread = CreateThread(0, 0, AddressOf GetHttpResponse, VarPtr(Urls(i)), 0, ThreadId) 'Tạo thread
            If Thread = 0 Then
                Console.WriteLine("Failed to create thread")
                CloseHandle(Mutex)
                Exit Sub
            End If
            Threads(i) = Thread
        Next
        For i = LBound(Threads) To UBound(Threads)
            WaitForSingleObject(Threads(i), INFINITE) 'Chờ các thread chạy xong
            CloseHandle(Threads(i)) 'Dọn dẹp thread
        Next
        CloseHandle(Mutex) 'Dọn dẹp Mutex sau khi dùng xong
        Console.WriteLine("Mutex has been destroyed")
        Console.WriteLine("Number of responses: " & CStr(HttpResponses.Count)) 'Số lượng http response tiếp nhận từ các thread
    End Sub
   
    Private Sub GetHttpResponse(ByVal Url As Variant)
        Dim ThreadId As Long = GetCurrentThreadId(), WaitResult As Long
        Console.WriteLine("Thread Id: " & CStr(ThreadId))
        'Khởi tạo COM library là MTA, thêm tùy chọn tắt tính năng OLE1DDE đã lỗi thời
        CoInitializeEx(0, COINIT_MULTITHREADED Or COINIT_DISABLE_OLE1DDE)
        Dim objWinHttp As WinHttp.WinHttpRequest
        Set objWinHttp = New WinHttp.WinHttpRequest
        With objWinHttp
            .Open("GET", Url, False)
            On Error Resume Next
            .Send()
            If Err.Number <> 0 Then
                Console.WriteLine("An error occurred. " & Err.Description)
            Else
                If .Status = 200 Then
                    Console.WriteLine("Response from " & Url & " with status code " & CStr(.Status))
                    WaitResult = WaitForSingleObject(Mutex, INFINITE) 'Thread tiến hành chiếm dụng Mutex
                    If WaitResult = WAIT_OBJECT_0 Then
                        Console.WriteLine("Mutex acquired by thread id " & CStr(ThreadId))
                        HttpResponses.Add(.ResponseText) 'Mục đích chiếm dụng Mutex là nhằm buộc các thread khác phải chờ đến lượt để ghi vào biến collection
                        'Tránh trường hợp các thread cùng ghi vào biến collection gây sai lệch kết quả, hay còn gọi là race condition
                        'Dùng xong Mutex thì hoàn trả để các thread khác sử dụng
                        If ReleaseMutex(Mutex) <> 0 Then
                            Console.WriteLine("Mutex released by thread id " & CStr(ThreadId))
                        Else: Console.WriteLine("An error occurred while trying to release mutex from thread id " & CStr(ThreadId))
                        End If
                    ElseIf WaitResult = WAIT_ABANDONED Then Console.WriteLine(CStr(ThreadId) & " acquired an abandoned mutex")
                    End If
                End If
            End If
            On Error GoTo 0
        End With
        CoUninitialize() 'Kết thúc sử dụng COM library
    End Sub
End Module

Kết quả trả về đúng như mong muốn.

1738665327346.png

Liên hệ đến khả năng lập trình đa luồng khi viết COM add-in cho Excel bằng TwinBasic
TwinBasic cho phép lập trình viên viết COM add-in cho các ứng dụng Office nói chung và Excel nói riêng. Như vậy lập trình viên có thể viết code đa luồng xử lý giúp cải thiện hiệu năng tính toán cho add-in, ví dụ như trích xuất dữ liệu từ các bảng tính sau đó tạo nhiều luồng xử lý tính toán và ghi kết quả ngược lại vào bảng tính, hoặc tạo một thread mới và đưa công việc tính toán nặng nề lên đó xử lý giúp UI của Excel không bị đơ, chẳng hạn.
Với mô hình đối tượng của Excel nói riêng và các ứng dụng Office nói chung, việc này có thể hơi phức tạp do nó luôn chạy trong thread chính STA. Tham khảo bài viết từ Microsoft Threading support in Office để biết thêm thông tin chi tiết.
 
Một ví dụ về lập trình đa luồng khi viết COM add-in cho Excel
Dưới đây là một ví dụ về việc lập trình đa luồng cho COM add-in trên Excel, trong đó tạo một thread mới có sử dụng mô hình đối tượng của Excel theo hướng dẫn của bài viết Threading support in Office.
Trước hết, người dùng cần sao chép code mẫu từ thư mục dưới đây có tên là Sample 5. MyCOMAddin.
1738943864184.png
Tiếp theo, người dùng chọn Project-References trên thanh trình đơn nằm ở góc trái cửa sổ và tiến hành cài đặt gói WinDevLib for Implements v1.x.x.x và gói Windows Development Library for twinBASIC v8.x.x.x. Những gói cài đặt này giúp người dùng không cần phải khai báo Win32 API giống như những gì thường làm khi lập trình bằng VB6/VBA, đây cũng có thể xem như phiên bản tệp header .h của C++ vậy.
1738944090934.png
Thêm các tham chiếu cần thiết
1738944113955.png
Thay đổi nội dung của tệp MyCOMAddin.twin như sau:
Mã:
[ClassId("EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA01")]
Class Demo
    Implements IMessageFilter
    Implements IDTExtensibility2
    [WithDispatchForwarding]
    Implements IRibbonExtensibility
    Private ExcelApp As Excel.Application

    Sub OnConnection(ByVal Application As Object, _
                            ByVal ConnectMode As ext_ConnectMode, _
                            ByVal AddInInst As Object, _
                            ByRef custom As Variant()) _
        Implements IDTExtensibility2.OnConnection
        Set ExcelApp = Application
    End Sub
   
    Sub OnDisconnection(ByVal RemoveMode As ext_DisconnectMode, _
                        ByRef custom As Variant()) _
        Implements IDTExtensibility2.OnDisconnection
        Set ExcelApp = Nothing
    End Sub
   
    Sub OnAddInsUpdate(ByRef custom As Variant()) _
        Implements IDTExtensibility2.OnAddInsUpdate

    End Sub
   
    Sub OnStartupComplete(ByRef custom As Variant()) _
        Implements IDTExtensibility2.OnStartupComplete
       
    End Sub
   
    Sub OnBeginShutdown(ByRef custom As Variant()) _
        Implements IDTExtensibility2.OnBeginShutdown

    End Sub
   
    Private Function GetCustomUI(ByVal RibbonID As String) As String _
        Implements IRibbonExtensibility.GetCustomUI
        Dim strXML As String
        strXML &= "<customUI xmlns=""http://schemas.microsoft.com/office/2006/01/customui"">" & vbCrLf
        strXML &= "  <ribbon startFromScratch=""false"">" & vbCrLf
        strXML &= "    <tabs>" & vbCrLf
        strXML &= "      <tab id=""tabTest"" label=""twinBASIC Test"">" & vbCrLf
        strXML &= "        <group id=""grpTwin1"" label=""Hello Group"">" & vbCrLf
        strXML &= "          <button id=""btnAddNewWorksheet"" size=""large"" label=""Tạo bảng tính mới"" getImage=""OnGetHelloWorldButtonImage"" onAction=""OnAddNewWorksheetClicked""/>" & vbCrLf
        strXML &= "        </group>" & vbCrLf
        strXML &= "      </tab>" & vbCrLf
        strXML &= "    </tabs>" & vbCrLf
        strXML &= "  </ribbon>" & vbCrLf
        strXML &= "</customUI>" & vbCrLf
        Return strXML
    End Function
   
    'Hàm gọi lại (callback) được kích hoạt khi người dùng bấm vào nút "Tạo bảng tính mới"
    Public Sub OnAddNewWorksheetClicked(Control As IRibbonControl)
        /*
            Gọi hàm SHCreateMemStream để cấp phát bộ nhớ (memory stream) dưới dạng IStream để chuẩn bị cho quá trình biến đổi dữ liệu (marshaling)
            từ biến ExcelApp (Excel.Application) để khi tạo thread mới thì thread này có thể sử dụng nó
            để gọi mô hình đối tượng của Excel
            Vùng nhớ này luôn đảm bảo tính an toàn thread (thread safety)
            Ngoài ra, cũng có thể sử dụng hàm CreateStreamOnHGlobal mà vẫn cho kết quả tương tự, tuy nhiên hiệu năng của hàm này kém hơn
        */
        Dim pStream As IStream = SHCreateMemStream(ByVal 0, 0)
        'Bắt đầu tiến trình biến đổi dữ liệu từ đối tượng Excel.Application thành interface IDispatch (late binding)
        Set pStream = CoMarshalInterThreadInterfaceInStream(IID_IDispatch(), ObjPtr(ExcelApp))
        Dim Thread As LongPtr
        'Tạo thread xử lý thủ tục AddNewWorksheet, tham số là biến pStream mang đối tượng Excel.Application đã được biến đổi dữ liệu
        Thread = CreateThread(ByVal 0, 0, AddressOf AddNewWorksheet, ByVal pStream, 0, 0)
        If Thread = 0 Then
            MsgBox("Failed to create thread", vbExclamation, "Thread creation error")
            Exit Sub
        End If
        'Tạo vòng lặp thông điệp (message loop) để giữ cho giao diện người dùng luôn phản hồi
        Dim Msg As MSG
        While GetMessage(Msg, 0, 0, 0) <> 0
            TranslateMessage(Msg)
            DispatchMessage(Msg)
            'Đợi cho đến khi thread xử lý xong công việc thì kết thúc vòng lặp thông điệp
            If WaitForSingleObject(Thread, 1) = WAIT_OBJECT_0 Then Exit While
        Wend
        'Dọn dẹp thread, lưu ý đóng handle của thread không có nghĩa là thread sẽ bị hủy bỏ
        CloseHandle(Thread)
    End Sub
   
    Private Sub AddNewWorksheet(ByVal Stream As IStream)
        /*
            Khởi tạo thread theo mô hình STA (single-threaded apartment)
            Đây là yêu cầu mà mô hình đối tượng của Excel nói chung và mô hình đối tượng các ứng dụng Office yêu cầu
            nếu muốn sử dụng mô hình của chúng trên thread không phải thread chính (hoặc UI thread)
        */
        Dim HResult As Long = CoInitializeEx(0, COINIT_APARTMENTTHREADED Or COINIT_DISABLE_OLE1DDE)
        If HResult = E_FAIL Then
            MsgBox("Failed to initialize COM library", vbExclamation, "Error")
            Exit Sub
        Else
            Dim MessageFilter As IMessageFilter
            'Đăng ký bộ lọc thông điệp (message filter) để nhận thông điệp từ Excel khi nó phàn nàn rằng đang bận xử lý việc khác
            'và không thể xử lý yêu cầu gọi đến mô hình đối tượng của nó trong thread này
            CoRegisterMessageFilter(Me, MessageFilter)
            Dim App As Object 'Khai báo biến với kiểu đối tượng Excel.Application
            On Error Resume Next
            'Biến đổi ngược (unmarshal) từ IStream về kiểu Excel.Application để sử dụng
            HResult = CoGetInterfaceAndReleaseStream(Stream, IID_IDispatch(), App)
            If HResult = E_FAIL Then
                MsgBox("Failed to unmarshal IStream", vbExclamation, "Error")
            Else
                'Giả lập công việc nào đó mất nhiều thời gian xử lý
                Sleep(2000)
                /*
                    Nếu Excel không thể xử lý phương thức này do đang bận làm gì đó thì thủ tục IMessageFilter_RetryRejectedCall sẽ được gọi
                    nhằm thông báo cho người dùng biết lý do
                */
                App.ActiveWorkbook.Worksheets.Add()
            End If
            If Err.Number <> 0 Then MsgBox(Err.Description, vbExclamation, "Error") 'Thông báo lỗi, nếu có
            On Error GoTo 0
        End If
        CoUninitialize() 'Kết thúc sử dụng COM library
    End Sub
   
    Public Function OnGetHelloWorldButtonImage(Control As IRibbonControl) As IPictureDisp
        Return LoadResPicture("twinBASIC_icon.bmp", vbResBitmap)
    End Function
   
    Private Sub IMessageFilter_HandleInComingCall(ByVal dwCallType As CALLTYPE, ByVal htaskCaller As LongPtr, ByVal dwTickCount As Long, lpInterfaceInfo As INTERFACEINFO)
       
    End Sub
   
    Private Sub IMessageFilter_MessagePending(ByVal htaskCallee As LongPtr, ByVal dwTickCount As Long, ByVal dwPendingType As PENDINGTYPE)
       
    End Sub
   
    'Đây là nơi tiếp nhận thông điệp phàn nàn từ Excel
    Private Sub IMessageFilter_RetryRejectedCall(ByVal htaskCallee As LongPtr, ByVal dwTickCount As Long, ByVal dwRejectType As SERVERCALL)
        Select Case dwRejectType
            Case SERVERCALL_RETRYLATER 'Excel nói người dùng nên thử lại sau
                MsgBox("Excel is busy doing other task and require you to retry later.", vbInformation)
            Case SERVERCALL_REJECTED 'Excel nói nó từ chối yêu cầu gọi mô hình đối tượng của nó
                MsgBox("Excel rejected your call to its object model", vbInformation)
        End Select
    End Sub
End Class
1738945918720.png
Chạy thử add-in, khi nhấp chuột vào nút trên ribbon, code sẽ tạo ra một worksheet mới mà vẫn giữ cho giao diện người dùng của Excel không bị đơ.
1738944438906.png
Giả sử khi code đang chạy mà người dùng đang chỉnh sửa ô nào đó trong worksheet thì Excel sẽ báo bận và yêu cầu người dùng thực hiện lại.
1738944722208.png
 
Lần chỉnh sửa cuối:
Này so với mình còn mới lạ quá, nhưng nếu dùng được đa luồng, đa tác vụ thì quá tốt.
 
Này so với mình còn mới lạ quá, nhưng nếu dùng được đa luồng, đa tác vụ thì quá tốt.
Nói chung là việc sử dụng đa luồng mang lại nhiều lợi ích, tuy nhiên lập trình viên cũng phải đối mặt với nhiều rủi ro như deadlock (bế tắc, xảy ra khi hai hoặc nhiều thread phối hợp với nhau không tốt dẫn đến điều kiện nào đó không đạt được và khiến cho chương trình bị treo vĩnh viễn) và race condition (điều kiện tranh giành, xảy ra khi hai hoặc nhiều thread cùng tranh nhau ghi vào tài nguyên dùng chung, v.v.,). Ngoài ra cũng phải thực hiện đồng bộ giữa các thread để đảm bảo các thread không gây xung đột lẫn nhau.
Với lại hiện giờ TwinBasic chưa có cú pháp hỗ trợ đa luồng nên sẽ khá vất vả, phải thực hiện tất cả các bước giống như đang viết code bằng C++.
 
Web KT

Bài viết mới nhất

Back
Top Bottom