ทำไมภาษาที่ใช้งานได้ (โดยเฉพาะ Erlang) จึงปรับขนาดได้ดี?


92

ฉันเฝ้าดูการมองเห็นที่เพิ่มขึ้นของภาษาการเขียนโปรแกรมที่ใช้งานได้และคุณสมบัติมาระยะหนึ่งแล้ว ฉันตรวจสอบพวกเขาและไม่เห็นเหตุผลของการอุทธรณ์

จากนั้นเมื่อเร็ว ๆ นี้ผมได้เข้าร่วมเควินสมิ ธ "พื้นฐานของ Erlang" นำเสนอต่อที่Codemash

ฉันสนุกกับการนำเสนอและได้เรียนรู้ว่าคุณลักษณะมากมายของการเขียนโปรแกรมเชิงฟังก์ชันช่วยให้หลีกเลี่ยงปัญหาเธรด / การทำงานพร้อมกันได้ง่ายขึ้นมาก ฉันเข้าใจว่าการขาดสถานะและความไม่แน่นอนทำให้หลายเธรดเปลี่ยนแปลงข้อมูลเดียวกันไม่ได้ แต่เควินกล่าวว่า (ถ้าฉันเข้าใจถูกต้อง) การสื่อสารทั้งหมดเกิดขึ้นผ่านข้อความและข้อความจะถูกประมวลผลพร้อมกัน (อีกครั้งเพื่อหลีกเลี่ยงปัญหาการเกิดพร้อมกัน)

แต่ฉันได้อ่านพบว่า Erlang ใช้ในแอปพลิเคชันที่ปรับขนาดได้สูง (เหตุผลทั้งหมดที่ Ericsson สร้างขึ้นตั้งแต่แรก) จะมีประสิทธิภาพในการจัดการคำขอหลายพันรายการต่อวินาทีได้อย่างไรหากจัดการทุกอย่างเป็นข้อความที่ประมวลผลแบบซิงโครนัส นั่นไม่ใช่เหตุผลที่เราเริ่มก้าวไปสู่การประมวลผลแบบอะซิงโครนัสดังนั้นเราจึงสามารถใช้ประโยชน์จากการรันเธรดการดำเนินการหลายชุดพร้อมกันและบรรลุความสามารถในการปรับขนาด ดูเหมือนว่าสถาปัตยกรรมนี้จะปลอดภัยกว่า แต่เป็นการถอยหลังในแง่ของความสามารถในการปรับขนาดได้ ฉันขาดอะไรไป?

ฉันเข้าใจว่าผู้สร้าง Erlang ตั้งใจที่จะหลีกเลี่ยงการสนับสนุนเธรดเพื่อหลีกเลี่ยงปัญหาการทำงานพร้อมกัน แต่ฉันคิดว่าการทำงานแบบมัลติเธรดเป็นสิ่งที่จำเป็นเพื่อให้สามารถปรับขนาดได้

ภาษาโปรแกรมที่ใช้งานได้จะปลอดภัยต่อเธรดโดยเนื้อแท้ได้อย่างไร แต่ก็ยังคงปรับขนาดได้


1
[ไม่ได้กล่าวถึง]: VM ของ Erlangs ยกระดับความเป็นอะซิงโครนัสไปสู่อีกระดับหนึ่ง โดยเวทวูดู (asm) จะอนุญาตให้ทำการซิงค์เช่นซ็อกเก็ต: อ่านเพื่อบล็อกโดยไม่ต้องหยุดเธรดระบบปฏิบัติการ สิ่งนี้ช่วยให้คุณสามารถเขียนรหัสที่เป็นซิงโครนัสเมื่อภาษาอื่นบังคับให้คุณเข้าสู่รัง async-callback มันง่ายกว่ามากในการเขียนแอพปรับขนาดด้วยภาพความคิดของบริการไมโครเธรดเดียว VS โดยคำนึงถึงภาพรวมทุกครั้งที่คุณวางอะไรลงบนฐานรหัส
Vans S

@Vans S น่าสนใจ
Jim Anderson

คำตอบ:


99

ภาษาที่ใช้งานได้ไม่ (โดยทั่วไป) อาศัยการกลายพันธุ์ตัวแปร ด้วยเหตุนี้เราจึงไม่จำเป็นต้องป้องกัน "สถานะที่ใช้ร่วมกัน" ของตัวแปรเนื่องจากค่าได้รับการแก้ไขแล้ว สิ่งนี้จะหลีกเลี่ยงการกระโดดข้ามส่วนใหญ่ที่ภาษาดั้งเดิมต้องผ่านเพื่อนำอัลกอริทึมไปใช้กับโปรเซสเซอร์หรือเครื่อง

Erlang ใช้ประโยชน์ได้มากกว่าภาษาที่ใช้งานได้ทั่วไปโดยการอบในระบบส่งข้อความที่ช่วยให้ทุกอย่างทำงานบนระบบตามเหตุการณ์ที่โค้ดส่วนหนึ่งกังวลเฉพาะการรับข้อความและการส่งข้อความโดยไม่ต้องกังวลกับภาพที่ใหญ่

สิ่งนี้หมายความว่าโปรแกรมเมอร์ (ในนาม) ไม่มั่นใจว่าข้อความจะถูกจัดการบนโปรเซสเซอร์หรือเครื่องอื่น: เพียงแค่ส่งข้อความก็เพียงพอแล้วที่จะดำเนินการต่อ ถ้ามันใส่ใจเกี่ยวกับการตอบสนองก็จะรอให้มันเป็นข้อความอื่น

ผลลัพธ์สุดท้ายของสิ่งนี้ก็คือข้อมูลโค้ดแต่ละตัวอย่างไม่ขึ้นกับตัวอย่างข้อมูลอื่น ๆ ไม่มีรหัสที่ใช้ร่วมกันไม่มีสถานะที่ใช้ร่วมกันและการโต้ตอบทั้งหมดที่มาจากระบบข้อความที่สามารถแจกจ่ายระหว่างฮาร์ดแวร์หลาย ๆ ชิ้น (หรือไม่ก็ได้)

ตรงกันข้ามกับระบบดั้งเดิม: เราต้องวาง mutexes และ semaphores ไว้รอบ ๆ ตัวแปรที่ "ป้องกัน" และการเรียกใช้โค้ด เรามีการผูกแน่นในการเรียกใช้ฟังก์ชันผ่านสแต็ก (รอให้การกลับมาเกิดขึ้น) ทั้งหมดนี้ก่อให้เกิดปัญหาคอขวดที่มีปัญหาน้อยกว่าในระบบที่ไม่ใช้ร่วมกันเช่น Erlang

แก้ไข: ฉันควรชี้ให้เห็นว่า Erlang เป็นแบบอะซิงโครนัส คุณส่งข้อความของคุณและสักวันอาจมีข้อความอื่นกลับมา หรือไม่.

ประเด็นของ Spencer เกี่ยวกับการไม่ดำเนินการตามคำสั่งก็มีความสำคัญและได้รับคำตอบเช่นกัน


ฉันเข้าใจสิ่งนี้ แต่ไม่เห็นว่ารูปแบบข้อความมีประสิทธิภาพอย่างไร ฉันจะเดาตรงกันข้าม นี่คือการเปิดหูเปิดตาของฉันอย่างแท้จริง ไม่น่าแปลกใจที่ภาษาโปรแกรมที่ใช้งานได้กำลังได้รับความสนใจอย่างมาก
Jim Anderson

3
คุณมีศักยภาพในการทำงานพร้อมกันจำนวนมากในระบบที่ไม่ใช้ร่วมกัน การใช้งานที่ไม่ดี (เช่นข้อความสูงที่ส่งผ่านค่าใช้จ่าย) อาจทำให้เกิดความเสียหายได้ แต่ Erlang ดูเหมือนจะทำให้ถูกต้องและทำให้ทุกอย่างมีน้ำหนักเบา
Godeke

สิ่งสำคัญคือต้องทราบว่าในขณะที่ Erlang มีข้อความที่ส่งผ่านความหมาย แต่ก็มีการใช้งานหน่วยความจำแบบแบ่งใช้ดังนั้นจึงมีการอธิบายความหมาย แต่ไม่ได้คัดลอกสิ่งต่างๆไปทั่วสถานที่หากไม่จำเป็นต้องทำ
Aaron Maenpaa

1
@Godeke: "Erlang (เช่นเดียวกับภาษาที่ใช้งานได้ส่วนใหญ่) จะเก็บข้อมูลเพียงอินสแตนซ์เดียวเมื่อเป็นไปได้" AFAIK, Erlang คัดลอกทุกอย่างที่ผ่านระหว่างกระบวนการที่มีน้ำหนักเบาเนื่องจากไม่มี GC พร้อมกัน
JD

1
@JonHarrop เกือบจะถูกต้อง: เมื่อกระบวนการส่งข้อความไปยังกระบวนการอื่นข้อความจะถูกคัดลอก ยกเว้นไบนารีขนาดใหญ่ซึ่งส่งผ่านโดยการอ้างอิง ดูเช่นjlouisramblings.blogspot.hu/2013/10/embrace-copying.htmlว่าเหตุใดจึงเป็นสิ่งที่ดี
hcs42

74

ระบบคิวข้อความนั้นยอดเยี่ยมเพราะสร้างเอฟเฟกต์ "ไฟและรอผลลัพธ์" ได้อย่างมีประสิทธิภาพซึ่งเป็นส่วนซิงโครนัสที่คุณกำลังอ่านอยู่ สิ่งที่ทำให้สิ่งนี้ยอดเยี่ยมอย่างไม่น่าเชื่อก็คือมันหมายความว่าไม่จำเป็นต้องดำเนินการบรรทัดตามลำดับ พิจารณารหัสต่อไปนี้:

r = methodWithALotOfDiskProcessing();
x = r + 1;
y = methodWithALotOfNetworkProcessing();
w = x * y

พิจารณาสักครู่ว่า methodWithALotOfDiskProcessing () ใช้เวลาประมาณ 2 วินาทีในการดำเนินการและ methodWithALotOfNetworkProcessing () ใช้เวลาประมาณ 1 วินาทีในการดำเนินการ ในภาษาขั้นตอนรหัสนี้จะใช้เวลาประมาณ 3 วินาทีในการรันเนื่องจากบรรทัดจะถูกดำเนินการตามลำดับ เรากำลังเสียเวลาในการรอให้วิธีการหนึ่งเสร็จสมบูรณ์ซึ่งสามารถทำงานพร้อมกันกับอีกวิธีหนึ่งโดยไม่ต้องแย่งชิงทรัพยากรเพียงแหล่งเดียว ในบรรทัดภาษาที่ใช้งานได้ของรหัสไม่ได้กำหนดว่าเมื่อใดที่โปรเซสเซอร์จะพยายามใช้ ภาษาที่ใช้งานได้จะลองทำสิ่งต่อไปนี้:

Execute line 1 ... wait.
Execute line 2 ... wait for r value.
Execute line 3 ... wait.
Execute line 4 ... wait for x and y value.
Line 3 returned ... y value set, message line 4.
Line 1 returned ... r value set, message line 2.
Line 2 returned ... x value set, message line 4.
Line 4 returned ... done.

จะเด็ดขนาดไหน ดำเนินการต่อด้วยรหัสและรอเฉพาะในกรณีที่จำเป็นเราได้ลดเวลารอลงเหลือสองวินาทีโดยอัตโนมัติ! : D ใช่แล้วแม้ว่าโค้ดจะซิงโครนัส แต่ก็มีแนวโน้มที่จะมีความหมายที่แตกต่างจากภาษาขั้นตอน

แก้ไข:

เมื่อคุณเข้าใจแนวคิดนี้ร่วมกับโพสต์ของ Godeke แล้วคุณจะจินตนาการได้ง่ายๆว่าการใช้ประโยชน์จากโปรเซสเซอร์หลายตัวเซิร์ฟเวอร์ฟาร์มที่เก็บข้อมูลซ้ำซ้อนและใครจะรู้อะไรอีกบ้าง


เย็น! ฉันเข้าใจผิดโดยสิ้นเชิงว่ามีการจัดการข้อความอย่างไร ขอบคุณโพสต์ของคุณช่วยได้
Jim Anderson

"ภาษาที่ใช้งานได้จะลองทำสิ่งต่อไปนี้" - ฉันไม่แน่ใจเกี่ยวกับภาษาที่ใช้งานได้อื่น ๆ แต่ใน Erlang ตัวอย่างจะทำงานเหมือนกับภาษาขั้นตอนทุกประการ คุณสามารถทำงานสองอย่างนี้ควบคู่กันไปได้โดยกระบวนการวางไข่ปล่อยให้มันดำเนินการสองงานแบบอะซิงโครนัสและรับผลลัพธ์ในตอนท้าย แต่มันไม่เหมือน "ในขณะที่โค้ดซิงโครนัสมีแนวโน้มที่จะมีความหมายที่แตกต่างจากภาษาขั้นตอน " ดูคำตอบของคริสด้วย
hcs42

16

เป็นไปได้ว่าคุณกำลังผสมซิงโครนัสกับซีเควนเชีย

เนื้อหาของฟังก์ชันใน erlang กำลังถูกประมวลผลตามลำดับ ดังนั้นสิ่งที่ Spencer พูดเกี่ยวกับ "เอฟเฟกต์อัตโนมัติ" นี้ไม่ได้ถือเป็นความจริงสำหรับ Erlang คุณสามารถจำลองพฤติกรรมนี้ด้วย erlang ได้

ตัวอย่างเช่นคุณสามารถสร้างกระบวนการที่คำนวณจำนวนคำในบรรทัด เนื่องจากเรามีหลายบรรทัดเราจึงสร้างกระบวนการดังกล่าวขึ้นมาสำหรับแต่ละบรรทัดและรับคำตอบเพื่อคำนวณผลรวมจากมัน

ด้วยวิธีนี้เราจะวางไข่กระบวนการที่ทำการคำนวณ "หนัก" (โดยใช้คอร์เพิ่มเติมหากมี) และหลังจากนั้นเราจะรวบรวมผลลัพธ์

-module(countwords).
-export([count_words_in_lines/1]).

count_words_in_lines(Lines) ->
    % For each line in lines run spawn_summarizer with the process id (pid)
    % and a line to work on as arguments.
    % This is a list comprehension and spawn_summarizer will return the pid
    % of the process that was created. So the variable Pids will hold a list
    % of process ids.
    Pids = [spawn_summarizer(self(), Line) || Line <- Lines], 
    % For each pid receive the answer. This will happen in the same order in
    % which the processes were created, because we saved [pid1, pid2, ...] in
    % the variable Pids and now we consume this list.
    Results = [receive_result(Pid) || Pid <- Pids],
    % Sum up the results.
    WordCount = lists:sum(Results),
    io:format("We've got ~p words, Sir!~n", [WordCount]).

spawn_summarizer(S, Line) ->
    % Create a anonymous function and save it in the variable F.
    F = fun() ->
        % Split line into words.
        ListOfWords = string:tokens(Line, " "),
        Length = length(ListOfWords),
        io:format("process ~p calculated ~p words~n", [self(), Length]),
        % Send a tuple containing our pid and Length to S.
        S ! {self(), Length}
    end,
    % There is no return in erlang, instead the last value in a function is
    % returned implicitly.
    % Spawn the anonymous function and return the pid of the new process.
    spawn(F).

% The Variable Pid gets bound in the function head.
% In erlang, you can only assign to a variable once.
receive_result(Pid) ->
    receive
        % Pattern-matching: the block behind "->" will execute only if we receive
        % a tuple that matches the one below. The variable Pid is already bound,
        % so we are waiting here for the answer of a specific process.
        % N is unbound so we accept any value.
        {Pid, N} ->
            io:format("Received \"~p\" from process ~p~n", [N, Pid]),
            N
    end.

และนี่คือสิ่งที่ดูเหมือนเมื่อเราเรียกใช้สิ่งนี้ในเชลล์:

Eshell V5.6.5  (abort with ^G)
1> Lines = ["This is a string of text", "and this is another", "and yet another", "it's getting boring now"].
["This is a string of text","and this is another",
 "and yet another","it's getting boring now"]
2> c(countwords).
{ok,countwords}
3> countwords:count_words_in_lines(Lines).
process <0.39.0> calculated 6 words
process <0.40.0> calculated 4 words
process <0.41.0> calculated 3 words
process <0.42.0> calculated 4 words
Received "6" from process <0.39.0>
Received "4" from process <0.40.0>
Received "3" from process <0.41.0>
Received "4" from process <0.42.0>
We've got 17 words, Sir!
ok
4> 

13

สิ่งสำคัญที่ทำให้ Erlang สามารถปรับขนาดได้นั้นเกี่ยวข้องกับการทำงานพร้อมกัน

ระบบปฏิบัติการจัดเตรียมการทำงานพร้อมกันโดยสองกลไก:

  • กระบวนการของระบบปฏิบัติการ
  • เธรดระบบปฏิบัติการ

กระบวนการไม่แชร์สถานะ - กระบวนการหนึ่งไม่สามารถขัดข้องได้ด้วยการออกแบบ

สถานะการแชร์เธรด - เธรดหนึ่งสามารถขัดข้องได้จากการออกแบบนั่นคือปัญหาของคุณ

ด้วย Erlang - เครื่องเสมือนใช้กระบวนการหนึ่งของระบบปฏิบัติการและ VM จัดเตรียมการทำงานพร้อมกันให้กับโปรแกรม Erlang โดยไม่ใช้เธรดของระบบปฏิบัติการ แต่จัดเตรียมกระบวนการ Erlang นั่นคือ Erlang ใช้ตัวกำหนดเวลาของตัวเอง

กระบวนการ Erlang เหล่านี้พูดคุยกันโดยการส่งข้อความ (จัดการโดย Erlang VM ไม่ใช่ระบบปฏิบัติการ) กระบวนการ Erlang ระบุถึงกันและกันโดยใช้ ID กระบวนการ (PID) ซึ่งมีแอดเดรสสามส่วน<<N3.N2.N1>>:

  • ประมวลผลไม่มี N1 บน
  • VM N2 เปิดอยู่
  • เครื่องทางกายภาพ N3

สองกระบวนการบน VM เดียวกันบน VM ต่างกันบนเครื่องเดียวกันหรือสองเครื่องสื่อสารในลักษณะเดียวกันการปรับขนาดของคุณจึงไม่ขึ้นอยู่กับจำนวนเครื่องจริงที่คุณปรับใช้แอปพลิเคชันของคุณ (ในการประมาณครั้งแรก)

Erlang เป็นเพียงเธรดที่ปลอดภัยในแง่ที่ไม่สำคัญ - ไม่มีเธรด (ภาษาที่เป็น SMP / multi-core VM ใช้เธรดระบบปฏิบัติการหนึ่งเธรดต่อหนึ่งคอร์)


7

คุณอาจมีความเข้าใจผิดว่า Erlang ทำงานอย่างไร รันไทม์ Erlang ลดการสลับบริบทบน CPU ให้น้อยที่สุด แต่ถ้ามีหลาย CPU พร้อมใช้งานทั้งหมดจะถูกใช้เพื่อประมวลผลข้อความ คุณไม่มี "เธรด" เหมือนกับที่คุณใช้ในภาษาอื่น ๆ แต่คุณสามารถมีการประมวลผลข้อความจำนวนมากพร้อมกันได้


4

ข้อความ Erlang เป็นแบบอะซิงโครนัสอย่างแท้จริงหากคุณต้องการตอบกลับข้อความของคุณแบบซิงโครนัสคุณต้องระบุรหัสอย่างชัดเจน สิ่งที่อาจกล่าวได้คือข้อความในกล่องข้อความกระบวนการจะถูกประมวลผลตามลำดับ ข้อความใด ๆ ที่ส่งไปยังกระบวนการจะอยู่ในกล่องข้อความของกระบวนการนั้นและกระบวนการจะเลือกข้อความหนึ่งข้อความจากกล่องนั้นประมวลผลจากนั้นไปยังข้อความถัดไปตามลำดับที่เห็นสมควร นี่เป็นการกระทำตามลำดับอย่างมากและบล็อกการรับก็ทำเช่นนั้น

ดูเหมือนว่าคุณได้ผสมซิงโครนัสและลำดับตามที่คริสกล่าวถึง



-2

ในภาษาที่ใช้งานได้อย่างแท้จริงลำดับการประเมินผลไม่สำคัญ - ในแอปพลิเคชันฟังก์ชัน fn (arg1, .. argn) สามารถประเมิน n อาร์กิวเมนต์ควบคู่ ซึ่งรับประกันความเท่าเทียมกันในระดับสูง (อัตโนมัติ)

Erlang ใช้โมเดลกระบวนการที่กระบวนการสามารถทำงานในเครื่องเสมือนเดียวกันหรือบนโปรเซสเซอร์อื่น - ไม่มีวิธีใดที่จะบอกได้ นั่นเป็นไปได้เพราะข้อความถูกคัดลอกระหว่างกระบวนการไม่มีสถานะที่ใช้ร่วมกัน (เปลี่ยนแปลงได้) พาราเลลลิซึมแบบหลายโปรเซสเซอร์ไปได้ไกลกว่ามัลติเธรดมากเนื่องจากเธรดขึ้นอยู่กับหน่วยความจำที่ใช้ร่วมกันจึงมีเธรดได้เพียง 8 เธรดที่ทำงานแบบขนานบน CPU แบบ 8 คอร์ในขณะที่การประมวลผลแบบมัลติสามารถปรับขนาดให้เท่ากับหลายพันของกระบวนการแบบขนาน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.