กรกฎาคม 19, 2019, 03:20:40 am *
ยินดีต้อนรับคุณ, บุคคลทั่วไป กรุณา เข้าสู่ระบบ หรือ ลงทะเบียน
ส่งอีเมล์ยืนยันการใช้งาน?

เข้าสู่ระบบด้วยชื่อผู้ใช้ รหัสผ่าน และระยะเวลาในเซสชั่น
   หน้าแรก   ช่วยเหลือ เข้าสู่ระบบ สมัครสมาชิก  
หน้า: [1]   ลงล่าง
  พิมพ์  
ผู้เขียน หัวข้อ: C# Tips & Tricks -- 12 -- Worker Task Design and Implementation  (อ่าน 1855 ครั้ง)
0 สมาชิก และ 1 บุคคลทั่วไป กำลังดูหัวข้อนี้
ShadowMan
Administrator
Hero Member
*****
ออฟไลน์ ออฟไลน์

เพศ: ชาย
กระทู้: 8272


ShadowWares


| |
« เมื่อ: พฤษภาคม 29, 2016, 02:42:14 pm »

C# Tips & Tricks -- 12 -- Worker Task Design and Implementation

ตอนก่อนๆ ได้พูดถึงเรื่อง delegate, Event และ Lambda expression ไปมากพอสมควร ทั้งทางตรงและทางอ้อม
ตอนนี้จะย้อนกลับมาพูดถึงเรื่อง Event ในรูปแบบ Publisher/Subscriber อีกครั้ง แต่จะไม่อธิบายในรายละเอียด (สนใจ อ่านรายละเอียดในตอน Event and Delegate)


ตอนนี้จะเป็นเทคนิคการออกแบบ Worker เน้นไปที่ Worker ที่ต้องการเวลาในการทำงานนาน นานที่ว่าคือนนมากพอที่จะทำให้ GUI โดยแช่เข็ง หรือไม่ตอบสนองต่อ User จนทำให้โปรแกรมเราเป็นที่น่ารำคาญใจ (หาความเป็น Responsive ไม่ได้)

Worker ในกลุ่มนี้ มีมากมายเช่น Query ช้อมูลจาก database การสื่อสารข้อมูลกับเครื่องมือเครื่องจักร รวมถึงการประมวลผลทุกชนิดที่ ใช้เวลานาน (นานที่ว่าในโลกของ GUI Thread 1/20 วินาที หรือต่ำกว่านั้น) นานที่ว่านิยามยาก แต่มือไรก็ตามที่รันโปรแกรมแล้วทำให้รู้สึกได้ถึงการกระตุกของ GUI นั่นถือว่านานมากแล้ว

ลองสั่งให้โปรแกรมให้ทำอะไรบางอย่าง แล้วลากหน้าต่างของโปรแกรมไปมาดู ถ้าเห็นว่าการเคลื่นที่ไม่ลื่นไหล นั่นถือเป็นความล้มเหลวอย่างสิ้นเชิงของโปรแกรมเมอร์ (ในมุมมองของ Multi-thread Programming)

เทคนิคการออกแบบที่พูดถึงในตอนนี้ ใช้ได้กับทุกภาษา ไม่จำเป็นต้องเป็น C# แต่รูปแบบของโปรแกรมจะแตกต่างไปตามแต่ละภาษา มาเริ่มกันที่ดูรายละเอียกของคลาส Worker แบบเต็มๆ

Code: (c-sharp)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace CustomEvents
{
    public class Worker
    {
        private volatile bool Run;

        public class WorkerEventArgs : EventArgs
        {
            public enum EVENT_CODE
            {
                STARTED,
                PROGRESS,
                TERMINATED,
                COMPLETED
            }
            public EVENT_CODE EventCode { get; set; }
            public int Percent { get; set; }
            public double EventData { get; set; }
        }

        public delegate void WorkerEventHandler(object sender, WorkerEventArgs args);
        public event WorkerEventHandler WorkAllEvents;


        private void PublishEvent(WorkerEventArgs args)
        {
            if(WorkAllEvents != null)
            {
                WorkAllEvents(this, args);
            }
        }

        public void Cancel()
        {
            Run = false;
        }

        public double DoCompute(double data)
        {
            double result = 0; int i = 0;

            PublishEvent(new WorkerEventArgs()
            {
                EventCode = WorkerEventArgs.EVENT_CODE.STARTED,
                EventData = result,
                Percent = i
            });

            Run = true;
            while (i++ < 99 & Run == true)
            {
                result += data;     /* do a simple computation      */
                Thread.Sleep(50);   /* simulat a computational time  */
                PublishEvent(new WorkerEventArgs()
                {
                    EventCode = WorkerEventArgs.EVENT_CODE.PROGRESS,
                    EventData = result,
                    Percent = i
                });
            }

            if (Run == true)
            {
                PublishEvent(new WorkerEventArgs()
                {
                    EventCode = WorkerEventArgs.EVENT_CODE.COMPLETED,
                    EventData = result,
                    Percent = i
                });
            }
            else
            {
                PublishEvent(new WorkerEventArgs()
                {
                    EventCode = WorkerEventArgs.EVENT_CODE.TERMINATED,
                    EventData = i,
                    Percent = i
                });
                return 0;    /* If the thread is terminated/canceled, returns 0 */
            }

            return result;
        }
    }
}


จาก code จะเห็นได้ว่า Work สามารถ Publish event ได้ 4 รูปแบบ:
  • STARTED: เริ่มต้นแล้ว
  • PROGRESS: กำลังทำงานอยู่
  • TERMINATED: ถูกสั่งให้หยุดก่อนงานเสร็จ
  • COMPLETED: ทำงานเสร็จแล้ว


สำหรับ WorkerEventArgs (ข้อมูลที่จะ Publish ไปให้ Subscriber) ทีอยู่ 3 ตัว:
  • EventCode: เป็นตัวบอกชนิดของ event
  • Percent: เป็นตัวบอกว่าทำงานไปได้กี่ % แล้ว
  • EventData: เป็นตัวบอกว่าขณะนี้ผลที่ได้จากการทำงานมีค่าเท่าไร่

พิจารณาที่ DoCompute() จะเห็นได้ว่า Event แต่ละแบบจะถูก Publish ไปให้ Subscriber ในจyงหวะและเวลาที่สอดคล้องกับการทำงาน

นั่นคือทั้งหมดของ Worker class ต่อไปไปดูที่ฝั่ง GUI หรือ Subscriber บ้าง



โปรแกรมในส่วนของ GUI:
Code: (c-sharp)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace CustomEvents
{
    public partial class Form1 : Form
    {

      /* Worker oject */
        Worker worker;

        public Form1()
        {
            InitializeComponent();
        }

      /* Called by helper task, activated by the GUI */
        private void DoInBackground()
        {
            Console.WriteLine("DoInBackground Thread Id - " +
                          Thread.CurrentThread.ManagedThreadId + " started");
            worker = new Worker();
            worker.WorkerEvents += Worker_WorkAllEvents;
            Task<double> t = Task.Factory.StartNew(() => worker.DoCompute(0.123));
            Console.WriteLine("Returned result: " + t.Result);
            Console.WriteLine("DoInBackground Thread Id " +
                         Thread.CurrentThread.ManagedThreadId + " completed");
        }

      /* Called by the worker object */
        private void Worker_WorkAllEvents(object sender, Worker.WorkerEventArgs args)
        {
         /* Event handling */
            switch(args.EventCode)
            {
                case Worker.WorkerEventArgs.EVENT_CODE.STARTED:
                    Console.WriteLine("Worker Thread Id " +
                              Thread.CurrentThread.ManagedThreadId + " started");
                    Invoke((MethodInvoker)(() => {
                        labelReport.Text = "Worker Started";
                        buttonStart.Enabled = false;
                        buttonCancel.Enabled = true;
                    }));
                    break;

                case Worker.WorkerEventArgs.EVENT_CODE.PROGRESS:
                    Invoke((MethodInvoker)(() => {
                        progressBar1.Value = args.Percent;
                        labelPercent.Text = args.Percent + "%";
                        labelReport.Text = "Current result: " + args.EventData.ToString("0.000");
                    }));
                    break;

                case Worker.WorkerEventArgs.EVENT_CODE.TERMINATED:
                    Console.WriteLine("Worker Thread Id " +
                                    Thread.CurrentThread.ManagedThreadId + " terminated");
                    Invoke((MethodInvoker)(() => {
                        progressBar1.Value = 0;
                        labelPercent.Text = "";
                        labelReport.Text = "Worker terminated";
                        buttonStart.Enabled = true;
                        buttonCancel.Enabled = false;

                    }));
                    break;

                case Worker.WorkerEventArgs.EVENT_CODE.COMPLETED:
                    Console.WriteLine("Worker Thread Id " +
                                  Thread.CurrentThread.ManagedThreadId + " completed");
                    Invoke((MethodInvoker)(() => {
                        progressBar1.Value = 0;
                        labelPercent.Text = "";
                        buttonStart.Enabled = true;
                        buttonCancel.Enabled = false;
                    }));
                    break;
            }
        }

        private void buttonStart_Click(object sender, EventArgs e)
        {
            Console.WriteLine("Main Thread Id " +
                         Thread.CurrentThread.ManagedThreadId + " started");
            Task.Factory.StartNew( () => DoInBackground() );
        }

        private void buttonCancel_Click(object sender, EventArgs e)
        {
            worker.Cancel();
        }
    }
}


ในที่นี้ Worker จะไม่ถูกเรียกให้ทำงานจาก GUI โดย ไม่เช่นนั้น Worker ก็จะถูกรันอยู่ภายใต้ Thread เดียวกับ GUI และเมื่อสั่งให้ GUI รอผล GUI ก็จะโดยแช่เข็งจนกว่า Worker จะทำงานเสร็จ
ปัญหานี้แก้ได้ง่ายๆ โดยการหาผู้ช่วย (ไม่อยากรอเอง ก็หาคนมา แล้วสั่งให้ไปรอแทน) ในที่นี้คือ Task หนึ่ง ที่รัน Method ชื่อ DoInBackground() (ดูรายละเอียดที่ buttonStart_Click()) ในที่นี้คือ

Code: (c-sharp)
 Task.Factory.StartNew( () => DoInBackground() );

*** anonymous function บวก Lambda expression ธรรมดาๆ ***
ถ้ายังเห็นว่าสิ่งนี้ยังไม่ธรรมดา อาจจะต้องกลับไปอ่านตอนก่อนหน้า แต่นั่นยังพอแน่ๆ ต้องหา C# ระดับกลางถึงสูงอ่านเพิ่มอีกสักหน่อย จะช่วยให้เข้าใจ code ในตอนนี้ได้มากยิ่งขึ้น
ส่วนนี้ตีความเป็นคำพูดได้ว่า "สร้าง Task มาตัวหนึ่ง (ไม่มีชื่อ) สั่งให้รันฟังก์ชั่นตัวหนึ่ง (ไม่มีชื่อเช่นกัน) ฟังก์ชั่นตัวนี้ไม่ต้องการ parameter และไม่มีการ return ใดๆ และในฟังก์ชั่่นดังกล่าวมีการเรียกใช้ฟังก์ชั่นอื่น ชื่อ DoInBackGround ซึ่งไม่ต้องการ parameter และไม่มีการ return ใดๆ เช่นกัน"

ใน DoInBackground() (ลูกจ้าง ของ GUI) ซึ่งตอนนี้ทำตัวเป็นนายหน้า แทนที่จะทำเอง ไม่ทำหรอก ไปจ้างชาวบ้านต่อ แต่การจ้างนี้ ก็ต้องนั่งรอผลงานของลูกจ้าง ทำอะไรต่อไม่ได้ ไม่เหมือน GUI ที่ลอยตัวไปแล้ว

ลูกจ้างระดับทำงานไม่ใช้ใครอื่น Worker ของเรานั่นเอง มาดูรายละเอียดการสร้างและรอคำตอบจาก Worker กันก่อน
Code: (c-sharp)
worker = new Worker();
worker.WorkerEvents += Worker_WorkAllEvents;
Task<double> t = Task.Factory.StartNew(() => worker.DoCompute(0.123));

ทำการสร้าง worker หรือคนงานมาหนึ่งคน แล้วบอกว่า ถ้ามีเหตุการใดๆ ในการทำงานเกิดขึ้นกระซิบบอกนานใหญ่ (GUI) ด้วยนะ
แล้วบอกต่อว่า ผมรอรับคำตอบแบบ double อยู่นะ รอตรงนี้แหละไม่ไปไหน (ไปไหนไม่ได้ โดน Suspended โดย OS แล้วทำมาเป็นพูดว่าจะรอ) พูดต่อว่า นี่ 0.123 นี่เป็นวัตถุดิบเริ่มต้นสำหรับงานนี้นะ ถ้าพร้อมแล้ว เริ่มทำงานได้เลย (ทั้งหมดนั่นคือ นายหน้า คุยกับ คนงาน)
ส่วนนี้มี  anonymous function บวก Lambda expression เช่นเดียวกัน ภาษาคนพูดได้คล้ายๆ กับก่อนหน้า:
"สร้าง Task มาตัวหนึ่ง (ไม่มีชื่อ) สั่งให้รันฟังก์ชั่นตัวหนึ่ง (ไม่มีชื่อเช่นกัน) ฟังก์ชั่นตัวนี้ไม่ต้องการ parameter และไม่มีการ return ใดๆ และในฟังก์ชั่่นดังกล่าวมีการเรียกใช้ฟังก์ชั่นของ worker ชื่อ DoCompute ซึ่งต้องการ parameter 1 ตัวแบบ double และมีการ return ค่ากลับแบบ double เช่นกัน"


ในระหว่างที่ Worker ทำงานไป ก็กระซิบบอกนายใหญ่ ว่าเกิดอะไรขึ้นบ้าง งานไปทำงานกี่เปอร์เซ็นแล้ว ฯลฯ เนื่องจากนายใหญ่ว่างมาก ได้ฟังอะไรมาก็เอาไปวาดจอเล่นไปเรื่อย (ขนาดนั้นเลย) ที่วาดนี่คือวาดให้นายใหญ่สูงสุด (User) ดูนะ
worker บอกอะไร และ GUI ทำอะไร ดูรายละเอียดที่ Worker_WorkAllEvents() จะเห็นว่าทั้ง 4 กรณีรับทราบโดย GUI โดยอศัย switch/case
การ Update GUI element ทั้งหลาย จะต้องทำผ่าน delegate ในที่นี้ใช้ Invoke() + anonymous function + Lambda expression และเนื่องจากใน function body ไม่มีการรับค่าและคืนค่ากลับ จึงทำการ cast เป็น MethodInvoker นั่นเอง  ผลการทำงานตามภาพ




ที่เห็นนิ่งๆ นั่นไม่ใช่เพราะโปรแกรมค้างนะครับ คือมันเป็น "ภาพนิ่ง"
อยากเห็นภาพเคลื่นไหว นำ code ไป compile แล้วรันดูครับ


บางคนอาจจะมีคำถามว่าทำไม GUI ต้องจ้างนายหน้าด้วย ในเมื่อสุดท้ายแล้วก็ต้องรอรับ Event จาก Worker และไปวาดจอ ทำไมไม่จ้าง/สร้าง worker เองโดยตรงล่ะ?

คำตอบคือไม่จะเป็นครับ เพราะในกรณีนี้ Worker รายงานผลมาให้ต่อเนื่อง และส่งคำตอบสุดท้ายมาให้อยู่แล้วผ่านทาง Event แต่อย่าลืมว่าโดยทั่วไป worker ของเราไม่มีความสามารถในการส่ง Event มาให้ เพราะไม่ว่าจะทำอะไร เราเขียนปรแกรมกันสุดๆ ใน GUI เลย จากนั้นก็รอผลสุดท้าย ในกรณีที่ไม่ต้องการ สร้าง worker ที่มีความสามารถ publish event จำเป็นต้องให้นายหน้ารอ และส่งคำตอบมาให้ GUI (GUI รอไม่ได้ ไม่งั้นจะไม่มีเวลาวาดรูปให้นยใหญ่ดู)
แล้วหนายหน้ารอที่ไหน รอที่นี่ครับ:

Code: (c-sharp)
Console.WriteLine("Returned result: " + t.Result);

t.Result บอกให้ Task t หยุกรอจนกว่าจะได้คำตอบกลับมา

และนั้นคือทั้งหมด การออกแบบ Program รูปแบบนี้ ไม่จำเป็นต้องเป็น C# นะครับ แต่ใช้ได้กับทุกภาภาษา โดยส่วนตัวผมจะออกแบบและใช้อยู่กับ C/C++ ในการสร้าง Library สำหรับงาน Multi-thread Event-Driven รูปแบบการเขียนโปรแกรมต่างกันเพียงเล็กน้อยเท่านั้น
สำหรับคนที่ใช้ภาษาอื่นก็นำวิธีการเหล่านี้ไปใช้ได้ครับ ปัจจุบัน Programming หรือ coding style เปลี่ยนไปพอสมควร จนเรื่อง Lambda expression, functional programming กลายเป็นสิ่งที่จำเป็นต้องรู้ ไม่เช่นนั้นจะอ่าน code สมัยใหม่ไม่ออก!!

Happy Coding!!
บันทึกการเข้า

By SDW: Do No Wrong Is Do Nothing
          If you want to increase your success rate, double your failure rate
Dorrisliza
Newbie
*
ออฟไลน์ ออฟไลน์

กระทู้: 2


| |
« ตอบ #1 เมื่อ: กุมภาพันธ์ 21, 2019, 02:55:00 pm »

หากต้องการถามอย่างอื่นได้หรือเปล่าครับ
บันทึกการเข้า

หน้า: [1]   ขึ้นบน
  พิมพ์  
 
กระโดดไป: