forked from elixirschool/elixirschool
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[TH] Initial Translation for Concurrency (elixirschool#1201)
* [TH] Initial Thai Translation for the Concurrency Lesson * [TH] Added English Terminology for Concurrency with the Thai Title
- Loading branch information
Showing
1 changed file
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
--- | ||
version: 1.0.0 | ||
title: การทำงานพร้อมกัน (Concurrency) | ||
--- | ||
|
||
หนึ่งในจุดขายหลักของ Elixir คือการรองรับการทำงานพร้อมๆ กัน ซึ่งเป็นผลพลอยได้จากการนำ Erlang VM (BEAM) มาใช้งาน ส่งผลให้การจัดการการทำงานพร้อมกันใน Elixir นั้นง่ายกว่าที่คิดมากๆ | ||
|
||
โดยรูปแบบการทำงานพร้อมๆ กันนั้น มีความจำเป็นต้องพึ่งพา actor ซึ่งเป็นโปรเซสที่สามารถสื่อสารกับโปรเซสอื่นๆ ได้โดยผ่านการส่งข้อความหากัน | ||
|
||
ในบทเรียนนี้ เราจะมาลองเรียนรู้ถึงโมดูลที่เอาไว้ใช้จัดการเรื่องการทำงานพร้อมกัน ที่มาพร้อมกับ Elixir ซึ่งในบทต่อๆ ไป เราจะพูดถึงคุณลักษณะต่างๆ ของ OTP ที่นำเรื่องการทำงานพร้อมการมาใช้ด้วย | ||
|
||
{% include toc.html %} | ||
|
||
## โปรเซส (Processes) | ||
|
||
โปรเซสต่างๆ ใน Erlang VM นั้นเบามากๆ และจะถูกนำไปประมวลผลในทุกๆ CPU ในเครื่อง ซึ่งตัวโปรเซสนั้นอาจจะดูเหมือน thread ที่มีอยู่ตามปกติ แต่จริงๆ แล้วมีความซับซ้อนน้อยกว่ากันมาก และไม่ใช่เรื่องแปลกเลยที่จะมีหลายพันโปรเซสทำงานพร้อมกันอยู่ในโปรแกรมเดียว | ||
|
||
วิธีที่ง่ายที่สุดที่จะสร้างโปรเซสใหม่ คือการใช้คำสั่ง `spawn` ซึ่งสามารถใส่ฟังก์ชั่นแบบไม่มีชื่อหรือมีชื่อเข้าไปก็ได้ และเมื่อเราสร้างโปรเซสใหม่ขึ้นมา เราจะได้รับ process identifier หรือที่เรียกกันว่า PID กลับมา เพื่อที่จะได้เรียกมันได้ถูกภายในโปรแกรมของเรา | ||
|
||
เราจะเริ่มจากการสร้างโมดูลใหม่ขึ้นมา และสร้างฟังก์ชั่นที่เราอยากจะเรียกใช้: | ||
|
||
```elixir | ||
defmodule Example do | ||
def add(a, b) do | ||
IO.puts(a + b) | ||
end | ||
end | ||
|
||
iex> Example.add(2, 3) | ||
5 | ||
:ok | ||
``` | ||
|
||
ถ้าเราต้องการเรียกใช้ฟังก์ชั่นนี้แบบ asynchronous เราจะต้องใช้คำสั่ง `spawn/3` แทน: | ||
|
||
```elixir | ||
iex> spawn(Example, :add, [2, 3]) | ||
5 | ||
#PID<0.80.0> | ||
``` | ||
|
||
### การส่งข้อความหากัน (Message Passing) | ||
|
||
เพื่อที่จะทำการสื่อสารต่างๆ โปรเซสจำเป็นจะต้องใช้การส่งข้อความหากัน ซึ่งต้องมีคำสั่งสองส่วนคือ `send/2` และ `receive` | ||
|
||
โดยที่ฟังก์ชั่น `send/2` ทำให้เราสามารถส่งข้อความไปยัง PIDs ต่างๆ ได้ และสามารถรับข้อความที่ถูกส่งมาโดยใช้คำสั่ง `receive` เพื่อ match กับข้อความ โดยถ้าไม่เจอ match การทำงานก็จะดำเนินต่อไปเรื่อยๆ โดยไม่ต้องหยุดรอ | ||
|
||
```elixir | ||
defmodule Example do | ||
def listen do | ||
receive do | ||
{:ok, "hello"} -> IO.puts "World" | ||
end | ||
|
||
listen | ||
end | ||
end | ||
|
||
iex> pid = spawn(Example, :listen, []) | ||
#PID<0.108.0> | ||
|
||
iex> send pid, {:ok, "hello"} | ||
World | ||
{:ok, "hello"} | ||
|
||
iex> send pid, :ok | ||
:ok | ||
``` | ||
|
||
คุณอาจสังเกตว่าฟังก์ชั่น `listen/0` นั้นเป็นแบบ recursive ซึ่งทำให้โปรเซสของเราสามารถรับข้อความได้หลายๆ ข้อความ เพราะถ้าไม่มี recursion โปรเซสของเราก็จะหยุดลงตั้งแต่ตอนที่รับข้อความแรกแล้ว | ||
|
||
### การเชื่อมโยงโปรเซสเข้าด้วยกัน (Process Linking) | ||
|
||
ปัญหาหนึ่งของ `spawn` คือการที่ไม่รู้ว่าโปรเซสจะพังลงเมื่อไหร่ และถ้าต้องการจะรู้ เราต้องทำการเชื่อมโยงโปรเซสต่างๆ เข้าด้วยกันโดยใช้คำสั่ง `spawn_link` ซึ่งโปรเซสที่เชื่อมโยงกันจะได้รับการแจ้งเตือนเมื่ออีกโปรเซสหยุดการทำงานลง: | ||
|
||
```elixir | ||
defmodule Example do | ||
def explode, do: exit(:kaboom) | ||
end | ||
|
||
iex> spawn(Example, :explode, []) | ||
#PID<0.66.0> | ||
|
||
iex> spawn_link(Example, :explode, []) | ||
** (EXIT from #PID<0.57.0>) evaluator process exited with reason: :kaboom | ||
``` | ||
|
||
ในบางครั้ง เราก็ไม่ต้องการให้โปรเซสที่เชื่อมโยงกันไปทำให้โปรเซสที่ทำงานอยู่พังลง เราจึงต้องดักฟังสัญญาณหยุดการทำงาน ซึ่งเราจะได้รับข้อความออกมาเป็น tuple แบบนี้: `{:EXIT, from_pid, reason}` | ||
|
||
```elixir | ||
defmodule Example do | ||
def explode, do: exit(:kaboom) | ||
def run do | ||
Process.flag(:trap_exit, true) | ||
spawn_link(Example, :explode, []) | ||
|
||
receive do | ||
{:EXIT, from_pid, reason} -> IO.puts "Exit reason: #{reason}" | ||
end | ||
end | ||
end | ||
|
||
iex> Example.run | ||
Exit reason: kaboom | ||
:ok | ||
``` | ||
|
||
### การติดตามดูสถานะของโปรเซส (Process Monitoring) | ||
|
||
จะเกิดอะไรขึ้นถ้าหากเราไม่อยากที่จะเชื่อมโยงสองโปรเซสเข้าด้วยกัน แต่ก็ยังอยากที่จะได้รับการแจ้งเตือนอยู่ดีล่ะ | ||
|
||
เพราะเหตุนี้ เราจึงสามารถติดตามสถานะของโปรเซสได้ ผ่านคำสั่ง `spawn_monitor` โดยขณะที่เรากำลังติดตามสถานะอยู่นั้น เราจะได้รับข้อความเมื่อโปรเซสของเราพังลง โดยที่โปรเซสหลักจะไม่พังตามไปด้วย และก็ไม่จำเป็นที่จะต้องไปดักฟังสัญญาณหยุดการทำงานด้วยเช่นกัน | ||
|
||
```elixir | ||
defmodule Example do | ||
def explode, do: exit(:kaboom) | ||
def run do | ||
{pid, ref} = spawn_monitor(Example, :explode, []) | ||
|
||
receive do | ||
{:DOWN, ref, :process, from_pid, reason} -> IO.puts "Exit reason: #{reason}" | ||
end | ||
end | ||
end | ||
|
||
iex> Example.run | ||
Exit reason: kaboom | ||
:ok | ||
``` | ||
|
||
## Agents | ||
|
||
agent คือ abstraction ของโปรเซสที่ทำงานอยู่ในเบื้องหลัง ซึ่งมีการเก็บสถานะ (state) อยู่ในตัว โดยเราสามารถเข้าถึงมันจากโปรเซสอื่นๆ ภายในโปรแกรมของเรา และสถานะของ agent ถูกกำหนดให้เป็นค่าที่ถูกส่งกลับมาจากฟังก์ชั่นของเรา: | ||
|
||
```elixir | ||
iex> {:ok, agent} = Agent.start_link(fn -> [1, 2, 3] end) | ||
{:ok, #PID<0.65.0>} | ||
|
||
iex> Agent.update(agent, fn (state) -> state ++ [4, 5] end) | ||
:ok | ||
|
||
iex> Agent.get(agent, &(&1)) | ||
[1, 2, 3, 4, 5] | ||
``` | ||
|
||
เมื่อเราตั้งชื่อให้ agent แล้ว เราสามารถเรียกมันด้วยชื่อได้เลย แทนที่จะเรียกด้วย PID: | ||
|
||
```elixir | ||
iex> Agent.start_link(fn -> [1, 2, 3] end, name: Numbers) | ||
{:ok, #PID<0.74.0>} | ||
|
||
iex> Agent.get(Numbers, &(&1)) | ||
[1, 2, 3] | ||
``` | ||
|
||
## หน้าที่การทำงาน (Task) | ||
|
||
task เป็นวิธีที่จะช่วยให้เราสามารถเรียกใช้ฟังก์ชั่นได้ในเบื้องหลัง และรับค่าที่ถูกส่งกลับออกมาได้ในภายหลัง ซึ่งมีความจำเป็นอย่างมากเมื่อเราต้องการที่จะจัดการกับการทำงานที่ใช้การประมวลผลเยอะๆ โดยที่ไม่ต้องไปหยุดการทำงานของโปรแกรมหลัก | ||
|
||
```elixir | ||
defmodule Example do | ||
def double(x) do | ||
:timer.sleep(2000) | ||
x * 2 | ||
end | ||
end | ||
|
||
iex> task = Task.async(Example, :double, [2000]) | ||
%Task{pid: #PID<0.111.0>, ref: #Reference<0.0.8.200>} | ||
|
||
# Do some work | ||
|
||
iex> Task.await(task) | ||
4000 | ||
``` |