Blog

  • Big endian và Little endian

    Big endian và Little endian

    Tiếp theo bài viết “Lan man về trình biên dịch (compiled) và trình thông dịch (interpreted)”, ở bài viết này ta sẽ làm rõ hơn cái cách mà máy tính lưu trữ dữ liệu.

    Tôi thấy bài viết “LITTLE ENDIAN VS. BIG ENDIAN” rất hay nên xin phép tác giả đưa lên đây để cùng nhau tìm hiểu .

    Little endian và big endian, đây là hai phương thức khác nhau để lưu trữ dữ liệu dạng nhị phân (binary). Bình thường thì chúng ta cũng chẳng cần quan tâm đến chúng làm gì. Bởi mọi việc sẽ được tự động hoá hết.

    Thế nhưng có những tình huống, ví dụ khi phải xử lý các tập tin có cấu trúc, tập tin binary, nhất là những tập tin được ghi bằng ngôn ngữ khác, thì việc hiểu về little endian và big endian là rất quan trọng. Bởi nếu không, rất có thể chúng ta sẽ đọc sai thứ tự và xử lý với dữ liệu được hiểu sai.

    Dữ liệu

    Dữ liệu là thể hiện của thông tin dưới dạng lưu trữ được. Thông tin là thứ trừu tượng, không có hình dạng, đó là những hiểu biết về các sự vật, sự việc xung quanh chúng ta. Để lưu trữ, cũng như truyền đạt thông tin đến mọi người, chúng ta cần đến dữ liệu. Dữ liệu có thể là chữ viết, hình ảnh được ghi trên giấy, tất cả chúng ta dữ liệu mà con người có thể hiểu được.

    Nhưng những dữ liệu đó cần phải được mã hoá một lần nữa, nếu chúng ta muốn lưu trữ chúng trên máy tính. Như chúng ta đều biết, máy tính chỉ làm việc với dữ liệu được mã hoá dưới dạng nhị phân, vậy nên mọi dữ liệu cần được mã hoá thành nhị phân mới có thể xử lý trên máy tính được.

    Thực ra, máy tính không hiểu được các ký tự 0, 1 trong hệ nhị phân đâu, nó hoạt động theo các tín hiệu điện tử. Mô tả chính xác thì rất khó, nhưng chúng ta có thể hiểu “sơ sơ” rằng, gặp bit 1 thì sẽ có dòng diện, gặp bit 0 thì không có. Như vậy, các bit 0, 1 được xử lý thành các tín hiệu điện tử tương ứng, và chúng ta coi đó như máy tính đã hiểu được dữ liệu nhị phân.

    Thế nhưng, mặc dù cùng sử dụng tín hiệu dạng nhị phân, các máy tính khác nhau cũng không thực sự nói chung một ngôn ngữ. Cũng giống như coi người vậy, khi nhìn các ký tự abc có người hiểu, có người không. Máy tính khi nhìn vào các tín hiệu tương ứng với các ký hiệu 0 hay 1, mỗi máy tính có thể hiểu theo một cách khác nhau.

    Thế nhưng, rất may là các máy tính vẫn hoạt động theo những tiêu chuẩn chung, thế nên nó vẫn có thể giao tiếp với nhau được. Tuy nhiên, lưu ý rằng, không phải bất cứ lúc nào, các máy tính cũng có thể hiểu được lẫn nhau.

    Trong máy tính, các dữ liệu nhị phân không được xử lý theo từng bit riêng lẻ, mà được xử lý thành từng khối 8 bit một, và đơn vị xử lý nhỏ nhất này gọi là byte.

    Ví dụ, số nguyên 123456789 được biểu diễn dưới dạng nhị phân sẽ là (ở đây tôi cho rằng kiểu dữ liệu int sẽ có kích thước là 4 byte, tuy nhiên, nhiều hệ thống 64 bit đã nâng kích thước này lên 8 byte)

    00000111 01011011 11001101 00010101
    

    Để ngắn gọn, chúng ta có thể viết nó dưới dạng hexa như sau:

    07 5b cd 15
    

    Đã có bao giờ, bạn tự hỏi, khi ghi dữ liệu này trên đĩa cứng chẳng hạn, nó được ghi thế nào chưa. Bạn cho rằng, nó sẽ được ghi lần lượt theo thứ tự mà chúng ta đang đọc và viết ở trên, thì bạn đã nhầm.

    Đây là cách viết theo kiểu số Ả rập cho chúng ta dễ hiểu thôi, máy tính không “đọc” các ký tự giống như chúng ta nên nó cũng không lưu trữ giống cách chúng ta viết các ký tự này ra đâu. Việc ghi dữ liệu như thế nào chính là lúc little endian và big endian được dùng đến.

    Đây là cách viết theo kiểu số Ả rập cho chúng ta dễ hiểu thôi, máy tính không “đọc” các ký tự giống như chúng ta nên nó cũng không lưu trữ giống cách chúng ta viết các ký tự này ra đâu. Việc ghi dữ liệu như thế nào chính là lúc little endian và big endian được dùng đến.

    Little endian và big endian là gì?

    Little endian và big endian là hai phương thức khác nhau để lưu trữ dữ liệu. Sự khác biệt của little endian và big endian khi lưu trữ chính là ở việc sắp xếp thứ tự các byte dữ liệu.

    Trong cơ chế lưu trữ little endian (xuất phát từ “little-end” nghĩa kết thúc nhỏ hơn), byte cuối cùng trong biểu diễn nhị phân trên sẽ được ghi trước. Ví dụ 123456789 ghi theo kiểu little endian sẽ thành

    15 cd 5b 07
    

    Hơi ngược một chút đúng không? Big endian (xuất phát từ “big-end”) thì ngược lại, là cơ chế ghi dữ liệu theo thứ tự bình thường mà chúng ta vẫn dùng. 123456789 được lưu trữ vẫn theo đúng thứ tự là

    07 5b cd 15
    

    Các thuật ngữ big-end hay little-end xuất phát từ cuốn tiểu thuyết Gulliver du ký (Gulliver’s Travels), trong đó nhân vật Lilliputans tranh luận về việc nên đập trứng bằng đầu to hay nhỏ.

    Và ngành IT đã ứng dụng thuật ngữ ngày, tương đối giống với nghĩa gốc. Lưu ý rằng, little endian hay big endian chỉ khác nhau ở cách sắp xếp các byte dữ liệu, còn thứ tự từng bit trong byte thì giống nhau. Rất may, các máy tính vẫn có điểm trung này.

    Thêm một lưu ý nữa rằng, little endian hay big endian chỉ khác biệt khi cần lưu trữ những dữ liệu có nhiều byte. Những dữ liệu chỉ có 1 byte (ví dụ ký tự ASCII) thì không ảnh hưởng gì (chính xác là dù dùng phương thức nào kết quả cũng như nhau).

    Little endian và big endian được dùng trên những máy tính nào?

    Việc sắp xếp các byte dữ liệu theo kiểu little endian hay big endian không chỉ xảy ra khi chúng ta lưu trữ dữ liệu ra bộ nhớ ngoài. Mọi hoạt động của máy tính đều sử dụng dữ liệu nhị phân, nên little endian/big endian hiện hữu trong mọi hoạt động của máy tính.

    Ngoài việc sử dụng little endian/big endian một phần phụ thuộc vào phần mềm (do lập trình viên cố ý sử dụng một trong hai loại, hoặc ngôn ngữ lập trình quy định trước), nó còn phụ thuộc vào bộ vi xử lý của chính máy tính đó.

    Các bộ vi xử lý Intel đều sử dụng little endian, các bộ vi xử lý cả ARM trước đây cũng là little endian, nhưng hiện này ARM đã nâng cấp vi xử lý của mình thành bi-endian (tức là xử lý cả little endian và big endian).

    Các bộ vi xử lý PowerPC và SPARK trước đây đều là big endian, nhưng hiện nay chúng cũng được nâng cấp thành bi-endian.

    Các làm nào thì tốt hơn: little endian hay big endian?

    Little endian hay big endian cũng như tranh luận gốc về việc đập trứng, không có một phương thức nào thực sự tốt hơn phương thức nào.

    Little endian hay big endian chỉ khác nhau ở việc lưu trữ thứ tự các byte dữ liệu. Cả hai phương thức đều không làm ảnh hưởng đến tốc độ xử lý của CPU. Thế nên cả hai phương thức đều vẫn tồn tại song song và sẽ không bao giờ có thể có một câu trả lời thoả đáng: Phương thức nào thì tốt hơn?

    Mỗi phương thức đều có những lợi thế nhất định. Với little endian, vì byte nhỏ nhất luôn nằm bên trái, nó sẽ cho phép chúng ta đọc dữ liệu với độ dài tuỳ ý. Nó sẽ rất thích hợp nếu chúng ta cần ép kiểu, ví dụ từ int thành long int.

    Với giả định int là 4 byte, long int là 8 byte, nếu dùng little endian, khi ép kiểu, địa chỉ bộ nhớ không cần phải thay đổi, chúng ta chỉ cần ghi tiếp các byte lớn hơn mà thôi.

    Nhưng nếu cũng trường hợp đó, mà sử dụng big endian, thì chúng ta sẽ phải dịch địa chỉ bộ nhớ hiện tại thêm 4 byte nữa mới có không gian để lưu trữ.

    Nhưng big endian cũng có nhưng lợi thế nhất định, với việc đọc dữ liệu byte lớn nhất trước, nó sẽ rất dễ dàng kiểm tra một số là âm hay dương, do byte chứa dấu được đọc đầu tiên.

    Xem các byte dữ liệu trong bộ nhớ

    Chương trình C đơn giản nhau cho chúng ta cách nhìn về việc sắp xếp các byte trong bộ nhớ.

    #include <stdio.h>
    
    /* function to show bytes in memory, from location start to start+n */
    void
    show_mem_rep (char *start, int n)
    {
      int i;
      for (i = 0; i < n; i++)
        printf (" %.2x", start[i]);
      printf ("\n");
    }
    
    /* Main function to call above function for 0x01234567 */
    int
    main ()
    {
      int i = 0x01234567;
      show_mem_rep ((char *) &i, sizeof (i));
      return 0;
    }
    

    Khi thực thi chương trình trên, nếu máy của bạn là little endian thì kết quả sẽ là

     67 45 23 01
    

    còn nếu máy bạn là big endian thì nó sẽ hiển thị theo thứ tự thông thường

     01 23 45 67
    

    Có cách nào để xác định máy tính của chúng ta là little endian hay big endian hay không? Có vô số các cách khác nhau, dưới đây là một trong số những cách đó:

    #include <stdio.h>
    
    int
    main ()
    {
      unsigned int i = 1;
      char *c = (char *) &i;
      if (*c)
        printf ("Little endian");
      else
        printf ("Big endian");
      return 0;
    }
    

    Với đoạn code đơn giản trên, c là con trỏ, nó trỏ đến vùng nhớ của biến i là một số nguyên. Bởi vì số nguyên là kiểu dữ liệu nhiều byte, trong khí dữ liệu của char chỉ là một byte mà thôi, nên *c sẽ trả về giá trị là byte đầu tiên của số nguyên i.

    Nếu máy tính của chúng ta là little endian thì byte đầu tiên này sẽ là 1, ngược lại thì nó sẽ là 0.

    Điều này ảnh hưởng thế nào đến việc lập trình

    Về cơ bản thì little endian hay big endian không có ảnh hưởng lắm đến việc lập trình. Phần lớn các lập trình viên không cần quan tâm nhiều lắm, bởi mọi việc đã được các trình biên dịch/thông dich đảm nhiệm hết.

    Tuy nhiên, một số trường hợp, chúng ta cần quan tâm, đặc biệt khi chuyển đổi dữ\ liệu giữa các máy tính khác nhau. Ví dụ: khi chúng ta cần xử lý một file có cấu trúc thế này, 4 byte đầu tiên là một số nguyên n, sau đó là n số nguyên, mỗi số chiếm 4 byte bộ nhớ, v.v…

    Trong trường hợp này, khi nhận file được tạo ra từ một máy tính khác, việc nó được ghi theo kiểu little endian hay big endian rõ ràng là ảnh hưởng rất nghiêm trọng, nếu sử dụng sai phương thức, chúng ta sẽ thu về dữ liệu sai.

    Một trường hợp khác nữa có thể xảy ra vấn đề là khi chúng ta ép kiểu cho các biến

    #include <stdio.h>
    
    int
    main ()
    {
      unsigned char arr[2] = { 0x01, 0x00 };
      unsigned short int x = *(unsigned short int *) arr;
      printf ("%d", x);
    
      return 0;
    }
    

    Trong đoạn code trên, chúng ta đã ép kiểu một array hai phần tử char thành một số nguyên 2 byte (short int). Trong ví dụ này, little endian hay big endian cũng có ảnh hưởng rất lớn.

    Một máy tính dùng little endian sẽ có kết quả là 1 trong khi big endian sẽ cho kết quả là 256. Để tránh những lỗi đáng tiếc có thể xảy ra, những code như trên cần phải tránh.

    NUXI là một vấn đề rất nổi tiếng liên quan đến little endian và big endian: UNIX được lưu trong một hệ thống big-endian sẽ được hiểu là NUXI trong một hệ thống little endian.

    Giả sử chúng ta cần lưu trữ 4 byte (UNIX) bằng hai số nguyên dạng short intUN và IX.

    #include <stdio.h>
    
    int
    main ()
    {
      short int *s; // pointer to set shorts
      s = (short int *)malloc(sizeof(short int));    // point to location 0
      *s = "UN";  // store first short: U * 256 + N (fictional code)
      s += 2;    // point to next location
      *s = "IX";  // store second short: I * 256 + X
    
      return 0;
    }
    

    Đoạn code trên hoàn toàn độc lập với hệ thống, bất kể nó là little hay big endian. Nếu chúng ta lưu trữ các giá trị “UN” và “IX” khi đọc ra, nó vẫn sẽ là “UNIX” hay không? Nếu mọi việc chỉ xảy ra trên một máy tính, dù là big endian hay little endian thì nó sẽ luôn là như vậy, bởi mọi thứ sẽ được tự động hoá giúp chúng ta.

    Với bất cứ dữ liệu nào cũng vậy, chúng ta luôn thu được dữ liệu đúng nếu đọc và ghi trong cùng một hệ thống. Thế nhưng, hãy xem xét kỹ hơn về việc sắp xếp các byte trong bộ nhớ.

    Một hệ thống big endian sẽ lưu trữ như sau:

    U N I X
    

    Còn một hệ thống little endian thì sẽ như sau:

    N U X I
    

    Mặc dù trông hơi ngược nhưng hệ thống little endian sẽ xử lý việc đọc giúp chúng ta, nên lưu trữ như vậy nhưng khi lấy ra chúng ta vẫn có dữ liệu ban đầu. Thế nhưng khi chúng ta ghi dữ liệu này ra file, chuyển sang một máy tính khác. Và mỗi máy tính lại xử lý theo cách riêng của nó thì UNIX trên máy big endian sẽ được hiểu là NUXI trên máy little endian (và ngược lại).

    Đây chính là vấn đều nguy hiểm nhất khi chúng ta trao đỏi dữ liệu qua lại giữa các máy tính với nhau, đặc biệt trong thời đại Internet ngày nay.

    Trao đổi dữ liệu giữa các máy có endian khác nhau

    Ngày nay, mọi máy tính đều được kết nối để trao đổi dữ liệu với nhau. Little endian hay big endian cũng đều phải trao đổi với nhau, nhưng làm thế nào để có hiểu được nhau khi chúng không nói chung một thứ tiếng?

    Có 2 giải pháp chính cho việc này

    Sử dụng chung định dạng

    Một phương án đơn giản nhất tất cả sử dụng chung một định dang khi truyền dữ liệu.

    Ví dụ những tập tin dạng PNG đều bắt buộc phải sử dụng big endian. Tương tự với các tập tin có cấu trúc khác. Đó là lý do vì sao chúng ta nhiều khi cần phải dùng những phần mềm chuyên dụng để đọc và ghi các file này.

    Thế nhưng trong kết nối với Internet, việc truyền dữ liệu còn phức tạp hơn thế. Chúng ta không thể cứ dùng một định dạng file nào đó, rồi truyền từng byte một sang máy khác được. Muốn tăng tốc độ, bắt buộc chúng ta phải truyền nhiều byte một lúc.

    Và khi đó chúng ta cần có một chuẩn chung. Hiện nay, chuẩn chung cho việc truyền dữ liệu trên mạng, gọi là network byte order chính là big endian. Thế nhưng, dù đã chuẩn chung rồi, thỉnh thoảng vẫn có những giao thức chơi chội hơn, sử dụng little endian.

    Để có thể chuyển đổi dữ liệu thành dữ liệu chuẩn theo network byte order, chương trình cần gọi hàm hton* (host-to-network) (trong ngôn ngữ C). Trong hệ thống big endian, hàm này không cần làm gì cả, còn little endian sẽ thực hiện chuyển đối các byte một chút.

    Dù hệ thống big endian không cần chuyển đổi dữ liệu, việc gọi hàm này vẫn là rất cần thiết. Chương trình của chúng ta có thể được viết bằng một ngôn ngữ (C) nhưng có thể được dịch và thực thi ở nhiều hệ thống khác nhau, việc gọi hàm này sẽ giúp chúng ta làm điều đó.

    Tương tự, ở chiều ngược lại, chúng ta cần gọi hàm ntoh* để chuyển đổi dữ liệu nhận được từ mạng về dữ liệu máy tính có thể hiểu được. Ngoài ra, chúng ta còn phải hiểu rõ kiểu dữ liệu mà chúng ta cần chuyển đổi nữa, danh sách các hàm chuyển đổi như sau:

    • htons – “Host to Network Short”
    • htonl– “Host to Network Long”
    • ntohs – “Network to Host Short”
    • ntohl – “Network to Host Long”

    Những hàm này vô cùng quan trọng khi thực hiện chia sẽ dữ liệu ở tầng thấp, ví dụ khi kiểm tra checksum của các gói tin chẳng hạn. Nếu không hiểu rõ về little endian và big endian thì khi cần làm việc về mạng, bạn sẽ gặp nhiều khó khăn.

    Sử dụng BOM (Byte Order Mark)

    Một phương án khác để giải quyết sự khác biệt về endian là sử dụng BOM (Byte Order Mark). Đây là một ký tự đặc biệt, có giá trị là 0xFEFF, được ghi ở vị trí đầu tiên của file.

    Nếu bạn đọc ký tự này là 0xFFFE (bị ngược) thì có nghĩa file này được ghi với endian khác với hệ thống của bạn, khi đó, bạn sẽ cần phải thay đổi phương thức đọc dữ liệu một chút.

    Có một vài vấn đề nhỏ với việc sử dụng BOM. Thứ nhất, BOM sẽ gây tăng dữ liệu được ghi vào file. Ngay cả khi chúng ta chỉ gửi đi 2 byte dữ liệu, chúng ta vẫn cần thêm 2 byte BOM nữa.

    Thứ hai, BOM không hoàn toàn thần thánh, bởi nó phụ thuộc vào lập trình viên. Có người có tâm thì đọc và xử lý khi gặp BOM, có người thì hoàn toàn bỏ quên nó và coi nói như dữ liệu thông thường. Unicode sử dụng BOM khi lưu trữ dữ liệu nhiều byte (nhiều ký tự Unicode được mã hoá thành 2, 3 thậm chí là 4 byte).

  • Swift: Map, Flat Map, Filter and Reduce

    Swift: Map, Flat Map, Filter and Reduce

    Xin chào mọi người.
    Trong swift có một số tính năng rất hay đó là Higher Order Function. Nó có một số hàm như là map, CompactMap, Filter and Reduce được sử dụng cho các kiểu dữ liệu dạng collection.

    Khởi tạo giá trị mẫu

    struct Person {
        let name: String
        let age: Int
        let pets: [String]
    }
    struct Pet {
        let name: String
        let age: Int
    }
    
    var peopleArray = [Person(name: "Jack", age: 11, pets: ["Dog", "Cat"]),
                       Person(name: "Queen", age: 12, pets: ["Pig"]),
                       Person(name: "King", age: 13, pets: [])]

    MAP

    Trước khi sử dụng chúng ta cùng tìm hiểu về syntax của hàm này trước nhé:

    let resultCollection = inputCollection.map { (elementOfCollection) -> ResultType in
       return ResultType()
    }

    Nhìn vào đoạn code trên ta có thể hiểu hàm này sẽ trả về cho ta một collection có kiểu dữ liệu là ResultType( Một kiểu dữ liệu bất kì mà bạn mong muốn, nó có thể là Int, Double, String …)

    OK, Giờ chúng ta đi vào code mẫu để dễ hiểu hơn

    Dạng đầy đủ:

    var ages = peopleArray.map { (person) -> Int in
        return person.age
    }
    print(ages)
    // OUTPUT: [11, 12, 13]

    Ngoài dạng thông thường thì hàm này còn có thể viết dưới dạng rút gọn như sau:

    let ages = peopleArray.map({ $0.age })
    print(ages)
    // OUTPUT: [11, 12, 13]

    $0: ở đây được hiểu là argument đầu tiên của function map, trong trường hợp này nó sẽ đại diện cho 1 phần tử trong mảng có kiểu dữ liệu là Person

    Ứng dụng:
    Hàm này nên được sử dụng khi mà bạn muốn tạo một collection mới có kiểu dữ liệu khác từ 1 collection hiện tại

    Flat Map

    let resultCollection = inputCollection.flatMap { (elementOfCollection) -> [ResultType] in
       return [ResultType]
    }

    Cũng giống như hàm map, hàm flatMap cũng trả về 1 collection nhưng flat map sẽ bỏ qua các tầng (nếu có).
    Để dễ hiểu hơn chúng ta sẽ đi vào ví dụ sau:

    Sử dụng hàm Map:

    let pets = peopleArray.map({ $0.pets })
    print(pets)
    //OUTPUT: [["Dog", "Cat"], ["Pig"], []]

    Dạng đầy đủ:

    let flatPets = peopleArray.flatMap { (person) -> [String] in
        return person.pets
    }
    print("Flat pets: \(flatPets)")
    // OUTPUT: ["Dog", "Cat", "Pig"]

    Dạng rút gọn:

    let flatPets = peopleArray.flatMap({ $0.pets })
    print("flatPets: \(flatPets)")
    // OUTPUT: flatPets: ["Dog", "Cat", "Pig"]

    Có thể thấy flat map sẽ loaị bỏ hết các tầng collection bên trong và chuyển về 1 collection chỉ còn 1 tầng duy nhất thay vì collection chứa collection như khi sử dụng hàm Map.
    Ngoài ra hàm flatMap cũng bỏ đi các giá trị collection empty hoặc nil.

    NOTE: Hàm flatMap chỉ trả về giá trị đúng như mong đợi khi kiểu của nó là non-optional.


    Vậy trong trường hợp khai báo kiểu optional thì sao: let pets: [String]?
    Trong trường hợp này hàm Flat Map sẽ trả về giá trị như hàm Map vì vậy chúng ta sẽ call flatMap thêm 1 lần nữa như sau:

    let flatPetsShort = peopleArray.flatMap({ $0.pets }).flatMap({ $0 })
    print("flatPetsShort: \(flatPetsShort)")
    //OUTPUT: flatPetsShort: ["Dog", "Cat", "Pig"]

    Ứng dụng:
    Flat Map thường sử dụng trong trường hợp lọc các giá trị nil ra khỏi collection hoặc chuyển 1 collection nhiều tầng thành collection 1 tầng

    Reduce

    Cấu trúc hàm Reduce

    let result = inputCollection.reduce(initialValue) { (result, nextElement) -> ResultType in
        return a value that can be computed in the next element.
    }

    Hàm reduce sẽ duyệt lần lượt các phần tử trong collection và trả về kết quả dựa trên initialValue(Giá trị khởi tạo) và phép tính ở hàm return.

    Bây giờ chúng ta đi vào ví dụ để dễ hiểu hơn:

    Bài toán cụ thể: Cần tính tổng số tuổi của mọi người trong collection bằng hàm Reduce thì chúng ta sẽ làm như sau:

    Dạng đầy đủ:

    let ageTotal = peopleArray.reduce(0) { (result, personNext) -> Int in
        return result + personNext.age
    }
    print(ageTotal)
    // OUTPUT: 36

    Dạng rút gọn:

    let ageTotal2 = peopleArray.map({ $0.age }).reduce(0, +)
    print(ageTotal2)
    // OUTPUT: 36

    Ở dạng rút gọn, để sử dụng được hàm Reduce trong trường hợp này. Chúng ta cần sử dụng hàm Map để tạo ra một collection mới, chứa các phần tử là tuổi của tất cả mọi người trước, rồi sử dụng hàm Reduce dạng rút gọn để thực hiện.

    Ứng dụng:
    Bạn nên sử dụng hàm Reduce khi bạn muốn kết hợp các phần tử trong collection

    Filter

    Cấu trúc hàm Filter

    let result = inputCollection.filter { (elementOfCollection) -> Bool in 
        return (Conditions)
    }

    Hàm Filter sẽ trả về kết quả là 1 collection chứa tất cả các phần tử thỏa mãn điều kiện (Conditions)

    Bây giờ chúng ta sẽ đi vào ví dụ cho dễ hiểu hơn:
    Bài toán của chúng ta là cần lọc ra được tất cả các Person có số tuổi > 11.

    Chúng ta sẽ sử dụng hàm Filter dạng đầy đủ như sau:

    let result = peopleArray.filter { (person) -> Bool in
        return person.age > 11
    }
    print("result: \(result)")

    Dạng rút gọn:

    let results = peopleArray.filter({ $0.age > 11 })
    print(results)

    Ứng dụng:
    Sử dụng khi bạn cần giải quyết bài toán lọc các phần tử trong collection có điều kiện xác định

    NOTE:

    print("Cảm ơn mọi người đã theo dõi bài viết của mình.\n
    Mọi đóng góp cũng như góp ý, mọi người hãy comment ở phía dưới để mình có thể hoàn thiện bài viết tốt hơn.") 

    THANK YOU!

  • Java Vector vs Arraylist: The Performance

    Java Vector vs Arraylist: The Performance

    Chắc hẳn anh em làm việc với Java sẽ quen thuộc với VectorArrayList. Cả hai đều implement interface List và có cấu trúc dữ liệu dạng dynamically resizeable arrays, giống như một mảng thông thường.

    Cốt thì đơn giản như sau

    ArrayList<T> al = new ArrayList<T>();
    Vector<T> v = new Vector<T>();
    

    Tuy nhiên thì vẫn có những điểm khác nhau giữa VectorArrayList:

    • Synchronization: Về cơ bản Vector Synchronized, tức là tại một thời điểm, một và chỉ 1 thread có thể sử dụng Vector, trong khi đó Array List thì không, và nhiều thread có thể làm việc trên cùng một Array List. Ví dụ: một thread có thể thực hiện việc thêm (add), và một thread khác có thể thực hiện việc remove ở trong một môi trường multithreading.

      Nếu nhiều thread truy cập mảng Array List đồng thời, thì chúng ta phải synchronize code block nếu block đó sửa cấu trúc(thêm hoặc xóa (các) phần tử). Re-assign value không làm thay đổi cấu trúc. Nói cách khác thì Vector thread-safe còn Array List sẽ đạt được thread-safe bằng synchronized.

    • Data Growth: Cả hai đều có thể quản lý dữ liệu động tức là ** grow and shrink dynamically** và tối ưu hoá việc sử dụng bộ nhớ. Khác biệt lớn nhất là size, cả 2 đều có capacity tức là số lượng phần tử tối da tại một thời điểm, và mặc định là 10 (kể cả khi cả 2 đều rỗng)

    ArrayList() Constructs an empty list with an initial capacity of ten. https://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html

    Vector() Constructs an empty vector so that its internal data array has size 10 and its standard capacity increment is zero. https://docs.oracle.com/javase/7/docs/api/java/util/Vector.html

    Vấn đề là capacity của ArrayList sẽ tăng 50% khi số lượng phần tử vượt quá capacity còn vector là 100%

    • Traversal: Vector có thể sử dụng cả EnumerationIterator để traversing trong khi ArrayList chỉ có thể sử dụng Iterator,
    • Performance: Về cơ bản thì Array List sẽ nhanh hơn, vì nó non-synchronized, còn Vector thì synchronized(thread-safe). Nếu một thread hoạt động trên một Vector, thì nó sẽ có lock, các thread khác sẽ cần chờ lock để làm việc.

    Đo Vector vs ArrayList

    Nói chung là cần test, mình đã chuẩn bị một bài test cho ArrayList và Vector, cốt ở đây nhé. Bài này mình sử dụng OpenJDK JMH để thực hiện benchmark và thực hiện ở 2 bài toán: add và traversal. Đây là kết quả.

    Về cơ bản thì ArrayList mạnh hơn ở cả 2 bài test vì non-thread-safe. Tuy nhiên để cân nhắc chúng ta sẽ thấy:

    • Với bài toán single-thread, ArrayList là một lựa chọn hiển nhiên, nhưng với multithreading rõ ràng Vector có sự ưu tiên hơn.
    • Nếu chúng ta không biết hoặc không áng chừng được lượng data trong Collection, nhưng biết được tốc độ tăng của lượng dữ liệu, Vector lợi thế hơn, vì với Vector chúng ta có thể quản lý được increment value của capacity.
    • Vector là một legacy class, ArrayList thì mới hơn nên nhanh hơn 🙂
  • Grand Central Dispatch (Part 3)

    Grand Central Dispatch (Part 3)

    Nội dung bài viết:

    • DispatchBarrier
    • DispatchWorkItem
    • Thread Sanitizer

    Dispatch Barrier

    • 1 trong những vấn đề tiêu biểu của đa luồng như đã đề cập ở phần 1 của bài viết là Race Condition.
    • Có 1 cách đơn giản để tránh Race Condition, đó là dùng serial queue để thực hiện các task, khi đó chỉ được 1 task được thực hiện tại mỗi thời điểm, nhưng sẽ làm app chậm lại vì chạy trên serial queue.
    • Vậy nếu bạn muốn các task được thực hiện concurrent, nhưng khi có 1 task thay đổi tài nguyên chung thì chỉ duy nhất task đó được thực hiện tại thời điểm đó?

    Đừng lo, GCD cung cấp cho bạn DispatchBarrier để xử lí điều đó 1 cách đơn giản.

    Note: Khi 1 task tiến hành việc thay đổi share resources, sẽ có 1 barrier đến, ngăn không cho bất kì 1 task mới nào được thực hiện trong thời gian này. Khi task đó hoàn thành việc thay đổi tài nguyên, barrier mất đi, các tasks tiếp tục chạy concurrent.

    Ví dụ sau đây là cách sử dụng Dispatch Barrier để tránh Race Condition.
    Giả sử bạn có 1 class Person như sau:

    Dưới đây là đoạn code nhiều task khác nhau cùng thay đổi share resources.

    let queue = DispatchQueue(label: "01", attributes: .concurrent)
    let nameChangeGroup = DispatchGroup()
    
    let person = Person(firstName: "Hoang", lastName: "Tuan")
    let nameList = [("A","B"),("C","D"),("E","F"),("G","H"),("I","J")]
    
    for (_, name) in nameList.enumerated() {
        queue.async(group: nameChangeGroup) {
            usleep(UInt32(10000 * idx))
            person.changeName(firstName: name.0, lastName: name.1)
            print("Current name: \(person.name)")
        }
    }
    
    nameChangeGroup.notify(queue: .main) {
        print("Final name: \(person.name)")
    }

    Và đây là kết quả được hiển thị:

    Có thể dễ dàng thấy, các task thực hiện thay đổi tài nguyên cùng thời điểm gây ra sai lệch kết quả. Đồng thời, kết quả đều khác nhau sau mỗi lần Run.

    Đó là lúc chúng ta sẽ sử dụng Dispatch Barrier để xử lí việc này.

    1. Thực hiện việc get name theo sync để không cho các task khác có thể đọc tài nguyên khi có 1 task đang đọc, vì có thể task đang đọc có thể sẽ chỉnh sửa tài nguyên sau đó.
    2. Đưa toàn bộ thân hàm của func changeName vào thực hiện trên luồng isolationQueue1 với flags là 1 barrier. Vì vậy, khi excute changeName, 1 barrier sẽ xuất hiện và không để bất kì task mới nào được thực hiện cho đến khi changeName hoàn thành.

    Và đây là kết quả:

    Kết quả khi đó được thực hiện đúng

    DispatchWorkItem

    DispatchWorkItem là những block of code. Điều khác biệt khi sử dụng DispatchWorkItem là bạn có thể cancel những task đang trong queue.

    Note: Bạn chỉ có thể cancel 1 DispatchWorkItem trước khi nó đi đến đầu queue và bắt đầu execute.

    Khởi tạo 1 DispatchWorkItem:

    let workItem = DispatchWorkItem(qos: .background, flags: .inheritQoS) {
         guard let url = URL(string: self.links[i]) else {
             return
         }
         URLSession.shared.dataTask(with: url) { (data, res, err) in
             guard let imageData = data else {
                 return
             }
             let myImage = UIImage(data: imageData)
             DispatchQueue.main.async {
                 self.listImage[i].image = myImage
             }
         }.resume()
    }

    Đưa DispatchWorkItem vào queue để thực hiện:

    DispatchQueue.global().async(execute: workItem)

    Cancel 1 DispatchWorkItem:

    workItem.cancel()

    Thread Sanitizer

    Sử dụng Thread Sanitizer để phát hiện & debug race condition:

    • Chọn Product>Scheme>Edit Scheme
    • Chọn vào "Thread Sanitizer":

    Khi đó, nếu code của bạn bị race condition, xcode sẽ thông báo cho bạn:

    1 tool khá hay để phát hiện race condition

    Kết luận: GCD là 1 API đỉnh và dễ sử dụng để quản lí multitask, đa luồng, nhưng có 1 nhược điểm là không cung cấp các state của task trong trường hợp bạn muốn quản lí sâu hơn. Khi đó, bạn phải tự custom State cho riêng mình, hoặc dùng Operation.

    -End-

  • Grand Central Dispatch (Part 2)

    Grand Central Dispatch (Part 2)

    Nội dung bài viết:

    • Dispatch Group
    • Semaphores

    Dispatch Group (cont):

    Ở phần trước, bạn đã biết về cơ chế enter-leave-notify của DispatchGroup để nhận biết khi các task trong group đã hoàn thành.
    Vậy nếu bạn muốn block luồng hiện tại cho đến khi các tasks trong group hoàn thành hoặc timeout thì sao? Ngoài cách sử dụng 1 vài logic, thì cách đơn giản hơn là dùng phương thức wait do DispatchGroup cung cấp.

    Phương thức wait của DispatchGroup là 1 sync function nên nó có tác dụng block luồng hiện tại cho đến khi mọi tasks trong group đã được thực hiện xong, hoặc timeout.

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    var formatter: DateFormatter = DateFormatter()
    formatter.dateFormat = "HH:mm:ss"
    
    // 1
    print("Start task 1 at time: \(formatter.string(from: Date()))")
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(10))
        print("End task 1 at time: \(formatter.string(from: Date()))")
    }
    
    // 2
    print("Start task 2 at time: \(formatter.string(from: Date()))")
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(2))
        print("End task 2 at time: \(formatter.string(from: Date()))")
    }
    
    // 3
    print("Start call time out at time: \(formatter.string(from: Date()))")
    if group.wait(timeout: .now() + 5) == .timedOut {
        print("Time out at time: \(formatter.string(from: Date()))")
    } else {
        print("All the jobs have completed at time: \(formatter.string(from: Date()))")
    }
    
    // 4
    queue.async(group: group) {
        print("Try to do task 3 at time: \(formatter.string(from: Date()))")
    }
    1. Bạn khởi tạo task đầu tiên, đưa vào queue để thực hiện theo cách async.
    2. Bạn khởi tạo task thứ 2, đưa vào queue để thực hiện theo cách async.
    3. Bạn khai báo hàm wait() với timeOut = 5s. Hàm sẽ block luồng hiện tại cho đến khi các task thực hiện xong hoặc sau 5s.
    4. Khởi tạo 1 task thứ 3, đưa vào queue để thực hiện theo cách async
    Mang đoạn code vào playground, thử sửa thời gian để hoàn thành task 1 thành 1s, xem điều gì sẽ xảy ra 😉
    • Sau 2s kể từ lúc bắt đầu, task 2 hoàn thành
    • Khi gọi wait() thì luồng hiện tại sẽ bị block -> Không thực hiện được tiếp task thứ 3 trong khoảng thời gian đó.
    • Sau 5s kể từ lúc Start time out, các task trong group chưa hoàn thành hết do task 1 cần 10s để thực hiện, vì vậy nên dispatchGroup sẽ trả quyền điều khiển về cho luồng.
    • Ngay sau khi luồng được trả quyền điều khiển, luồng thực hiện ngay lập tức task 3.
    • Sau 10s kể từ lúc bắt đầu, task 1 hoàn thành.

    Note: Vì wait() block luồng gọi nó, nên bạn không bao giờ nên gọi wait ở main queue.

    Semaphores

    • Giả sử bạn có nhiều task cùng truy cập vào 1 tài nguyên chung, và bạn muốn giới hạn số lượng task truy cập vào tài nguyên chung đó tại mỗi thời điểm. -> GCD cung cấp cho bạn Semaphore để giúp bạn xử lí việc này dễ dàng.
    • semaphore cung cấp 2 phương thức là wait()signal(). Chúng tương tự như enter() và leave() của dispatchGroup. wait() sẽ gọi khi bắt đầu 1 task và gọi signal() để thông báo rằng task đã hoàn thành.

    Giả sử bạn có 10 task cùng thực hiện 1 lúc, nhưng bạn muốn chỉ có tối đa 4 task chạy tại 1 thời điểm( ví dụ như load ảnh) -> Semaphore sẽ giúp bạn

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    // 1
    let semaphore = DispatchSemaphore(value: 4)
    
    // 2
    for i in 1...10 {
        // 3
        queue.async(group: group) {
            // 4
            semaphore.wait()
            // 5
            defer {
                semaphore.signal()
            }
            print(" Start task \(i) at time: \(Date().timeIntervalSince1970)")
    
            Thread.sleep(forTimeInterval: 3)
            print("Finish task \(i) at time: \(Date().timeIntervalSince1970)")
        }
    }
    1. Khởi tạo 1 semaphore, giá trị value biểu thị cho số lượng task tối đa chạy trong 1 thời điểm.
    2. Tạo 1 vòng lặp gồm 10 task chạy.
    3. Mỗi task sẽ chạy theo kiểu async.
    4. gọi hàm wait() để thông báo là task bắt đầu chạy.
    5. gọi signal() để thông báo là task đã hoàn thành.
    Mang đoạn code vào playground, thử không gọi signal() hoặc wait() xem điều gì sẽ xảy ra
    • Task 1, 2, 3, 4 bắt đầu được chạy đầu tiên.
    • Khi task 2, 3, 1 hoàn thành, task 5, 6, 7 ngay lập tức được thực hiện.
    • Task 4 hoàn thành, task 8 ngay lập tức được thực hiện.

      -> Có thể thấy, tại mỗi thời điểm chỉ có tối đa 4 task chạy.

    Note:

    • Luôn nhớ gọi hàm signal() khi task được thực hiện xong, nếu không thì semaphore sẽ không biết task đã hoàn thành để thực hiện task mới.
    • Gọi wait() khi bắt đầu 1 task để thông báo cho semaphore rằng 1 task mới sẽ bắt đầu thực hiện.

    Ở bài viết tiếp theo, mình sẽ giới thiệu về DispatchBarrier, DispatchWorkItem và Thread Sanitizer.

    Nguồn tham khảo: Ray Wenderlich & Lets Build that app.

  • Grand Central Dispatch (Part 1)

    Grand Central Dispatch (Part 1)

    Tổng quan:

    Để app có UI, UX mượt mà và nhanh, ta cần chia các tasks cần nhiều thời gian chạy ra các luồng khác để thực hiện -> GCD là 1 trong các cách giúp các bạn quản lí các luồng đó.

    Nội dung bài viết:

    • 1 số khái niệm cơ bản về luồng, hàng đợi.
    • Cách tạo luồng bằng GCD.
    • Các vấn đề hay gặp phải trong quản lí luồng.
    • Các basic func của GCD.

    Giới thiệu 1 số khái niệm cơ bản về luồng, hàng đợi:

    Thread

    • Luồng là nơi các task được thực hiện.
    • 1 app hoặc 1 chương trình sẽ có 1 hoặc nhiều luồng.
    • Mỗi luồng có thể thực hiện đồng thời, tuy nhiên nó tùy thuộc vào hệ thống để biết khi nào nó được thực hiện và được thực hiện thế nào.
    • GCD được xây dựng dựa trên các luồng. Với GCD, bạn đưa các task vào dispatch queue và GCD sẽ tự quyết định luồng nào được dùng để thực hiện các task.

    Queue

    • Queue là hàng đợi, hoạt động theo cơ chế FIFO.
    • Bạn đưa các task vào queue, và GCD sẽ thực hiện chúng theo cơ chế FIFO.
    • Bản thân Dispatch Queues đã là thread safe nên bạn có thể đưa task vào queue từ bất kì luồng nào.
    • Vì GCD sẽ tự động chọn luồng để thực hiện các task giúp bạn, nên việc của bạn là chỉ cần chọn loại queue phù hợp để đưa task vào.

    Có 2 loại queue là SerialConcurrent.
    Serial: hàng đợi chỉ thực hiện 1 task vào 1 thời điểm, task này xong thì thực hiện task tiếp theo.
    Concurrent: hàng đợi thực hiện nhiều task tại 1 thời điểm, các task vẫn được thực hiện theo thứ tự FIFO, nhưng bạn sẽ không biết số task thực hiện tại 1 thời điểm hay thời điểm các task hoàn thành.

    Serial Queue và Concurrent Queue

    Với GCD, bạn có thể thực hiện các task async hoặc sync.
    sync: 1 task được thực hiện kiểu synchronous sẽ trả quyền điều khiển cho hàm gọi chúng sau khi task được hoàn thành.
    async: 1 task được thực hiện kiểu asynchronus sẽ trả quyền điều khiển ngay lập tức mà không cần đợi phải hoàn thành. Vì vậy, 1 async task sẽ không block luồng hiện tại. Luồng hiện tại sẽ được trả quyền điều khiển để thực hiện task tiếp theo.

    Cách tạo queue với GCD:

    1. Khởi tạo: GCD cung cấp cách khởi tạo luồng với các quality of Service khác nhau như sau:

    Swift gồm 5 loại quality of Service được sử dụng cho các mục đích khác nhau:

    Cách gọi ra Main queue – đây là nơi để bạn thực hiện các task updates UI:

    DispatchQueue.main

    Cách tạo ra 1 private queue: (mặc định là serial queue)

    let serialQueue = DispatchQueue(label: "techOver")

    Để tạo ra 1 queue kiểu concurrent thì bạn set thuộc tính attribute kiểu concurrent:

    let serialQueue = DispatchQueue(label: "techOver", attributes: .concurrent)

    2. Thêm task vào các queue với kiểu thực hiện sync hoặc async:

    DispatchQueue.global().async {
       // do expensive non-UI task
       DispatchQueue.main.async {
          // do updates UI when task is finished
       }
    }

    Các vấn đề hay gặp về quản lí luồng:

    priority inversion: Các task với priority cao sẽ được thực hiện trước các task có priority thấp, tuy nhiên các task có priority thấp lại lock resource, dẫn đến các task với priority cao phải đợi để được thực hiện -> Làm app chậm.
    Race condition: 2 task cùng truy cập để 1 sửa 1 tài nguyên chung trong cùng 1 thời điểm, dẫn đến sai lệch kết quả.

    Deadlock: Xảy ra khi 2 tasks đang đợi lẫn nhau giải phóng tài nguyên để có thể thực hiện tiếp -> Dẫn đến việc cả 2 tasks đều không thể thực hiện -> Treo app.

    Các basic func của GCD:

    1. Dispatch Group: Giả sử bạn có nhiều task được thực hiện theo kiểu async, khi đó bạn sẽ không thể biết khi nào chúng hoàn tất bởi chúng trả về quyền điều khiển ngay lập tức -> Dispatch Group dùng để nhận biết khi 1 nhóm các task được thực hiện xong.

    // 1
    let dispatchGroup = DispatchGroup()
    // 2
    let viewContainer = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
    viewContainer.backgroundColor = .red
    view.addSubview(viewContainer)
    
    // A box move around in the view
    let box = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
    box.backgroundColor = .yellow
    viewContainer.addSubview(box)
    
    // A label is appear when all the animations finish
    let label = UILabel(frame: CGRect(x: 15, y: 100, width: 360, height: 40))
    label.font = label.font.withSize(50)
    label.text = "All Done!"
    label.textColor = .yellow
    label.textAlignment = .center
    label.isHidden = true
    viewContainer.addSubview(label)
    
    // Animations
    // 3
    dispatchGroup.enter()
    UIView.animate(withDuration: 2, animations: {
        box.center = CGPoint(x: 300, y: 300)
    }, completion: { _ in
        UIView.animate(withDuration: 3, animations: {
            box.transform = CGAffineTransform(rotationAngle: .pi/4)
        }, completion: { _ in
             // 4
             self.dispatchGroup.leave()
        })
    })
    // 5        
    dispatchGroup.notify(queue: .main) {
        UIView.animate(withDuration: 2, animations: {
            viewContainer.backgroundColor = .blue
        }, completion: { _ in
            label.isHidden = false
        })
    }

    Note: Thử đưa đoạn code này vào viewDidLoad và run thử

    1. Khởi tạo 1 DispatchGroup.
    2. Khởi tạo 1 vài view để thực hiện animation.
    3. Gọi hàm enter() để đưa task cần thực hiện vào trong group.
    4. Gọi hàm leave() để báo với group rằng task đã thực hiện xong.
    5. Khai báo xem hàm notify sẽ làm gì khi các task trong group được thực hiện xong. Hàm notify sẽ được tự động gọi đến khi tất cả các task được đưa vào group đã được thực hiện xong.

    Note: Số lần gọi enter() phải bằng số lần gọi hàm leave().

    Kết luận: Thay vì phải đặt delay giữa các animation thì chúng ta có thể dùng DispatchGroup để nhận biết khi 1 animation đã thực hiện xong để thực hiện tiếp animation khác.

    Ở bài viết tiếp theo sẽ tiếp tục là về các basic func và 1 vài advance func của GCD.

  • Cách lấy UDID của thiết bị iPhone, iPad, iPod…

    Cách lấy UDID của thiết bị iPhone, iPad, iPod…

    + UDID là gì?
    UDID là viết tắt của Unique Device Identifier là hình thức cho nhận dạng thiết bị duy nhất và là một chuỗi chữ số xác định duy nhất iPad hoặc iPhone… của bạn từ tất cả các thiết bị khác.
    + UDID dùng khi nào?
    Nó dùng ở nhiều chỗ, nhưng dùng nhiều nhất với lập trình viên là để đăng kí thiết bị trên trang Developer Apple, chuẩn bị cho quá trình build file .ipa theo hình thức Adhoc…
    Dưới đây là các cách lấy UDID mà các bạn có thể tham khảo. ?

    Cách 1: Dùng thiết bị chạy MacOS.

    B1: Nối thiết bị iDevice với máy tính của bạn.
    B2: Mở “Finder” và click vào các bước giống hình.
    1) Nhấp chuột tên device.
    2) Nhấp chuột vào thông tin device, đến khi hiện ra thông tin UDID như hình.

    Cách 2: Dùng thiết bị chạy Window.

    Mở ứng dụng “iTunes”, chọn vào hình “thiết bị”.

    Chọn mục “Summary” , kích chuột vào dòng “Serial Number” đến khi hiện ra “UDID” như hình dưới.

    Hình mình mượn trên mạng vì ko có thiết bị window. 😀

    Cách 3: Dùng ngay iDevices.
    (Cách này khá hữu dụng, vì nó giúp lấy UDID ngay trên điện thoại…trong trường hợp khách hàng, hoặc sếp… của bạn ở xa yêu cầu gửi cho bản build .ipa để cài đặt… thì bạn có thể gửi ngay hướng dẫn này cho họ ?)


    Truy cập đường link https://get.udid.io/ và làm theo các bước dưới hình

    Kết quả là dãy số ở bước 11.
    Trên đây là 1 số cách mình thấy khá thông dụng, tất nhiên còn nhiều hình thức khác.
    Hi vọng bài viết hữu dụng cho các bạn. ?

  • Optimize Lambda function with Nodejs.

    Optimize Lambda function with Nodejs.

    Gần đây có nghiên cứu lại mấy vấn đề của Lambda function và mày mò vào Node Summit, bài viết này thực ra trình bày lại topic này của Matt Lavin

    Về cơ bản với Lambda, AWS đã làm gần hết mọi thứ về management, scale function, kết nối đến các service như DynamoDB, SQS,… Gần như chúng ta chỉ cần chú ý đến việc coding là chính. Tuy nhiên để mọi thứ tốt hơn cho người dùng thì cần giảm latency, response nhanh hơn và dễ debug hơn trong những trường hợp cần thiết và chính trong source code Lambda function tức là:

    • Cải tiện latency
    • Tìm ra bug performance
    • Debug.

    Bài này sẽ nói về các cách optimze coding là chính, những phần khác thì hãy xem kỹ topic nhé.

    Đầu tiên bao giờ cũng cần tìm hiểu xem Lambda hoạt động như nào nhưng trước tiên mình sẽ đưa một ví dụ điển hình về lambda function:

    const dynamodb = require('aws-sdk/clients/dynamodb');
    const docClient = new dynamodb.DocumentClient();
    const tableName = process.env.SAMPLE_TABLE;
    exports.getByIdHandler = async (event) => {
        const { httpMethod, path, pathParameters } = event;
        if (httpMethod !== 'GET') {
            throw new Error(`Unsupported method`);
        }
        console.log('received:', JSON.stringify(event));
        const { id } = pathParameters;
        const params = {
            TableName: tableName,
            Key: { id },
        };
        const { Item } = await docClient.get(params).promise();
        const response = {
            statusCode: 200,
            body: JSON.stringify(Item),
        };
        return response;
    };
    

    Khá là điển hình với việc: Khởi tạo SDK, handle request, query database và đưa ra kết quả, tất nhiên trước đó sẽ là download source code và khởi chạy lambda function. Và hãy ghép nó vào mô hình lifecycle của lambda function như ở bên dưới.

    Như hình bên trên toàn bộ Lifecycle của AWS Lambda bao gồm Cold StartWarm Start. Warm start: bao gồm phần thời gian code chạy Cold start: thời gian chuẩn bị.

    Như vậy có thể thấy rằng phần warm start là phần coding đơn thuần và optimze như chúng ta optimze source code khi sử dụng các framework hay runtime khác. Mặt khác, mọi người thường nghĩ rằng Lambda Function sẽ thực hiện toàn bộ các bước trên mỗi lần execute nhưng không Lambda sẽ không khởi chạy lại Cold Start, miễn là bạn không update source code nhưng chỉ trong 15 phút thôi nhưng vậy là quá đủ. Reduce latency sẽ bắt đầu từ đây.

    Như hình trên cứ sau một khoảng thời gian nhất định Lambda function lại thực hiện Cold Start, những chỗ thời gian execute cao bất thường ấy, nhìn chung hãy để function Lambda luôn sẵn sàng để execute.

    Một cách chính thống hơn thì có thể tìm hiểu ở đây, AWS đề cập đến Lambda execution context (Môi trường để running Lambda code), context này sẽ bị đóng băng sau khi sử dụng xong function và được giã đông khi chạy lần tiếp và AWS cũng đề xuất một vài thủ thuật để optimize Lambda function:

    • Đầu tiên bắt đầu với handler method, Các object được khai báo bên ngoài handler vẫn được khởi tạo, cung cấp tối ưu hóa bổ sung khi handler được gọi lại. Ví dụ: nếu Lambda connect đến database (RDS, DynamoDB), thay vì kết nối đi kết nối lại, kết nối được tái sử dụng qua các lần invoke khác nhau trong một lambda instance. Một cách đơn giản có thể lazy load connection, như bây giờ AWS đã cải tiến SDK để dùng keep alive hoặc đơn giản là chuyễn những thứ nặng nề ra khỏi handler, cache lại AWS SDK Client
        const AWS = require('aws-sdk')
        // http or https
        const https = require('https');
        const agent = new https.Agent({
          keepAlive: true
        });
      
        const dynamodb = new AWS.DynamoDB({
          httpOptions: {
            agent
          }
        });
    
        fuction fuckingHeavyFunction() {
        }
    
        const outsideHeavyResult = fuckingHeavyFunction(); // run on every Lambda init instance.
    
        exports.handler = async (event) => {
          const heavyResult = fuckingHeavyFunction(); // run on every lambda request
          return response;
        };
    
    • Mỗi Lambda function có 512Mb lưu trữ ở /tmp, bạn có thể lưu trữ bất kỳ thứ gì. Vùng lưu trữ này sẽ được đóng băng cùng với Execution context, như vậy bạn có thể lưu trữ nhiều thứ ở trong này, ví dụ những tính toán, variable ít thay đổi có thể lưu trữ lại và dùng lại cho lần tiếp theo.
    • Nếu sử dụng background process trong Lambda function, hãy chắc chắn nó được hoàn toàn hoàn thành khi Lambda function kết thúc, vì có thể nó được sử dụng lại và tiếp tục chạy. Dẫn đến những bug không như ý. Nhưng nói chung cũng không nên nghĩ rằng Lambda sẽ sử dụng lại các tài nguyên khi chạy lại Lambda function, hãy chuẩn bị lại các tài nguyên hoặc kiểm tra việc sử dụng cho chắc chắn.
  • Binding Objective-C libraries – Xamarin iOS

    Binding Objective-C libraries – Xamarin iOS

    Content

    1. Create project Objective-C lib
    2. Define functions
    3. Config project
    4. Create makefile
    5. Create project binding library in solution Xamarin
    6. Add binding library to project iOS Xamarin
    7. Call Library API trong code Xamarin.

    1. Create project objective-C

    Open Xcode, File -> New project -> Cocoa Touch Static Library> Next

    Tiếp theo, nhập tên project, phần language nhớ chọn Objective-C nhé

    2. Define functions

    Define các functions vào file .m(implement) và file .h (header) phần logic các bạn tự code nhé @@

    3. Config project

    Setting target version sang iOS mà bên Xamarin các bạn sẽ chọn, ở đây project của mình để là 10.0

    4. Run terminal commands

    4.1 Create 1 makefile

    Với content như dưới đây, chú ý thay chỗ in đậm bằng tên của project

    XBUILD=/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild
    PROJECT_ROOT=./TênProject
    PROJECT=$(PROJECT_ROOT)/ TênProject.xcodeproj
    TARGET= TênProject

    all: lib$(TARGET).a

    lib$(TARGET)-i386.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphonesimulator -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphonesimulator/lib$(TARGET).a $@

    lib$(TARGET)-armv7.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphoneos -arch armv7 -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphoneos/lib$(TARGET).a $@

    lib$(TARGET)-arm64.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphoneos -arch arm64 -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphoneos/lib$(TARGET).a $@

    lib$(TARGET).a: lib$(TARGET)-i386.a lib$(TARGET)-armv7.a lib$(TARGET)-arm64.a
    xcrun -sdk iphoneos lipo -create -output $@ $^

    clean:
    -rm -f *.a *.dll

    4.2 Execute makefile

    Vào terminal đi đến folder chứa Makefile gõ:
    •“make” – để chạy makefile
    •“make clean” – để clean các file thư viện .a được generate ra sau khi đã chạy ”make” ( Hàm này để đỡ phải xoá file .a = tay thôi)

    4.3 The API definition file

    Sau khi đã có file .a, chạy lệnh sau:

    sharpie bind -sdk iphoneos12.4 /Users/apple/Documents/UrlFilterLib2/UrlFilterLib2/*.h -scope /Users/apple/Documents/UrlFilterLib2/UrlFilterLib2/

    Chú ý đường dẫn vào library mỗi máy khác nhau, và cái số ”12.4” là hỗ trợ iOS version mới nhất của Xcode,
    • Ex :  Xcode 10.3 có hỗ trợ iOS mới nhất là 12.4
    • Sau khi chạy lệnh trên, sẽ generate ra file ApiDefinition.cs

    Đến đây là chúng ta đã hoàn thành bước tạo library rồi đó. Tiếp theo là sử dụng nó trong project Xamarin nhé .

    5. Add binding library to project Xamarin

    1. Click chuột phải vào Solution ->  Add ->  Add new project

    2. Chọn iOS -> Library -> Bindings Library.

    3. Next.

    4. Kéo file .a vào binding library project vừa tạo

    5. Alert hiện lên, chọn “Copy the file in the directory”  -> OK
    Copy content trong file ApiDefinition.cs tại step 4  vào file ApiDefinition.cs trong project  Xamarin này.

    6. Sau đó nhấn chọn build Library project.

    6. Add binding library to project Xamarin

    1. Click chuột phải vào “References”, chọn “Edit references”.
    2. Tích chọn Library project..
    3. OK

    7. Call library API

    Sau khi thực hiện các bước ở trên chúng ta có thể gọi chúng như với các lib của Xamarin rồi

    Kết Luận

    Như vậy là chúng ta đã hoàn thành việc binding lib từ objective-c vào Xamarin iOS. Phần tiếp theo mình sẽ hướng dẫn binding với android.
    Tham khảo tại: https://docs.microsoft.com/en-us/xamarin/cross-platform/macios/binding/?context=xamarin/ios

  • Vài điều cần biết về Laravel Model

    Vài điều cần biết về Laravel Model

    Mọi người làm Laravel cũng khá nhiều nhưng đã bao giờ đi sâu về tìm hiểu Laravel Model chưa hay dùng như thế nào cho hiệu quả.

    Laravel Model hay chính xác hơn Eloquent Model là một class ActiveRecord để chúng ta làm việc dễ dàng hơn với database, sau đây thì có vài cái hay ho về Laravel Model.

    Tạo model

    php artisan make:model Flight
    

    Ở guide của Laravel thì sẽ hướng dẫn như trên, và class Flight sẽ được tạo ở namespace App hay app folder.

    php artisan make:model Models/Flight
    

    Một cách tương tự thì chúng ta hay dùng App\Models vậy sao không tạo class luôn trong đó?

    Casting attributes

    Cái này thì không có trong Eloquent doc

    $casts attribute là attribute của Eloquent, nó sử dụng để cast một field trong object về một kiểu dữ liệu nào đó

    protected $casts = [
        'is_published' => 'boolean'
    ];
    

    Bây giờ is_publish field sẽ luôn được trả về boolean, ngay cả khi chúng ta lưu trữ là 0 hay 1 trong database. Ngoài boolean` ra thì có thể cast nhiều cái khác ngay cảdatedatetime“`.

    Một lỗi khá phổ biến là lỗi format thời gian, chúng ta trả ra nhiều kiểu format khác nhau: dd-MM-YYYY, YYYY-MM-dd.

    Bây giờ có thể thống nhất như này:

    protected $casts = [
        'published_at' => 'datetime:Y-m-d',
    ];
    

    Như vậy chúng ta không cần format date ở đâu nữa, kể cả các template framework dùng với Laravel.

    Visibility

    Một số thuộc tính không nên được trả về JSON response client, ví dụ như password. Chúng ta có thể ẩn nó đi, hay exclude khỏi response của model, như kiểu một blacklist vậy dùng $hidden.

    protected $hidden = [
        'password'
    ];
    

    Ngược lại thì chúng ta lại có whitelist – $visible:

    protected $visible = [
        'first_name',
        'last_name'
    ];
    

    Khi đó ngoài những field có trong $visible ra thì toàn bộ những field còn lại sẽ bị ẩn, tương tự như: $fillable$guarded.

    Accessors

    Accessors đúng như cái tên, thì giúp gộp nhiều attribute thành một để chúng ta access. Ví dụ: Bình thường khi cần full name chúng ta làm như này:

    $full_name = $this->first_name . ' ' . $this->last_name;
    

    Với Accessors thì mọi chuyện đơn giản hơn, cú pháp nhắn tin như này:

    get[NameOfAttribute]Attribute
    

    đây là ví dụ:

    public function getFullNameAttribute() {
        return "{$this->first_name} {$this->last_name}";
    }
    

    Xong rồi gọi như này:

    $full_name = $user->full_name;
    

    Mutators

    Tương tự như Accessors, thì Mutators dùng cho việc update giá trị, cú pháp cũng tương tự như Accessors luôn:

    public function setLastNameAttribute($value) {
        $this->attributes['last_name'] = ucfirst($value);
    }
    

    thì ví dụ

    $user->last_name = 'hoang';
    $last_name = $user->last_name; //cái này sẽ trả ra Hoang
    //vì ucfirst sẽ capitalized cái chữ cái đầu
    

    Appending values

    Mặc định, Accessors với cả relations thì sẽ không được thêm vào array hay JSON của Model, vậy nếu cần thì chúng ta dùng $appends ví dụ với cái getFullNameAttribute như trên:

    $appends = [
        'full_name'
    ];
    

    Có cái này hơi ngu chút: $appends thì vẫn sử dụng snake case còn Accessors thì sử dụng camel case.

    Vụ appending với cả relations thì mọi người tự tìm hiểu nhé.

    Touches

    Khi model có relations BelongsTo hoặc BelongsToMany với model khác, trong trường hợp này, một Comment thuộc về Blog, trong một số trường hợp, chúng ta cần update timestamp của Blog khi edit hoặc thêm mới Comment. $touches có thể làm điều này.

    class Comment extends Model
    {
        protected $touches = ['blog'];
    
        public function blog()
        {
            return $this->belongsTo(App\Blog::class);
        }
    }
    

    Trên đây là mấy cái mình vừa tìm hiểu được :D, có điều vẫn còn nhiều cái nữa, mong sẽ được chia sẽ trong những phần sau.