Archive

Archive for November, 2012

เขียนโปรแกรมอย่างไรให้เป็น Thread-safe [ Writing Thread safe code in C# ]

 การเขียน multithreading  เพื่อเพิ่มประสิทธิภาพการทำงานให้กับ windows app ต้องระวังเรื่องการเรียกหรือทำงานกับ controls   ซึ่งจะต้องตระหนักเรื่อง thread-safe

การเข้าถึง windows forms controls จะไม่เป็น thread-safe ถ้าหาก มี thread  2 threads หรือ มากกว่า เข้าไปเรียกใช้ controls นั้น เพราะมันอาจทำให้ controls นั้น ทำงานหรือแสดงผล ไม่เป็นไปตามที่ คาดหมายไว้ หรือ ที่เรียกว่า inconsistent state  ผลก็คือ ทำให้ thread ที่เข้าใช้งาน ได้ผลลัพธ์ที่ไม่ถูกต้อง  ทำให้เกิด race conditions และ deadlocks  ดังนั้น เราจะต้องมั่นใจว่า การเข้าถึง control นั้นทำในวิธีที่ ปลอดภัย หรือ thread-safe

ใน .NET Framework จะช่วยให้แจ้งเราทราบ หากมีการเข้าถึง control ในลักษณะนี้  โดยแจ้งให้ทราบขณะที่เรา  debug โปรแกรม  หรือ เรา run โปรแกรมใน debug mode  เป็น message ว่า InvalidOperationException แล้วแจ้งชื่อ control name ที่ มีการเข้าถึงจาก thread อื่น ที่ไม่ใช่ thread ที่สร้าง control ที่เกิดปัญหานั้น

ตัวอย่างต่อไป จะเป็นการแสดงให้เห็นถึงการเรียก windows forms controls  ในลักษณะที่ เป็น thread-safe และไม่เป็น thread-safe   โดยยกตัวอย่าง การ กำหนดค่า Text property ให้กับ TextBox  เพื่อแสดงให้เห็นใน สองวิธีการ ตาม code ด้านล่าง

**code ทั้งหมดอ้างอิงจาก msdn.microsoft.com

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace CrossThreadDemo
{
	public class Form1 : Form
	{
		// This delegate enables asynchronous calls for setting
		// the text property on a TextBox control.
		delegate void SetTextCallback(string text);

		// This thread is used to demonstrate both thread-safe and
		// unsafe ways to call a Windows Forms control.
		private Thread demoThread = null;

		// This BackgroundWorker is used to demonstrate the
		// preferred way of performing asynchronous operations.
		private BackgroundWorker backgroundWorker1;

		private TextBox textBox1;
		private Button setTextUnsafeBtn;
		private Button setTextSafeBtn;
		private Button setTextBackgroundWorkerBtn;

		private System.ComponentModel.IContainer components = null;

		public Form1()
		{
			InitializeComponent();
		}

		protected override void Dispose(bool disposing)
		{
			if (disposing && (components != null))
			{
				components.Dispose();
			}
			base.Dispose(disposing);
		}

		// This event handler creates a thread that calls a
		// Windows Forms control in an unsafe way.
		private void setTextUnsafeBtn_Click(
			object sender,
			EventArgs e)
		{
			this.demoThread =
				new Thread(new ThreadStart(this.ThreadProcUnsafe));

			this.demoThread.Start();
		}

		// This method is executed on the worker thread and makes
		// an unsafe call on the TextBox control.
		private void ThreadProcUnsafe()
		{
			this.textBox1.Text = "This text was set unsafely.";
		}

		// This event handler creates a thread that calls a
		// Windows Forms control in a thread-safe way.
		private void setTextSafeBtn_Click(
			object sender,
			EventArgs e)
		{
			this.demoThread =
				new Thread(new ThreadStart(this.ThreadProcSafe));

			this.demoThread.Start();
		}

		// This method is executed on the worker thread and makes
		// a thread-safe call on the TextBox control.
		private void ThreadProcSafe()
		{
			this.SetText("This text was set safely.");
		}

		// This method demonstrates a pattern for making thread-safe
		// calls on a Windows Forms control.
		//
		// If the calling thread is different from the thread that
		// created the TextBox control, this method creates a
		// SetTextCallback and calls itself asynchronously using the
		// Invoke method.
		//
		// If the calling thread is the same as the thread that created
		// the TextBox control, the Text property is set directly.

		private void SetText(string text)
		{
			// InvokeRequired required compares the thread ID of the
			// calling thread to the thread ID of the creating thread.
			// If these threads are different, it returns true.
			if (this.textBox1.InvokeRequired)
			{
				SetTextCallback d = new SetTextCallback(SetText);
				this.Invoke(d, new object[] { text });
			}
			else
			{
				this.textBox1.Text = text;
			}
		}

		// This event handler starts the form's
		// BackgroundWorker by calling RunWorkerAsync.
		//
		// The Text property of the TextBox control is set
		// when the BackgroundWorker raises the RunWorkerCompleted
		// event.
		private void setTextBackgroundWorkerBtn_Click(
			object sender,
			EventArgs e)
		{
			this.backgroundWorker1.RunWorkerAsync();
		}

		// This event handler sets the Text property of the TextBox
		// control. It is called on the thread that created the
		// TextBox control, so the call is thread-safe.
		//
		// BackgroundWorker is the preferred way to perform asynchronous
		// operations.

		private void backgroundWorker1_RunWorkerCompleted(
			object sender,
			RunWorkerCompletedEventArgs e)
		{
			this.textBox1.Text =
				"This text was set safely by BackgroundWorker.";
		}

		#region Windows Form Designer generated code

		private void InitializeComponent()
		{
			this.textBox1 = new System.Windows.Forms.TextBox();
			this.setTextUnsafeBtn = new System.Windows.Forms.Button();
			this.setTextSafeBtn = new System.Windows.Forms.Button();
			this.setTextBackgroundWorkerBtn = new System.Windows.Forms.Button();
			this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
			this.SuspendLayout();
			//
			// textBox1
			//
			this.textBox1.Location = new System.Drawing.Point(12, 12);
			this.textBox1.Name = "textBox1";
			this.textBox1.Size = new System.Drawing.Size(240, 20);
			this.textBox1.TabIndex = 0;
			//
			// setTextUnsafeBtn
			//
			this.setTextUnsafeBtn.Location = new System.Drawing.Point(15, 55);
			this.setTextUnsafeBtn.Name = "setTextUnsafeBtn";
			this.setTextUnsafeBtn.TabIndex = 1;
			this.setTextUnsafeBtn.Text = "Unsafe Call";
			this.setTextUnsafeBtn.Click += new System.EventHandler(this.setTextUnsafeBtn_Click);
			//
			// setTextSafeBtn
			//
			this.setTextSafeBtn.Location = new System.Drawing.Point(96, 55);
			this.setTextSafeBtn.Name = "setTextSafeBtn";
			this.setTextSafeBtn.TabIndex = 2;
			this.setTextSafeBtn.Text = "Safe Call";
			this.setTextSafeBtn.Click += new System.EventHandler(this.setTextSafeBtn_Click);
			//
			// setTextBackgroundWorkerBtn
			//
			this.setTextBackgroundWorkerBtn.Location = new System.Drawing.Point(177, 55);
			this.setTextBackgroundWorkerBtn.Name = "setTextBackgroundWorkerBtn";
			this.setTextBackgroundWorkerBtn.TabIndex = 3;
			this.setTextBackgroundWorkerBtn.Text = "Safe BW Call";
			this.setTextBackgroundWorkerBtn.Click += new System.EventHandler(this.setTextBackgroundWorkerBtn_Click);
			//
			// backgroundWorker1
			//
			this.backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);
			//
			// Form1
			//
			this.ClientSize = new System.Drawing.Size(268, 96);
			this.Controls.Add(this.setTextBackgroundWorkerBtn);
			this.Controls.Add(this.setTextSafeBtn);
			this.Controls.Add(this.setTextUnsafeBtn);
			this.Controls.Add(this.textBox1);
			this.Name = "Form1";
			this.Text = "Form1";
			this.ResumeLayout(false);
			this.PerformLayout();

		}

		#endregion

		[STAThread]
		static void Main()
		{
			Application.EnableVisualStyles();
			Application.Run(new Form1());
		}

	}
}

จาก code จะเห็นว่ามีการแสดงตัวอย่างการใช้งาน Thread 2 วิธีคือ
1. private Thread demoThread = null; // ใช้ class thread เป็น worker thread
2. private BackgroundWorker backgroundWorker1; // เป็น component ที่ทำงานบน windows form

ซึ่งการ ใช้ class thread นั้นเป็นการสร้าง thread ลูกของ windows form ขึ้นมาอีก 1 thread ซึ่ง worker thread ของ class นี้ จะเกิดสร้างปัญหา
เมื่อต้องเขเาไปเรียกใช้ component ที่วางอยู่บน form ในกรณีนี้คือ TextBox1 เหตุการแบบนี้เรียกว่า ไม่เป็น thread-safe ดังนั้นการเข้าถึงต้องมีวิธีการ เข้าถึงโดยการตรวจสอบ
InvokeRequired ของ textBox1 และ ดำเนินการเข้าถึง แตกแต่างกัน
– ถ้า InvokeRequired = true ให้เข้าถึงโดยใช้ delegate object ผ่าน thread หลักก็คือ Windows form (InVoke)
– ถ้า InvokeRequired = false ให้เข้าถึงได้โดยตรง
ตาม code ที่ตัมานะครับ

private void SetText(string text)
 {
     // InvokeRequired required compares the thread ID of the
     // calling thread to the thread ID of the creating thread.
     // If these threads are different, it returns true.
    if (this.textBox1.InvokeRequired)
    {
        SetTextCallback d = new SetTextCallback(SetText);
        this.Invoke(d, new object[] { text });
    }
     else
    {
       this.textBox1.Text = text;
    }
 }

ส่วนถ้าเราใช้ background worker ในการทำ multithreading เราก็สามารถเขาถึงได้โดยตรง ผ่าน event run ProgressChanged และ RunWorker conmpleted

ครับ เป็นอันว่าพอสมควรแก่ ประเด็นแล้วนะครับ เพื่อน ๆ ที่เคยเจอปัญหานี้คงพอเข้าใจแล้วนะครับ ขอให้สนุกสนานกับการเขียนโปรแกรม นะครับ

Categories: C# .NET