Hiển thị các bài đăng có nhãn Golang. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn Golang. Hiển thị tất cả bài đăng

27 tháng 7, 2019

Go Scheduler

Go Scheduler

Trong bài viết này, chúng ta sẽ cùng tìm hiểu xem Go Scheduler hoạt động như thế nào, thông qua đó ta có thể đưa ra các quyết định về mặt kỹ thuật tốt hơn khi lập trình bằng ngôn ngữ Go.

Khi một chương trình phần mềm được viết bằng ngôn ngữ Go khởi động, nó được cung cấp một Bộ vi xử lý ảo - Logical Processor (P) cho mỗi virtual core trên máy host. Nếu bạn có một bộ vi xử lý với nhiều hardware thread trên mỗi physical core (Hyper-Threading) thì chương trình của bạn sẽ coi mỗi hardware thread như một virtual core.

Ví dụ, giả sử chúng ta có thông số kĩ thuật sau của một máy tính Macbook Pro:

Hình 1:

Hình 1

Như bạn có thể thấy, chúng ta có một bộ vi xử lý đơn với 4 physical core. Theo như những thông số trên thì chúng ta không thấy thông số nào nói đến số lượng hardware thread trên mỗi physical core. Tuy nhiên, bộ vi xử lý Intel Core I7 có tính năng Hyper-Threading, điều này có nghĩa là sẽ có 2 hardware thread cho mỗi physical core. Vậy nên chương trình của bạn sẽ có thể hiểu là có 8 virtual core sẵn sàng để thực thi song song các tác vụ trên các OS Thread.

package main

import (
	"fmt"
	"runtime"
)

func main() {

    // NumCPU returns the number of logical
    // CPUs usable by the current process.
    fmt.Println(runtime.NumCPU())
}

Khi chúng ta thực hiện đoạn code trên với những thông số đã xem qua của máy Macbook Pro thì kết quả NumCPU() sẽ là 8. Mọi chương trình khi chạy sẽ đều được cung cấp 8 P. Mỗi P sẽ được gán cho một OS Thread (M) (M là viết tắt của từ machine). Các Thread này sẽ được quản lý bởi OS và OS vẫn chịu trách nhiệm cho việc đặt Thread lên Core trong quá trình thực thi. Điều này có nghĩa là mỗi khi chúng ta khởi chạy chương trình, chúng ta sẽ có 8 thread sẵn sàng để thực thi công việc, mỗi thread được đính kèm vào một P.

Mọi chương trình Go cũng được cung cấp một Goroutine ban đầu (G), Goroutine giống như OS Thread nhưng mà là ở tầng ứng dụng. Cũng giống như các OS Thread được chuyển đổi ngữ cảnh (context-switch) trên một core, các Goroutine cũng được chuyển đổi ngữ cảnh trên M.

Có 2 run-queue khác nhau trong Go scheduler: Global Run Queue (GRQ)Local Run Queue (LRQ). Mỗi P được cung cấp một LRQ quản lý các Goroutine đã được chỉ định để thực thi bên trong context của P. Những Goroutine này sẽ lần lượt được switch-context on hoặc off trên M mà đã được gán cho P. GRQ dành cho các Goroutine chưa được gán cho một P nào. Ta sẽ thảo luận về tiến trình dịch chuyển các Goroutine từ GRQ sang LRQ trong phần sau.

Hình 2:

Hình 2

Cooperating Scheduler

Như chúng ta biết thì OS scheduler là một Preemptive scheduler. Về cơ bản thì điều này nghĩa là chúng ta sẽ không dự đoán được scheduler sẽ làm gì và vào bất kì lúc nào. Kernel sẽ chịu trách nhiệm đưa ra quyết định và mọi thứ sẽ không thể xác định rõ ràng. Các ứng dụng chạy trên OS không có quyền kiểm soát những gì đang xảy ra bên trong Kernel bằng scheduler trừ khi chúng tận dụng atomic hoặc mutex.

Go scheduler là một phần của Go runtime, và Go runtime được tích hợp vào trong ứng dụng của bạn. Nghĩa là, Go scheduler sẽ chạy trong User space, phía trên Kernel. Hiện tại, cách triển khai Go scheduler không phải là Preemptive scheduler mà là một Cooperating scheduler. Với việc là một Cooperating scheduler nghĩa là scheduler này cần các sự kiện User space được xác định rõ ràng để có thể đưa ra quyết định schedule.

Goroutine States

Cũng giống như Thread, Goroutine cũng có ba trạng thái high-level, chúng chỉ ra vai trò Go scheduler đối với một Goroutine bất kì. Một Goroutine có thể ở một trong ba trạng thái: Waiting, Runable hoặc Executing.

Waiting: Nghĩa là Goroutine bị dừng và đang đợi điều gì đó để tiếp tục. Điều này có thể là vì chờ hệ điều hành phản hồi (system call) hoặc lời gọi đồng bộ (atomic hoặc mutex). Đây là những nguyên nhân gốc rễ dẫn đến hiệu năng kém của một chương trình.

Runnable: Nghĩa là Goroutine đã sẵn sàng để thực thi các chỉ dẫn đã được chỉ định trên M. Nếu chúng ta có nhiều Goroutine cùng trong trạng thái này thì cũng có nghĩa là các Goroutine cũng sẽ phải đợi lâu hơn đến đến lượt thực thi.

Executing: Nghĩa là các Goroutine đã được đặt trên một M và đang thực thi các chỉ dẫn của nó. Công việc liên quan đến ứng dụng đang được thực hiện.

Context Switching

Go Scheduler yêu cầu các sự kiện User-space được xác định rõ xảy ra tại các safe-point trong code để thực hiện context-switch. Các sự kiện và safe-point này được thể hiện bên trong các lời gọi hàm. Các lời gọi hàm rất quan trọng đối với trạng thái của Go Scheduler. Từ Go version 1.11 trở xuống, nếu bạn chạy một vòng lặp đơn giản bất kì mà không thực hiện các lời gọi hàm thì có thể sẽ gây ra độ trễ bên trong Scheduler và Garbage Collection. Các lời gọi hàm phải xảy ra bên trong một khung thời gian hợp lí là cực kì quan trọng.

Có bốn loại sự kiện xảy ra trong các chương trình Go cho phép Scheduler đưa ra các quyết định schedule. Điều này không có nghĩa là nó sẽ luôn luôn xảy ra ở một trong các loại sự kiện này, mà chỉ là Scheduler sẽ có cơ hội để đưa ra quyết định schedule.

Bốn loại sự kiện được nhắc tới là:

  • Sử dụng từ khóa go

  • Garbage collection

  • System call

  • Synchronization và Orchestration

Sử dụng từ khóa go

Từ khóa go giúp bạn tạo mới các Goroutine, khi một Goroutine được tạo mới thì nó sẽ chờ được schedule bởi Go Scheduler.

Garbage collection

GC chạy bằng cách sử dụng chính các Goroutine của riêng nó nên các Goroutine này cần thời gian để thực thi ở trên M. Điều này dẫn đến việc tạo ra nhiều sự hỗn loạn khi schedule. Tuy nhiên, Go Scheduler đủ "thông minh" để đưa ra các quyết định hợp lí. Một trong các quyết định thông minh là thực hiện context-switch cho một Goroutine muốn sử dụng đến Heap trong quá trình GC. Khi đang trong quá trình GC, rất nhiều quyết định schedule sẽ được đưa ra.

System call

Nếu một Goroutine thực hiện một lời gọi system call sẽ khiến Goroutine này bị block trên M, đôi khi scheduler sẽ có thể thực hiện context-switch off Goroutine này trên M và thực hiện context-switch một Goroutine mới ngay trên M này. Tuy nhiên, thỉnh thoảng một M mới sẽ được tạo để tiếp tục thực thi các Goroutine đang trong hàng đợi của P. Chúng ta sẽ tìm hiểu chi tiết về điều này trong phần sau.

Synchronization và Orchestration

Nếu một atomic, mutex hoặc channel được gọi sẽ dẫn đến Goroutine bị block, Go Scheduler có thể thực hiện context-switch một Goroutine mới để tiếp tục thực thi công việc. Một khi Goroutine bị block có thể tiếp tục thực thi tiếp thì nó có thể được cho trở lại vào hàng đợi và đợi đến lượt được context-switch.

Các System-call bất đồng bộ

Khi hệ điều hành của bạn có khả năng xử lý bất đồng bộ một system-call thì network-poller có thể được sử dụng để xử lý system-call này hiệu quả hơn. Điều này có thể thực hiện được là nhờ vào kqueue trên MacOS, epoll trên Linux hoặc iocp trên Windows.

Ngày nay, các system-call dựa trên network có thể được xử lý bất đồng bộ nhờ vào hệ điều hành của bạn. Bằng cách sử dụng network-poller, Go Scheduler có thể ngăn chặn được việc các Goroutine bị block trên M khi các system-call kiểu này được tạo ra. Điều này sẽ giúp cho M tiếp tục được sử dụng để thực thi các Goroutine khác trong LRQ mà không phải tạo ra thêm các M. Nó sẽ giúp giảm thiểu thời gian tải của quá trình schedule trên hệ điều hành.

Hình 3:

Hình 3

Nhìn vào hình 3 chúng ta có thể thấy Goroutine-1 đang thực thi công việc trên M và có 3 Goroutine khác đang đợi đến lượt trong LRQ. Network-poller thì đang nhàn rỗi.

Hình 4:

Hình 4

Trong hình 4, Goroutine-1 muốn thực hiện một network system call, vì vậy Goroutine-1 được chuyển đến cho network-pollernetwork system call sẽ được xử lý bất đồng bộ. Một khi Goroutine-1 được chuyển đến network-poller, thì bây giờ M sẽ thực thi công việc của một Goroutine khác trong LRQ. Như hình 4 thì Goroutine sẽ được context-switch trên M.

Hình 5:

Hình 5

Trong hình 5, network-poller hoàn thành xử lý bất đồng bộ network system call và Goroutine-1 được chuyển lại cho LRQ. Khi Goroutine-1 đến lượt context-switch trở lại, có đoạn code mà nó chịu trách nhiệm xử lý có thể được thực thi lại. Điểm lợi lớn nhất ở đây đó là khi ta thực thi các network system call thì không cần phải tạo thêm M. Network-poller có một OS Thread và nó xử lý event-loop rất hiệu quả.

Synchronous System Call

Vậy điều gì sẽ xảy ra khi một Goroutine muốn thực hiện một system-call không thể được hoàn thành bằng xử lý bất đồng bộ? Trong trường hợp này, network-poller không thể được sử dụng và Goroutine tạo ra system-call sẽ bị block trên M. Chúng ta sẽ không có cách nào để ngăn điều này xảy ra. System-call khi xử lý file là một ví dụ điển hình của một system-call không thể xử lý bất đồng bộ.

Hình 6:

Hình 6

Trong hình 6, Goroutine-1 tạo một synchronous system call dẫn đến nó sẽ bị block trên M1

Hình 7:

Hình 7

Trong hình 7, Go Scheduler có thể xác định được Goroutine-1 gây ra block trên M. Lúc này, Go Scheduler sẽ tách M1 khỏi P kèm theo Goroutine-1 đi với nó. Sau đó, Go Scheduler tạo ra M2 để phục vụ cho P. Lúc này, Goroutine-2 có thể sẽ được lựa chọn từ LRQ và được context-switch trên M2. Nếu một M đã tồn tại từ trước, quá trình chuyển đổi này sẽ diễn ra nhanh hơn so với việc phải tạo mới M2

Hình 8:

Hình 8

Trong hình 8, blocking system call được tạo bởi Goroutine-1 kết thúc. Lúc này, Goroutine-1 được chuyển lại cho LRQ và được xử lý lại bởi P và kịch bản xử lý system call có thể lại tái diễn một lần nữa.

view raw Go Scheduler.md hosted with ❤ by GitHub

24 tháng 2, 2019

Package context trong Go

Go Concurrency Patterns: Context

Introduction

Trong Go, mỗi request đến sẽ được handle trong một goroutine. Các request-handler thường bổ sung thêm các goroutine để truy cập đến backend như database và các RPC service.

Context

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Method Done trả về một channel được sử dụng như một tín hiệu hủy cho các function đại diện cho Context: khi một channel close, các function nên hủy bỏ công việc đang thực thi và return. Method Err trả về một error chỉ ra lý do mà Context bị hủy.

Lý do mà Context không có method Cancel cũng tương tự như channel Done chỉ là một channel nhận kết quả (receive-only): hàm mà nhận tín hiệu cancel thông thường sẽ không phải là một trong các hàm gửi tín hiệu (signal) đi. Đặc biệt, khi một operation cha khởi chạy goroutine cho các sub-operation, những sub-operation này sẽ không thể hủy operation cha. Thay vào đó, hàm WithCancel có cung cấp cách để hủy một Context mới.

Context có thể được dùng đồng thời bởi nhiều goroutine. Chúng ta có thể truyền một Context cho goroutine bất kì và có thể hủy Context để báo hiệu cho tất cả các goroutine chứa Context này.

Method Deadline cho phép các function xác định xem liệu chúng có nên thực thi công việc hay không. Nếu thời gian còn lại quá ngắn thì nó có thể không đáng để thực thi. Ta cũng có thể sử dụng một thời hạn (deadline) để thực hiện thiết lập một timeout cho các giao tiếp I/O.

Method Value cho phép một Context mang theo dữ liệu thuộc phạm vi request (request-scope). Dữ liệu này bắt buộc phải là safe khi được sử dụng đồng thời bời nhiều goroutine.

Derived contexts

Package context cung cấp các hàm dẫn xuất một giá trị Context mới từ một Context đã tồn tại, chúng sẽ tạo thành dạng cây (Context-tree): khi một Context bị cancel, tất cả Context đã được dẫn xuất từ nó cũng sẽ bị cancel.

Background sẽ là root của các Context-tree, nó không bao giờ bị cancel:

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context

Các hàm WithCancelWithTimeout trả về giá trị Context dẫn xuất có thể bị cancel trước Context cha. Context liên kết với một request đến thường bị cancel khi handler của request này return. Hàm WithCancel cũng hữu ích để cancel các request dư thừa khi sử dụng nhiều replica. Hàm WithTimeout thì có thể giúp thiết lập một deadline cho các request đến các server backend:

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue cung cấp cách liên kết các giá trị request-scope với một Context:

// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

Cách tốt nhất để nắm được cách sử dụng package context là tham khảo qua các ví dụ thực tế.

Ví dụ: Google Web Search

Ví dụ của chúng ta là một server HTTP handle các URL /search?q=golang&timeout=1s bằng việc forward từ khóa truy vấn "golang" đến Google Web Search API và hiển thị ra kết quả. Tham số timeout báo cho server biết khoảng thời gian để cancel request.

Code được chia thành 3 package:

  • server cung cấp hàm main và handler cho /search.
  • userip cung cấp các hàm để trích xuất địa chỉ IP của người dùng từ request và liên kết nó với một Context.
  • google cung cấp hàm Search để gửi truy vấn đến cho Google.

Server

Ở đây server sẽ xử lý các request có format giống như search?q=golang bằng cách cung cấp một vài kết quả đầu tiên khi tìm kiếm bằng Google. Ta sẽ sử dụng hàm handleSearch để handle endpoint /search. Handler này khởi tạo một Context gọi là ctx và bố trí cancel khi handler return. Nếu request đến bao gồm tham số timeout trong URL thì Context sẽ bị cancel tự động nếu như hết thời gian timeout:

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // ctx.Done channel, which is the cancellation signal for requests
    // started by this handler.
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // The request has a timeout, so create a context that is
        // canceled automatically when the timeout expires.
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // Cancel ctx as soon as handleSearch returns.

Handler này trích xuất từ khóa truy vấn từ request và địa chỉ IP của client bằng cách sử dụng package userip. Backend request cần đến địa chỉ IP của client vì vậy hàm handleSearch đính kèm nó vào trong ctx:

    // Check the search query.
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Store the user IP in ctx for use by code in other packages.
    userIP, err := userip.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ctx = userip.NewContext(ctx, userIP)

Handler gọi đến google.Search sử dụng ctxquery:

    // Run the Google search and print the results.
    start := time.Now()
    results, err := google.Search(ctx, query)
    elapsed := time.Since(start)

Nếu tìm kiếm thành công, handler này sẽ hiển thị ra kết quả:

    if err := resultsTemplate.Execute(w, struct {
        Results          google.Results
        Timeout, Elapsed time.Duration
    }{
        Results: results,
        Timeout: timeout,
        Elapsed: elapsed,
    }); err != nil {
        log.Print(err)
        return
    }

Package userip

Package userip cung cấp các function để trích xuất địa chỉ IP người dùng từ request gửi lên và liên kết nó với một Context. Một Context chứa một đối tượng map với keyvalue đều là kiểu dữ liệu interface{}. Kiểu dữ liệu của key phải hỗ trợ so sánh equal, và value phải an toàn khi được sử dụng đồng thời bời nhiều goroutine. Các package giống như userip ẩn đi chi tiết ánh xạ key-value và cung cấp khả năng truy cập vào một value cụ thể trong Context.

Để tránh trùng lặp kiểu dữ liệu key, userip định nghĩa ra một kiểu unexportedkey và sử dụng một value của kiểu dữ liệu này như một context-key:

// The key type is unexported to prevent collisions with context keys defined in
// other packages.
type key int

// userIPkey is the context key for the user IP address.  Its value of zero is
// arbitrary.  If this package defined other context keys, they would have
// different integer values.
const userIPKey key = 0

FromRequest trích xuất một giá trị userIP từ một http.Request:

func FromRequest(req *http.Request) (net.IP, error) {
    ip, _, err := net.SplitHostPort(req.RemoteAddr)
    if err != nil {
        return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
    }
    ...
}

NewContext khởi tạo một đối tượng Context mới mang giá trị userIP đã được cấp:

func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

FromContext trích xuất một userIP từ một Context:

func FromContext(ctx context.Context) (net.IP, bool) {
    // ctx.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil.
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}
// Package google provides a function to do Google searches using the Google Web
// Search API. See https://developers.google.com/web-search/docs/
//
// This package is an example to accompany https://blog.golang.org/context.
// It is not intended for use by others.
//
// Google has since disabled its search API,
// and so this package is no longer useful.
package google
import (
"context"
"encoding/json"
"net/http"
"golang.org/x/blog/content/context/userip"
)
// Results is an ordered list of search results.
type Results []Result
// A Result contains the title and URL of a search result.
type Result struct {
Title, URL string
}
// Search sends query to Google search and returns the results.
func Search(ctx context.Context, query string) (Results, error) {
// Prepare the Google Search API request.
req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("q", query)
// If ctx is carrying the user IP address, forward it to the server.
// Google APIs use the user IP to distinguish server-initiated requests
// from end-user requests.
if userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()
// Issue the HTTP request and handle the response. The httpDo function
// cancels the request if ctx.Done is closed.
var results Results
err = httpDo(ctx, req, func(resp *http.Response, err error) error {
if err != nil {
return err
}
defer resp.Body.Close()
// Parse the JSON search result.
// https://developers.google.com/web-search/docs/#fonje
var data struct {
ResponseData struct {
Results []struct {
TitleNoFormatting string
URL string
}
}
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
for _, res := range data.ResponseData.Results {
results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
}
return nil
})
// httpDo waits for the closure we provided to return, so it's safe to
// read results here.
return results, err
}
// httpDo issues the HTTP request and calls f with the response. If ctx.Done is
// closed while the request or f is running, httpDo cancels the request, waits
// for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error.
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// Run the HTTP request in a goroutine and pass the response to f.
c := make(chan error, 1)
req = req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()
select {
case <-ctx.Done():
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}
view raw google.go hosted with ❤ by GitHub
// The server program issues Google search requests and demonstrates the use of
// the go.net Context API. It serves on port 8080.
//
// The /search endpoint accepts these query params:
// q=the Google search query
// timeout=a timeout for the request, in time.Duration format
//
// For example, http://localhost:8080/search?q=golang&timeout=1s serves the
// first few Google search results for "golang" or a "deadline exceeded" error
// if the timeout expires.
package main
import (
"context"
"html/template"
"log"
"net/http"
"time"
"golang.org/x/blog/content/context/google"
"golang.org/x/blog/content/context/userip"
)
func main() {
http.HandleFunc("/search", handleSearch)
log.Fatal(http.ListenAndServe(":8080", nil))
}
// handleSearch handles URLs like /search?q=golang&timeout=1s by forwarding the
// query to google.Search. If the query param includes timeout, the search is
// canceled after that duration elapses.
func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx is the Context for this handler. Calling cancel closes the
// ctx.Done channel, which is the cancellation signal for requests
// started by this handler.
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// The request has a timeout, so create a context that is
// canceled automatically when the timeout expires.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel() // Cancel ctx as soon as handleSearch returns.
// Check the search query.
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}
// Store the user IP in ctx for use by code in other packages.
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)
// Run the Google search and print the results.
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := resultsTemplate.Execute(w, struct {
Results google.Results
Timeout, Elapsed time.Duration
}{
Results: results,
Timeout: timeout,
Elapsed: elapsed,
}); err != nil {
log.Print(err)
return
}
}
var resultsTemplate = template.Must(template.New("results").Parse(`
<html>
<head/>
<body>
<ol>
{{range .Results}}
<li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
{{end}}
</ol>
<p>{{len .Results}} results in {{.Elapsed}}; timeout {{.Timeout}}</p>
</body>
</html>
`))
view raw server.go hosted with ❤ by GitHub
// Package userip provides functions for extracting a user IP address from a
// request and associating it with a Context.
//
// This package is an example to accompany https://blog.golang.org/context.
// It is not intended for use by others.
package userip
import (
"context"
"fmt"
"net"
"net/http"
)
// FromRequest extracts the user IP address from req, if present.
func FromRequest(req *http.Request) (net.IP, error) {
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}
userIP := net.ParseIP(ip)
if userIP == nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}
return userIP, nil
}
// The key type is unexported to prevent collisions with context keys defined in
// other packages.
type key int
// userIPkey is the context key for the user IP address. Its value of zero is
// arbitrary. If this package defined other context keys, they would have
// different integer values.
const userIPKey key = 0
// NewContext returns a new Context carrying userIP.
func NewContext(ctx context.Context, userIP net.IP) context.Context {
return context.WithValue(ctx, userIPKey, userIP)
}
// FromContext extracts the user IP address from ctx, if present.
func FromContext(ctx context.Context) (net.IP, bool) {
// ctx.Value returns nil if ctx has no value for the key;
// the net.IP type assertion returns ok=false for nil.
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}
view raw userip.go hosted with ❤ by GitHub

26 tháng 1, 2019

Một số điểm cải thiện tốc độ của Go

Một số điểm cải thiện tốc độ của Go

1. Xử lý và lưu trữ dữ liệu

Tính năng đầu tiên tôi muốn nói đến đó là việc xử lý và lưu trữ các giá trị hiệu quả của Go Go

var gocon int32 = 2019

Ở trên là một ví dụ về một giá trị trong Go. Khi biên dịch, gocon tiêu thụ chính xác 4 byte của bộ nhớ.

So sánh Go với một vài ngôn ngữ khác:

Python

% python
>>> from sys import getsizeof
>>> gocon = 2019
>>> getsizeof(gocon)
24

Do cách Python biểu diễn các biến, để lưu một giá trị tương tự, Python cần tiêu thụ bộ nhớ gấp 6 lần. Phần bộ nhớ nhiều hơn này được Python sử dụng để theo doi thông tin của kiểu dữ liệu, tham chiếu,...

Hãy xem qua ví dụ sử dụng Java:

Java

int gocon = 2019;

Tương tự như Go, kiểu int trong Java tiêu thụ 4 byte của bộ nhớ để lưu giá trị này. Tuy nhiên, để sử dụng giá trị này trong một collection như List hoặc Map thì trình biên dịch cần phải chuyển nó về đối tượng Integer.

Java

// 16 bytes on 32 bit JVM
// 24 bytes on 64 bit JVM
Integer gocon = new Integer(2019);

Vì vậy một số nguyên trong Java thường được trông như trên và tiêu thụ 16 byte hoặc 24 byte của bộ nhớ.

Tại sao điều này lại quan trọng? Bộ nhớ rẻ và có sẵn, vậy tại sao nên nắm rõ về chi phí này?

Hình 1

Trên đây là đồ thị cho ta thấy tốc độ CPU xung nhịp và tốc độ memory bus.

Lưu ý là khoảng cách giữa tốc độ xung nhịp của CPU và tốc độ memory bus liên tục mở rộng. Sự khác biệt hiệu năng là CPU dùng bao nhiêu thời gian để đợi memory.

CPU cache

Từ cuối những năm 1960, những nhà thiết kế CPU đã hiểu hơn về vấn đề này. Giải pháp của họ là sử dụng cache, một vùng nhỏ hơn và nhanh hơn memory được chèn giữa CPU và main memory.

Ví dụ:

// Location is a point in a three dimensional space
type Location struct {
    // 8 bytes per float64
    // 24 bytes in total
    X, Y, Z float64
}

// Locations consumes 24 * 1000 bytes
var Locations [1000]Location

Trên đây là một kiểu dữ liệu Location lưu tọa độ của các đối tượng trong không gian 3 chiều. Nó được viết bằng ngôn ngữ Go, vì vậy mỗi Location tiêu thụ chính xác 24 byte của bộ nhớ.

Chúng ta có thể sử dụng kiểu dữ liệu này để tạo ra một mảng bao gồm 1,000 đối tượng Location, việc này sẽ tiêu tốn 24,000 byte của bộ nhớ.

Bên trong mảng này, cấu trúc của Location được lưu trữ tuần tự mà không phải là các con trỏ đến 1,000 cấu trúc Location được lưu trữ ngẫu nhiên. Điều này rất quan trọng vì bây giờ toàn bộ 1,000 cấu trúc Location được lưu tuần tự trong cache, được đóng gói cùng nhau.

Go cho phép tạo ra các cấu trúc dữ liệu nhỏ gọn, tránh những chi phí gián tiếp không cần thiết.

Cấu trúc dữ liệu nhỏ sẽ được cache tốt hơn.

Cache tốt hơn sẽ dẫn đến hiệu năng tốt hơn.

2. Inlining

Mỗi lời gọi hàm đều tiêu tốn chi phí. Có ba điều diễn ra khi một hàm được gọi:

  • Một stack frame mới được tạo ra và thông tin chi tiết của đối tượng gọi hàm này được lưu lại
  • Bất kì thanh ghi nào có thể được ghi đè trong suốt quá trình gọi hàm đều được lưu lại trong stack
  • Bộ vi xử lý tính toán địa chỉ của hàm đó và thực thi một nhánh đến địa chỉ mới

Ví dụ về inlining:

package util

// Max returns the larger of a or b.
func Max(a int, b int) int {
    if a > b {
        return a
    }
    return b
}
package main

import util

// Double returns twice the value of the larger of a or b.
func Double(a int, b int) int {
    return 2 * util.Max(a, b)
}

Ví dụ này cho ta thấy hàm Double gọi util.Max.

Để tránh chi phí của việc gọi hàm util.Max, trình biên dịch có thể chuyển nội dung xử lý của hàm util.Max (inline) vào bên trong hàm Double. Kết quả là sẽ trông giống như sau:

Sau khi thực hiện inline:

func Double(a int, b int) int {
    temp := b                  //
    if a > b {                 // Nội dung xử lý của hàm util.Max được sao chép vào trong hàm Double
        temp = a               //
    }                          //
    return 2 * temp
}

Sau khi thực hiện inline ta không còn thấy lời gọi hàm đến util.Max nữa, nhưng logic xử lý của hàm Double vẫn không thay đổi.

Inlining không phải chỉ có mỗi Go có, nhiều trình biên dịch hoặc ngôn ngữ JITed cũng thực hiện việc tối ưu này. Nhưng cách Go thực hiện như thế nào?

Go cài đặt điều này rất đơn giản, khi một package được biên dịch, nhưng function đủ nhỏ để thực hiện inlining đều được đánh dấu và sau đó thực hiện biên dịch như bình thường.

Sau đó cả mã nguồn của hàm đó và phiên bản của lần biên dịch đều được lưu lại.

util.a

% strings ~/pkg/linux_amd64/util.a
...
package util
        import runtime "runtime"
        func @"".Max (@"".a
2 int, @"".b
3 int) (? int) { if @"".a
2 > @"".b
3 { return @"".a
2 }; return @"".b

Lược bỏ code chết

func Test() bool {
    return false
}

func Expensive() {
    if Test() {
        // something expensive
    }
}

Trong ví dụ trên, mặc dù hàm Test luôn luôn trả về giá trị false, hàm Expensive không thể biết rằng không cần thực thi nó.

Khi hàm Test được inline, chúng ta sẽ thấy một vài thứ giống như sau:

func Expensive() {
    if false {
        // something expensive is now unreachable
    }
}

Trình biên dịch bây giờ biết rằng đoạn code này không thể truy cập. Điều này không chỉ tiết kiệm chi phí vì không phải thực hiện gọi hàm Test, nó còn tiết kiệm trong quá trình biên dịch hoặc thực thi các đoạn code không bao giờ truy cập đến.

3. Escape analysis

Garbage collection giúp Go trở thành một ngôn ngữ đơn giản hơn và an toàn hơn.

Sự xuất hiện của garbage collection không có nghĩa là nó sẽ khiến Go chậm đi, hoặc nó có thể quyết định tốc độ xử lý chương trình của bạn.

Điều đó có nghĩa là bộ nhớ được phân vùng trên heap sẽ phải đi kèm với chi phí. Nó giống như một khoản nợ làm tốn thời gian xử lý của CPU mỗi khi GC chạy đến khi bộ nhớ đó được thu hồi.

Phân vùng nhớ của process

Hình 2

Tuy nhiên ta cũng có một cùng nhớ khác được gọi là Stack. Không giống như ngôn ngữ C đó là các giá trị sẽ luôn nằm ở vùng nhớ Heap khi sử dụng malloc, hoặc trên Stack nếu được khai báo bên trong phạm vi của một hàm, Go thực hiện một cải tiến gọi là escape analysis.

Escape analysis:

  • Xác định khi nếu có bất kì tham chiếu nào đến một giá trị thoát ra khỏi hàm mà giá trị được khai báo.
  • Nếu không có tham chiếu nào thoát ra ngoài, giá trị đó có thể được lưu trữ ở trên Stack.
  • Các giá trị được lưu ở trên Stack không cần phải cấp phát hoặc thu hồi.

Hãy xem qua một vài ví dụ sau:

// Sum returns the sum of the numbers 1 to 100.
func Sum() int {
    numbers := make([]int, 100) // numbers never escapes Sum()
    for i := range numbers {
        numbers[i] = i + 1
    }
    var sum int
    for _, i := range numbers {
        sum += i
    }
    return sum
}

Hàm Sum cộng các số từ 1 đến 100 và trả về kết quả. Đoạn code thực hiện logic này khá dị, nhưng đây là một ví dụ giúp ta hiểu cách Escape Analysis hoạt động.

numbers slice chỉ được tham chiếu bên trong hàm Sum, nên trình biên dịch sẽ lưu trữ slice 100 số nguyên đó ở trên vùng nhớ Stack mà không phải Heap. Vì vậy, sẽ không cần GC thủ hồi numbers, nó sẽ tự động được thu hồi sau khi hàm Sum kết thúc.

Ví dụ 2:

const Width, Height = 640,480
type Cursor struct {
    X, Y int
}

func Center(c *Cursor) { // Center() dosen't retain a reference to c
    c.X += Width / 2
    c.Y += Height / 2
}

func CenterCursor() {
    c := new(Cursor)        // c created with new() but never visible
    Center(c)               // outside CenterCursor(), so will be
    fmt.Println(c.X, c.Y)   // allocated on the stack
}

Trong hàm CenterCursor chúng ta tạo một Cursor mới và lưu một con trỏ của nó ở trong biến c.

Sau đó ta truyền c qua hàm Center() để thực hiện di chuyển Cursor đến giữa màn hình.

Và cuối cùng ta in ra giá trị XY của Cursor.

Ngay cả khi c được khởi tạo bằng hàm new, nó sẽ không được lưu ở trong phân vùng Heap, vì không có tham chiếu c thoát ra khỏi hàm CenterCursor.

% go build -gcflags=-m esc.go
# command-line-arguments
./esc.go:26 can inline Center       # Center() is inlined into CenterCursor()
./esc.go:33 inlining call to Center #
./esc.go:6 Sum make([]int, 100) does not escape # make([]int, 100) 
./esc.go:26 Cemter c does not escape # func Center(c *Cursor)
./esc.go:32 CenterCursor new (Cursor) does not escape # c:= new(Cursor)
./esc.go:34 NewPoint ... argument does not escape # fmt.Println(c.X, c.Y)

Escape analysis được thực hiện trong quá trình biên dịch, cấp phát trên Stack sẽ luôn nhanh hơn Heap.

4. Goroutines

Go có goroutine, là nền tảng cho khái niệm concurrency trong ngôn ngữ Go.

Trong quá khứ, máy tính chỉ chạy một process trong một thời điểm, sau những năm 60 ý tưởng multiprocessing hay time-sharing trờ nên phổ biến.

Trong một hệ thống time-sharing, hệ điều hành phải liên tục chuyển sự "chăm sóc" của CPU giữa các process bằng cách ghi lại trạng thái của process hiện tại, sau đó khôi phục trạng thái của một process khác.

Đây được gọi là process switching.

Chi phí process switch

Chi phí khi thực hiện một process switch:

  • Đầu tiên là kernel cần lưu nội dung của tất cả các thanh ghi của CPU cho process đó, sau đó khôi phục các giá trị cho process khác.
  • Kernel cũng cần xóa các ánh xạ của CPU từ virtual memory đến physical memory vì chúng chỉ hợp lệ với process hiện tại
  • Cuối cùng là chi phí để thực hiện chuyển đổi context (context switch) của hệ điều hành, và chi phí của chức năng lập lịch để chọn process tiếp theo chiếm CPU

Vì một process switch có thể xảy ra ở bất kì thời điểm nào trong một quá trình thực thi process, hệ điều hành cần lưu nội dung của tất cả các thanh ghi vì nó không biết hiện tại thanh ghi nào đang được sử dụng.

Blocking syscalls

// package runtime
TEXT        ·Syscall(SB),NOSPLIT,$0-56
            CALL    runtime·entersyscall(SB)
            ...
            MOVQ    8(SP), AX       // syscall number
            SYSCALL
            ...
            CALL    runtime·exitsyscall(SB)
            RET

Thread

  • Nhiều thread có thể cùng chia sẻ không gian địa chỉ ô nhớ
  • Việc tạo và chuyển đổi nhanh hơn so với các process

Điều này dẫn đến sự phát triển của thread, về mặt khái niệm thì thread và process giống nhau nhưng các thread chia sẻ không gian địa chỉ ô nhớ.

Vì các thread chia sẻ không gian địa chỉ ô nhớ, nên chúng nhỏ hơn process và cũng nhanh hơn khi tạo và nhanh hơn khi chuyển đổi giữa các thread với nhau.

Goroutine

Goroutine lấy ý tưởng từ Thread. Goroutine là co-operatively scheduled, không dựa vào kernel để quản lý time-sharing.

Việc chuyển đổi giữa các gorountine chỉ xảy ra ở thời điểm xác định, khi một lời gọi rõ ràng được tạo tới Go runtime scheduler.

Trình biên dịch biết các thanh ghi nào đang sử dụng và lưu chúng tự động.

z_img_001.jpg
view raw z_img_001.jpg hosted with ❤ by GitHub
z_img_002.jpg
view raw z_img_002.jpg hosted with ❤ by GitHub

26 tháng 8, 2018

Xử lý đồng thời trong ngôn ngữ lập trình Go

Xử lý đồng thời trong ngôn ngữ lập trình Go

Bằng một cách nào đó có thể bạn đã từng nghe tới ngôn ngữ lập trình Go. Ngôn ngữ lập trình này đang ngày càng phổ biến vì rất nhiều lý do hoàn toàn xứng đáng. Go xử lý nhanh, đơn giản và nó có một cộng đồng lớn ở đằng sau. Một trong những khía cạnh thú vị nhất của việc học ngôn ngữ này chính là mô hình xử lý đồng thời của nó. Nguyên tắc đồng thời của Go tạo ra các chương trình đồng thời, đa luồng đơn giản và rất thú vị. Tôi sẽ giới thiệu các nguyên tắc đồng thời của Go thông qua một số ví dụ minh họa dưới đây với hy vọng rằng nó sẽ làm cho các khái niệm này dễ dàng cho việc học tập, tìm hiểu nó ở trong tương lai. Bài viết này dành cho những người mới tham gia Go và muốn bắt đầu tìm hiểu về các thành phần chủ chốt trong xử lý đồng thời của ngôn ngữ lập trình Go: goroutinechannel.

Image-018

Chương trình đơn luồng và đa luồng

Bạn có thể đã viết nhiều chương trình đơn luồng trước đây. Một khuôn mẫu chung trong lập trình là có nhiều hàm thực hiện một tác vụ cụ thể, nhưng chúng không được gọi cho đến khi phần trước của chương trình lấy dữ liệu sẵn sàng cho hàm tiếp theo.

Hình 1

Sau đây là ví dụ đầu tiên của chúng ta, một chương trình khai thác quặng. Các hàm trong ví dụ này thực hiện: tìm quặng, khai thác quặng và luyện kim. Trong ví dụ của chúng ta, mỏ và quặng được biểu diễn dưới dạng một mảng các chuỗi, với mỗi hàm nhận và trả về một mảng các chuỗi đã qua xử lý. Đối với một ứng dụng đơn luồng, chương trình sẽ được thiết kế như sau.

Hình 2

Có 3 hàm chính. Một finder, một miner và một smelter. Trong phiên bản này của chương trình, các chức năng của chúng ta chạy trên một thread đơn, hàm này thực hiện ngay sau hàm kia - và thread đơn này (gopher có tên Gary) sẽ cần phải làm tất cả công việc.

func main() {
    theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
    foundOre := finder(theMine)
    minedOre := miner(foundOre)
    smelter(minedOre)
}

In ra mảng kết quả "ore" ở cuối mỗi hàm, chúng ta nhận được kết quả sau:

From Finder: [ore ore ore]
From Miner: [minedOre minedOre minedOre]
From Smelter: [smeltedOre smeltedOre smeltedOre]

Kiểu lập trình này có lợi ích đó là thiết kế dễ dàng, nhưng điều gì xảy ra khi bạn muốn tận dụng nhiều luồng và thực hiện các hàm độc lập với nhau? Đây là khi chúng ta cần đến lập trình đồng thời.

Hình 3

Thiết kế khai thác kiểu này sẽ hiệu quả hơn nhiều. Bây giờ, nhiều luồng (các gopher) đang hoạt động độc lập; do đó, Gary không phải đảm nhận một mình toàn bộ công việc nữa. Có một con gopher tìm quặng, một con khai thác quặng, và một con khác sẽ luyện kim - tất cả đều làm việc cùng một lúc.

Để chúng ta có thể đưa loại chức năng này vào mã của chúng ta, chúng ta sẽ cần hai thứ: một cách để tạo ra các gopher làm việc độc lập, và một cách để các gopher có thể giao tiếp (gửi quặng) với nhau. Đây là nơi các nguyên tố đồng thời của Go xuất hiện: goroutine và các channel.

Goroutine

Goroutine có thể được coi như một thread nhỏ. Tạo một goroutine dễ dàng như việc để bắt đầu gọi một hàm. Một ví dụ về việc nó dễ dàng như thế nào, hãy tạo ra hai hàm tìm kiếm (finder), gọi đến chúng bằng cách sử dụng từ khóa go, và cho in ra console mỗi khi tìm thấy "quặng" trong mỏ.

Hình 4

func main() {
    theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
    go finder1(theMine)
    go finder2(theMine)
    <-time.After(time.Second * 5) // you can ignore this for now
}

Đây là đầu ra từ chương trình của chúng ta:

Finder 1 found ore!
Finder 2 found ore!
Finder 1 found ore!
Finder 1 found ore!
Finder 2 found ore!
Finder 2 found ore!

Như bạn có thể thấy từ đầu ra ở trên, các máy tìm kiếm đang chạy đồng thời. Không có thứ tự thực sự trong những người tìm thấy quặng đầu tiên, và khi chạy nhiều lần, thứ tự không phải lúc nào cũng giống nhau.

Đây là một tiến bộ tuyệt vời! Bây giờ chúng ta có một cách dễ dàng để thiết lập một chương trình đa luồng (multi-gopher), nhưng điều gì sẽ xảy ra khi chúng ta cần các goroutine độc lập của chúng ta để giao tiếp với nhau? Chào mừng bạn đến với thế giới ma thuật của các channel.

Channel

Hình 5

Các channel cho phép các goroutine giao tiếp với nhau. Bạn có thể nghĩ về một channel như một đường ống, từ đó các goroutine có thể gửi và nhận thông tin từ các goroutine khác.

Hình 6

myFirstChannel := make(chan string)

Các goroutine có thể gửi và nhận trên channel. Điều này được thực hiện thông qua việc sử dụng một mũi tên (<-) trỏ theo hướng dữ liệu đang dịch chuyển.

Hình 7

myFirstChannel <- "hello" // Send
myVariable := <- myFirstChannel // Receive

Bây giờ bằng cách sử dụng một channel, chúng ta sẽ có được quặng ngay khi được tìm thấy bởi các gopher khảo sát gửi cho, mà không cần chờ đợi họ khám phá xong tất cả mọi thứ.

Hình 8

Tôi đã cập nhật lại ví dụ đặt các đoạn code tìm kiếm và các hàm khai thác thiết lập dưới dạng các hàm ẩn danh. Nếu bạn chưa bao giờ thấy các hàm lambda, bạn không nên tập trung quá nhiều vào phần đó của chương trình, chỉ cần biết rằng mỗi hàm được gọi với từ khóa go, vì vậy chúng đang được chạy theo cách riêng của chúng. Điều quan trọng là phải lưu ý đến cách thức các goroutine truyền thông tin với nhau bằng cách sử dụng channel, cụ thể là oreChan. Các hàm ẩn danh sẽ được giải thích ở cuối bài viết.

func main() {
    theMine := [5]string{"ore1", "ore2", "ore3"}
    oreChan := make(chan string)
    // Finder
    go func(mine [5]string) {
        for _, item := range mine {
            oreChan <- item // send
        }
    }(theMine)

    // Ore Breaker
    go func() {
        for i := 0; i < 3; i++ {
            foundOre := <-oreChan // receive
            fmt.Println("Miner: Received " + foundOre + " from finder")
        }
    }()
    <-time.After(time.Second * 5) // Again, ignore this for now
}

Trong đầu ra dưới đây, bạn có thể thấy rằng Miner của chúng ta nhận được các mẩu "quặng" một lúc từ việc đọc từ channel quặng (oreChan) ba lần.

Miner: Received ore1 from finder
Miner: Received ore2 from finder
Miner: Received ore3 from finder

Tuyệt vời, bây giờ chúng ta có thể gửi dữ liệu giữa các goroutine khác nhau (các gopher) trong chương trình của chúng ta. Trước khi chúng ta bắt đầu viết các chương trình phức tạp với các channel, trước tiên hãy tìm hiểu qua một số thuộc tính quan trọng của channel.

Channel Blocking

Các channel chặn các goroutine trong một vài tình huống khác nhau. Điều này cho phép các goroutine của chúng ta đồng bộ hóa với nhau trong một thời điểm, trước khi tiếp tục thực hiện tiếp các logic của chúng theo cách độc lập.

Blocking on a Send

Image-009

Một khi một goroutine (gopher) gửi đến một channel, việc gửi của goroutine này sẽ bị chặn cho đến khi một goroutine khác nhận được những gì được gửi trên channel đó.

Blocking on a Receive

Image-010

Tương tự như chặn sau khi gửi trên một channel, một goroutine có thể chặn chờ đợi để có được một giá trị từ một channel, mà không có gì được gửi đến nó.

Blocking có thể hơi khó hiểu lúc đầu, nhưng bạn có thể nghĩ nó giống như một giao dịch giữa hai goroutine (các gopher). Cho dù một gopher đang chờ đợi tiền hoặc gửi tiền, nó sẽ đợi cho đến khi đối tác khác trong giao dịch xuất hiện.

Bây giờ chúng ta có một ý tưởng về những cách khác nhau một goroutine có thể chặn trong khi giao tiếp thông qua một channel, cho phép thảo luận về hai loại khác nhau của các channel: không có bộ đệm, và đệm . Chọn loại channel bạn sử dụng có thể thay đổi cách hoạt động của chương trình.

Unbuffered Channel

Image-011

Chúng ta đã sử dụng các unbuffered channel trong tất cả các ví dụ trước. Điều làm cho chúng trở nên độc đáo đó là chỉ có một phần dữ liệu phù hợp được qua channel tại một thời điểm.

Buffered Channel

Image-012

Trong các chương trình đồng thời, thời gian không phải lúc nào cũng hoàn hảo. Trong ví dụ khai thác quặng của chúng ta, chúng ta có thể rơi vào một tình huống mà các gopher khảo sát của chúng ta có thể tìm thấy ba mảnh quặng trong thời gian bằng với thời gian mà các gopher phá quặng chỉ xử lý xong một mảnh quặng. Để không để cho các gopher khảo sát dành phần lớn thời gian của mình chờ đợi để gửi cho các gopher phá quặng một chút quặng cho đến khi nó có thể kết thúc, chúng ta có thể sử dụng một buffered channel. Hãy bắt đầu bằng cách tạo buffered channel có dung lượng là 3.

bufferedChan := make(chan string, 3)

Các buffered channel hoạt động tương tự như các unbuffered channel, nhưng với một lần - chúng ta có thể gửi nhiều phần dữ liệu đến channel này trước khi cần một goroutine khác đọc dữ liệu từ channel đó.

Image-013

bufferedChan := make(chan string, 3)
go func() {
    bufferedChan <- "first"
    fmt.Println("Sent 1st")
    bufferedChan <- "second"
    fmt.Println("Sent 2nd")
    bufferedChan <- "third"
    fmt.Println("Sent 3rd")
}()
<-time.After(time.Second * 1)
go func() {
    firstRead := <- bufferedChan
    fmt.Println("Receiving..")
    fmt.Println(firstRead)
    secondRead := <- bufferedChan
    fmt.Println(secondRead)
    thirdRead := <- bufferedChan
    fmt.Println(thirdRead)
}()

Thứ tự in giữa hai goroutine của chúng ta sẽ là:

Sent 1st
Sent 2nd
Sent 3rd
Receiving..
first
second
third

Để đơn giản, chúng ta sẽ không sử dụng các buffered channel trong chương trình cuối cùng của chúng ta, nhưng điều quan trọng là phải biết loại channel nào có sẵn trong vành đai công cụ đồng thời của bạn.

Lưu ý: Sử dụng buffered channel không ngăn chặn việc blocking xảy ra. Ví dụ, nếu gopher khảo sát nhanh hơn 10 lần so với gopher phá quặng, và chúng giao tiếp thông qua một buffered channel có kích thước là 2, thì gopher khảo sát sẽ vẫn bị chặn nhiều lần trong chương trình.

Kết hợp tất cả chúng cùng nhau

Bây giờ với sức mạnh của goroutine và các channel, chúng ta có thể viết một chương trình tận dụng tối đa nhiều luồng bằng cách sử dụng các nguyên tố đồng thời của Go.

Image-014

theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
oreChannel := make(chan string)
minedOreChan := make(chan string)
// Finder
go func(mine [5]string) {
    for _, item := range mine {
        if item == "ore" {
            oreChannel <- item // send item on oreChannel
        }
    }
}(theMine)
// Ore Breaker
go func() {
    for i := 0; i < 3; i++ {
        foundOre := <-oreChannel // read from oreChannel
        fmt.Println("From Finder: ", foundOre)
        minedOreChan <- "minedOre" // send to minedOreChan
    }
}()
// Smelter
go func() {
    for i := 0; i < 3; i++ {
        minedOre := <-minedOreChan // read from minedOreChan
        fmt.Println("From Miner: ", minedOre)
        fmt.Println("From Smelter: Ore is smelted")
    }
}()
<-time.After(time.Second * 5) // Again, you can ignore this

Đầu ra của chương trình này là như sau:

From Finder:  ore
From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted
From Miner:  minedOre
From Smelter: Ore is smelted
From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted

Đây là một cải tiến lớn từ ví dụ ban đầu của chúng ta! Bây giờ mỗi chức năng của chúng ta đang chạy độc lập trên các goroutine riêng của chúng. Ngoài ra, mỗi khi có một phần quặng được xử lý, nó được chuyển sang giai đoạn tiếp theo của dây chuyền khai thác.

Để giữ sự tập trung vào sự hiểu biết cơ bản về channel và thực hiện các thói quen, có một số thông tin quan trọng mà tôi không đề cập ở trên, nếu bạn không biết, có thể gây ra một số rắc rối khi bạn bắt đầu lập trình. Bây giờ bạn đã hiểu được cách thức hoạt động của các thường trình và channel, hãy xem qua một số thông tin bạn cần biết trước khi bắt đầu viết mã với các thường trình và channel đi.

Một số điểm chú ý

Các Goroutine ẩn danh

Image-015

Tương tự như cách chúng ta có thể thiết lập một hàm để chạy theo goroutine riêng của nó bằng cách sử dụng từ khóa go , chúng ta có thể tạo một hàm ẩn danh để chạy trên thường trình đi riêng của nó bằng cách sử dụng định dạng sau:

// Anonymous go routine
go func() {
    fmt.Println("I'm running in my own go routine")
}()

Bằng cách này, nếu chúng ta chỉ cần gọi một hàm một lần, chúng ta có thể đặt nó theo cách chạy riêng của nó để chạy, mà không cần lo lắng về việc tạo ra một khai báo hàm chính thức.

Hàm main là một Goroutine

Hình 16

Các chức năng chính thực sự chạy trong goroutine riêng của mình! Điều quan trọng cần biết là một khi hàm main trả về, nó đóng tất cả các thường trình đi khác hiện đang chạy. Đây là lý do tại sao chúng ta đã có một bộ đếm thời gian ở dưới cùng của chức năng chính của chúng ta - mà tạo ra một channel và gửi một giá trị vào nó sau 5 giây.

<-time.After(time.Second * 5) // Receiving from channel after 5 sec

Hãy nhớ làm thế nào một goroutine sẽ chặn trên một đọc cho đến khi một cái gì đó được gửi? Đó là chính xác những gì đang xảy ra với các thói quen chính bằng cách thêm mã này ở trên. Các thói quen chính sẽ chặn, cho goroutine khác của chúng ta 5 giây của cuộc sống bổ sung để chạy.

Bây giờ có nhiều cách tốt hơn để xử lý chặn chức năng chính cho đến khi tất cả các goroutine khác hoàn tất. Một thực tế phổ biến là tạo một channel đã hoàn thành mà chức năng chính sẽ chặn để chờ đọc. Khi bạn hoàn thành công việc của mình, hãy viết thư cho channel này và chương trình sẽ kết thúc.

Hình 17

func main() {
    doneChan := make(chan string)
    go func() {
        // Do some work…
        doneChan <- "I'm all done!"
    }()
 
    <-doneChan // block until go routine signals work is done
}

Bạn có thể lấy toàn bộ dữ liệu từ một Channel

Trong một ví dụ trước, chúng ta đã đọc miner của chúng ta từ một channel trong một vòng lặp for đã trải qua 3 lần lặp. Điều gì sẽ xảy ra nếu chúng ta không biết chính xác có bao nhiêu mẩu quặng sẽ đến từ công cụ tìm? Vâng, tương tự như sử dụng range trên một tập dữ liệu, bạn có thể sử dụng range trên một channel .

Cập nhật chức năng khai thác trước đây của chúng ta, chúng ta có thể viết như sau:

// Ore Breaker
go func() {
    for foundOre := range oreChan {
        fmt.Println("Miner: Received " + foundOre + " from finder")
    }
}()

Vì thợ mỏ cần phải đọc mọi thứ mà người khảo sát gửi cho anh ta, sử dụng range trên một channel ở đây đảm bảo chúng ta nhận được mọi thứ được gửi đến.

Lưu ý: Việc phân luồng qua channel sẽ chặn cho đến khi một mục khác được gửi đến channel này. Cách duy nhất để ngăn chặn goroutine từ chặn sau khi tất cả gửi đã xảy ra là bằng cách đóng channel bằng close(channel)

Bạn có thể không bị chặn khi đọc dữ liệu từ Channel

Có một kỹ thuật mà bạn có thể thực hiện đọc mà không bị chặn trên một channel, bằng cách sử dụng cấu trúc select case của Go . Bằng cách sử dụng như ví dụ bên dưới, goroutine của bạn sẽ đọc dữ liệu từ channel nếu có nội dung nào đó hoặc sẽ nhảy vào trường hợp mặc định.

myChan := make(chan string)
 
go func(){
    myChan <- "Message!"
}()
 
select {
    case msg := <- myChan:
        fmt.Println(msg)
    default:
        fmt.Println("No Msg")
}
<-time.After(time.Second * 1)
select {
    case msg := <- myChan:
        fmt.Println(msg)
    default:
        fmt.Println("No Msg")
}

Khi chạy, ví dụ này có đầu ra sau:

No Msg
Message!

Bạn cũng có thể không chặn việc gửi đến một Channel

Không chặn việc gửi dữ liệu cũng sử dụng chung một cấu trúc select case, sự khác biệt duy nhất là trường hợp chúng ta sẽ trông giống như gửi chứ không phải là nhận.

select {
    case myChan <- "message":
        fmt.Println("sent the message")
    default:
        fmt.Println("no message sent")
}
z_img_001.jpeg
view raw z_img_001.jpeg hosted with ❤ by GitHub
z_img_002.jpeg
view raw z_img_002.jpeg hosted with ❤ by GitHub
z_img_003.jpeg
view raw z_img_003.jpeg hosted with ❤ by GitHub
z_img_004.jpeg
view raw z_img_004.jpeg hosted with ❤ by GitHub
z_img_005.jpeg
view raw z_img_005.jpeg hosted with ❤ by GitHub
z_img_006.jpeg
view raw z_img_006.jpeg hosted with ❤ by GitHub
z_img_007.jpeg
view raw z_img_007.jpeg hosted with ❤ by GitHub
z_img_008.jpeg
view raw z_img_008.jpeg hosted with ❤ by GitHub
z_img_009.jpeg
view raw z_img_009.jpeg hosted with ❤ by GitHub
z_img_010.jpeg
view raw z_img_010.jpeg hosted with ❤ by GitHub
z_img_011.jpeg
view raw z_img_011.jpeg hosted with ❤ by GitHub
z_img_012.jpeg
view raw z_img_012.jpeg hosted with ❤ by GitHub
z_img_013.jpeg
view raw z_img_013.jpeg hosted with ❤ by GitHub
z_img_014.jpeg
view raw z_img_014.jpeg hosted with ❤ by GitHub
z_img_015.jpeg
view raw z_img_015.jpeg hosted with ❤ by GitHub
z_img_016.jpeg
view raw z_img_016.jpeg hosted with ❤ by GitHub
z_img_017.jpeg
view raw z_img_017.jpeg hosted with ❤ by GitHub
z_img_018.jpeg
view raw z_img_018.jpeg hosted with ❤ by GitHub

25 tháng 5, 2018

So sánh dữ liệu trong ngôn ngữ Go

1. Boolean

Kiểu dữ liệu boolean có thể được đối chiếu giữa những giá trị true hoặc false được định nghĩa từ trước (hoặc các biểu thức điều kiện). Lỗi sẽ xảy ra nếu như ta so sánh một giá trị kiểu boolean với một giá trị là kiểu số.

var a := true

if a != (10 == 20) {
    fmt.Println("a not true")
}

// Xảy ra lỗi trong quá trình biên dịch
if a == 1 { ... }

2. Kiểu số nguyên và kiểu số động

So sánh các giá trị kiểu số hoạt động như thông thường, chúng được hỗ trợ cả hai kiểu so sánh tương đường và so sánh thứ tự.

import (
    "fmt"
    "math"
)

func main() {
    a := 3.1415
    if a != math.Pi {
        fmt.Println("a is not pi")
    }
}

Tuy nhiên, do tính nghiêm ngặt giữa các kiểu dữ liệu trong ngôn ngữ Go, nên một giá trị kiểu số nguyên chỉ có thể so sánh với một giá trị kiểu số nguyên khác, và một giá trị kiểu số động chỉ có thể so sánh với một giá trị kiểu số động khác (hoặc biểu thức tạo chúng). Nếu bạn cố thử so sánh giá trị kiểu số nguyên với giá trị kiểu số động, thì bạn sẽ cần phải chuyển đổi chúng về cùng kiểu nếu không sẽ xảy ra lỗi trong quá trình biên dịch.

func main() {
    a := 3.1415
    b := 6
    if a != b {
        fmt.Println("a is not b")
    } else if a <= b {
        fmt.Println("a is in the right position")
    }
}
// Lỗi biên dịch:
// operation: a != b (mismatched types float64 and int)

Trong ví dụ trên, đoạn code này sẽ chỉ được biên dịch thành công nếu cả hai biến được khai báo với kiểu dữ liệu giống nhau hoặc chuyển đổi về cùng kiểu dữ liệu.

func main() {
    a := 3.1415
    b := 6
    if a != float64(b) {
        fmt.Println("a is not b")
    }
}

3. Kiểu số phức

Các giá trị số phức cũng có thể được kiểm tra tương đương. Hai số phức tương đương với nhau nếu phần thực và phần ảo tương ứng của chúng bằng nhau.

func main() {
    a := complex(-3.25, 2)
    b := -3.25 + 2i
    if a == b {
        fmt.Println("a complex as b")
    }
}

Tuy nhiên, do tính chất của số phức, chúng không được hỗ trợ so sánh lớn hơn, nhỏ hơn ở trong Go.

func main() {
  a := complex(-3.25, 2)
  b := -3.25 + 2i
  if a < b {
    fmt.Println("a complex as b")
  }
}
// Lỗi biên dịch
// invalid operation: a <= b (operator <= not defined on complex128)

4. String

Trong Go, các giá trị chuỗi hỗ trợ cả hai kiểu so sánh tương đương và so sánh lớn hơn, nhỏ hơn. Không có các chức năng bổ sung để so sánh các chuỗi. Giá trị có thể được tự động so sánh sử dụng từ điển bằng các toán tử ==, !=, <=, <, >, và >=.

func main() {
    cols := []string{
        "xanadu", "red", "fulvous",
        "white", "green", "blue",
        "orange", "black", "almond"
    }
    for _, col := range cols {
        if col >= "red" || col == "black" {
            fmt.Println(col)
        }
    }
}

5. Struct

Hai giá trị struct có thể được kiểm tra tính tương đương bằng cách so sánh các giá trị riêng lẻ của chúng. Nói chung, hai giá trị struct được quyết định là tương đương nếu chúng có cùng kiểu và các trường tương ứng của chúng tương đương với nhau.

func main() {
    p1 := struct {a string; b int}{"left", 4}
    p2 := struct {a string; b int}{a: "left", b: 4}
    if p1 == p2 {
        fmt.Println("Same position")
    }
}

Trong đoạn code phía trên, struct p1 tương đương với struct p2 vì chúng có cùng kiểu dữ liệu và các trường tương ứng của chúng có giá trị giống nhau. Bất kì sự thay đổi nào trong giá trị các trường sẽ gây ra các việc struct không còn tương đương với nhau nữa.

Tuy nhiên, các giá trị struct không thể so sánh được bằng các toán tử so sánh lớn hơn, nhỏ hơn. Vì vậy đoạn code dưới đây sẽ biên dịch lỗi:

func main() {
    p1 := struct {a string; b int}{"left", 4}
    p2 := struct {a string; b int}{a: "left", b: 4}
    if p1 > p2 {
        fmt.Println("Same position")
    }

    // Lỗi biên dịch
    // invalid operation: p1 > p2 (operator > not defined on struct)
}

6. Array

Các giá trị mảng được so sánh tương đương bằng cách so sánh các phần tử của các loại dữ liệu xác định. Các mảng được quyết định là tương đương nếu như các phần tử tương ứng trong chúng tương đương với nhau.

func main() {
    pair1 := [2]int {4, 2}
    pair2 := [2]int {2, 4}
    if pair1 != pair2 {
        fmt.Println("different pair")
    }
}

Giống như các giá trị struct, ta không thể so sánh các mảng bằng các toán tử so sánh lớn hơn, nhỏ hơn <, <=, >, >=. Nếu cố thử làm điều đó sẽ gây ra lỗi khi biên dịch.

7. Pointer

Các giá trị pointer có thể được so sánh tương đương nhưng so sánh lớn hơn, nhỏ hơn thì không. Hai gá trị pointer được quyết định là tương đương nếu chúng trỏ đến cùng một giá trị trong bộ nhớ (hoặc nếu chúng cùng là nil). Ví dụ, &pair trong đoạn code dưới đây sẽ tương đương với ptr2, trong khi &pairptr thì không tương đương.

func main() {
    pair := [2]int {4, 2}
    ptr := &[2]int {4, 2}
    ptr2 := &pair

    if &pair != ptr {
        fmt.Println("pointing different")
    }
    if &pair == ptr2 {
        fmt.Println("pointing the same")
    }
}

8. Interface

Các giá trị interface không những có thể được so sánh với các giá trị interface khác, mà còn có thể so sánh với các kiểu dữ liệu implement chúng.

Hai giá trị interface được quyết định là bằng nhau nếu các kiểu dữ liệu cụ thể, các giá trị của chúng có thể so sánh được và tương đương với nhau, hoặc cả hai interface đều là nil.

Ví dụ, trong đoạn code dưới đây, hai giá trị interface r0r2 là tương đương vì chúng implement cùng kiểu dữ liệu cụ thể và cùng giá trị như nhau, rectangle{l:3, w:6}. Mặt khác, hai giá trị interface r0r1 mặc dù cùng implement interface giống nhau, nhưng các giá trị khác nhau nên chúng sẽ không tương đương, rectangle{3, 6} vs rectangle{6, 3}. Tương tự, các biến r1s0 không tương đương vì chúng có các giá trị khác nhau mặc dù chùng cùng implement một interface.

type shape interface {
    area() int
}
type rectangle struct {
     l int
     w int
}
func (r rectangle) area() int {
    return r.l * r.w
}
type square struct {
    l int
}
func (s square) area() int {
     return s.l * s.l
}
func main() {
   var r0 shape = rectangle{3, 6}
   var r1 shape = rectangle{6, 3}
   var r2 shape = rectangle{3, 6}
   var s0 shape = square{5}
 
   if r0 == r2 {
     fmt.Println("r0 and r2 same shapes")
   }
 
   fmt.Println("r1 and s0 equal", (r1 == s0))
}

Một điều quan trọng cần chú ý đó là, nếu các kiểu dữ liệu của interface không thể so sánh mà ta vẫn cố thử so sánh chúng thì sẽ gây ra lỗi trong quá trình runtime.

9. Chanel

Các giá trị Chanel chỉ có thể sử dụng so sánh tương đương. Hai giá trị chanel được cho là tương đương nhau nếu chúng bắt nguồn từ cùng một câu lệnh make (nghĩa là chúng cùng tham chiếu đến một giá trị chanel trong bộ nhớ).

Ví dụ, trong ví dụ sau đây, ch0 sẽ không tương đương với ch1 ngay cả khi chúng có cùng kiểu dữ liệu. Tuy nhiên, ch1 sẽ tương đương với ch2 vì cả hai đều tham chiếu đến cùng một chanel.

func main() {
    ch0 := make(chan int)
    ch1 := make(chan int)
    ch2 := ch1

    fmt.Println("ch0 == ch1", (ch0 == ch1))
    fmt.Println("ch1 == ch2", (ch1 == ch2))
}