Thread 相關類別
有三個相關的類別:Thread, ThreadStart, ParameterizedThreadStart
ThreadStart
宣告的 function會被 thread 執行,ThreadStart 負責建立無參數的委派函式
ParameterizedThreadStart
宣告的 function會被 thread 執行,ThreadStart 負責建立有參數的委派函式
Thread
建立、控制、管理 Thread,常用的屬性:
屬性 說明 回傳資料型別 CurrentThread 目前正在執行的 thread Thread IsAlive thread 的執行狀態 Boolean IsBackground thread 是否為背景執行緒 Boolean ManagedThreadId thread 的識別號碼 Integer Name thread 的名稱 String 或 null ThreadState thread 的狀態 ThreadState (enum) 常用 method
method 說明 Abort() 停止 thread,停止後,無法重新啟動 BeginCriticalRegion() 設定 Critical region EndCriticalRegion() 結束 Critical region Interrupt() 中斷 WaitSleepJoin 狀態的 thread Join([a]) 封鎖執行緒直到停止執行為止,a 為 integer (ms) 或 TimeSpan ResetAbort() 取消正在要求的 Abort() Sleep(a) 暫停執行緒 a (ms) 或 TimeSpan Start([a]) 啟動 thread,a為委派函式的參數,object 型別 Note: 使用 Abort() 不保證一定能停止 thread,有可能會發生 exception。ex: SecurityException: 沒有權限停止 thread,ThreadStateException: 停止已暫停的 thread
ThreadState (enum)
位於 System.Threading namespace
列舉常數 value 說明 Aborted 256 執行緒目前無作用,但狀態尚未變更為 Stopped AbortRequested 128 已收到 Abort() Background 4 背景執行 Running 0 正在執行 Stopped 16 已停止 StopRequested 1 已被要求停止中 Suspended 64 已暫停 SuspendedRequested 2 已被要求暫停中 Unstarted 8 還沒開始執行,未被呼叫 Start() WaitSleepJoin 32 已被封鎖 列出呼叫哪個 method 會導致狀態改變
method state 建立 thread Unstarted Start() Running Sleep() WaitSleepJoin 對另一個物件呼叫 Monitor.Wait() WaitSleepJoin Join WaitSleepJoin Interrupt() Runing Suspend() SuspendedRequested 回應 Suspend() Suspended Resume() Running Abort() AbortRequested 回應 Abort() Stopped thread 已終止 Stopped
建立 thread
建立委派的 method
使用 ThreadStart 建立委派物件
使用委派物件建立 Thread 型別的 Thread 物件
Start()
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
bool fgDone;
Int32 sum;
int guess;
public Form1()
{
InitializeComponent();
}
void count()
{
while (sum < int.MaxValue)
sum++;
fgDone = true;
}
void count_param(object num)
{
Random rd = new Random();
while(guess!=(int)num)
{
guess = rd.Next(1, 101);
Thread.Sleep(100);
}
fgDone = true;
}
// 沒有用 thread,計算過程中,視窗會卡住,無法使用
private void Button1_Click(object sender, EventArgs e)
{
textBox1.AppendText("開始計算\r\n");
sum = 0;
while (sum < int.MaxValue)
sum++;
textBox1.AppendText("計算完畢,sum= " +
sum.ToString()+"\r\n");
}
// 用 ThreadStart 產生 Thread
private void button2_Click(object sender, EventArgs e)
{
ThreadStart thdStart = new ThreadStart(count);
Thread thd = new Thread(thdStart);
sum = 0;
fgDone = false;
textBox1.AppendText("執行緒開始執行\r\n");
thd.Start();
timer1.Enabled = true;
}
// 用 ParameterizedThreadStart 產生 Thread
private void button3_Click(object sender, EventArgs e)
{
int num = 78;
ParameterizedThreadStart paramStart =
new ParameterizedThreadStart(count_param);
Thread thd = new Thread(paramStart);
fgDone = false;
guess = -1;
thd.Start(num);
timer2.Enabled = true;
}
private void timer1_Tick(object sender, EventArgs e)
{
if (fgDone)
{
timer1.Enabled = false;
textBox1.AppendText("計算完畢,sum= " +
sum.ToString()+"\r\n");
}
}
private void timer2_Tick(object sender, EventArgs e)
{
textBox1.AppendText(guess.ToString() + "\r\n");
if (fgDone)
{
timer2.Enabled = false;
textBox1.AppendText("找到了\r\n");
}
}
}
}
取得 thread 執行結果
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
Int32 sum;
public Form1()
{
InitializeComponent();
}
void count()
{
while (sum < int.MaxValue)
sum++;
}
void count_param(object param)
{
Int32[] pp = (Int32[])param;
while (pp[0] < int.MaxValue)
pp[0]++;
}
// 透過 sum 全域變數,儲存 thread 的執行結果
private void button1_Click(object sender, EventArgs e)
{
Thread thd = new Thread(count);
sum = 0;
textBox1.AppendText("執行緒開始執行\r\n");
thd.Start();
textBox1.AppendText("使用Join()方法,"+"" +
"所以必須等待執行緒執行結束...\r\n");
thd.Join();
textBox1.AppendText("sum=" + sum.ToString() +
"\r\n");
}
// 透過傳入 thread 的參數,儲存 thread 的執行結果
// 因為該參數是 array,傳入是 call by value
// 會將 sum1[0] 的記憶體位址傳給該 method
private void button2_Click(object sender, EventArgs e)
{
Thread thd = new Thread(count_param);
Int32 []sum1 = { 0 };
textBox1.AppendText("執行緒開始執行\r\n");
thd.Start(sum1);
textBox1.AppendText("使用Join()方法," + "" +
"所以必須等待執行緒執行結束...\r\n");
thd.Join();
textBox1.AppendText("sum1=" + sum1[0].ToString() +
"\r\n");
}
}
}
thread 生命週期
Thread 提供 Abort(), Join(), Interrupt(), Sleep(), Suspend(), Resume(),其中 Suspend(), Resume() 已經建議不要使用。
Join() 會等待 thread 結束,呼叫 Join() 的 thread 會持續等待無法回應。
結束 Thread 的方法,是讓該 Thread 完工後自行結束,如果是持續工作的 Thread,就要用 Abort(),或是用全域變數判斷要不要繼續執行。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
bool fgDone_a, fgDone_b;
int guess_a, guess_b;
Thread th_a=null;
bool fgRun;
int num_a = 78, num_b=50;
public Form1()
{
InitializeComponent();
}
// 亂數產生數字,直到該數字為 78
// 結束時,設定 fgDone_a,並停止 timer1
void count_a()
{
Random rd = new Random();
try
{
while (guess_a != num_a)
{
guess_a = rd.Next(1, 101);
Thread.Sleep(100);
}
fgDone_a = true;
}
catch (ThreadAbortException ex)
{
timer1.Enabled = false;
}
catch (ThreadInterruptedException ex)
{
timer1.Enabled = false;
}
}
void count_b()
{
Random rd = new Random();
while (guess_b != num_b && fgRun)
{
guess_b = rd.Next(1, 101);
Thread.Sleep(100);
}
fgDone_b = true;
}
private void timer1_Tick(object sender, EventArgs e)
{
textBox1.AppendText(guess_a.ToString() + "\r\n");
if (fgDone_a)
{
timer1.Enabled = false;
textBox1.AppendText("找到了\r\n");
}
}
// 用 count_a 產生 Thread th_a
// 同時啟動 timer1,timer1 會呼叫 timer1_Tick
private void button1_Click(object sender, EventArgs e)
{
Thread thd = new Thread(count_a);
th_a = thd;
fgDone_a = false;
guess_a = -1;
thd.Start();
timer1.Enabled = true;
}
// 呼叫 th_a.Abort()
private void button2_Click(object sender, EventArgs e)
{
textBox1.AppendText("使用Abort()中止執行緒\r\n");
th_a.Abort();
}
// 呼叫 th_a.Interrupt()
private void button3_Click(object sender, EventArgs e)
{
textBox1.AppendText("Interrupt()中斷執行緒\r\n");
th_a.Interrupt();
}
// 用 count_b 產生 Thread th_b
// 將全域變數 fgRun 設定為 true
// 同時啟動 timer2,timer2 會呼叫 timer2_Tick
private void button4_Click(object sender, EventArgs e)
{
Thread thd = new Thread(count_b);
fgDone_b = false;
guess_b = -1;
fgRun = true;
thd.Start();
timer2.Enabled = true;
}
// 將全域變數 fgRun 設定為 false
private void button5_Click(object sender, EventArgs e)
{
fgRun = false;
}
private void timer2_Tick(object sender, EventArgs e)
{
textBox2.AppendText(guess_b.ToString() + "\r\n");
if (fgDone_b)
{
timer2.Enabled = false;
if(!fgRun)
textBox2.AppendText("結束執行緒\r\n");
else
textBox2.AppendText("找到了\r\n");
}
}
// 關閉 Form1,要停止 th_a,fgRun 設定為 false
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
if(th_a!=null)
th_a.Abort();
fgRun = false;
}
}
}
Thead 存取表單控制項
Form UI 控制項是由 UI Thread 控制,如果自己產生的 Thread 要跨到 UI thread 存取控制項,會發生錯誤。
前面的例子,是在 Thread 裡面計算後,將過程記錄在全域變數中,然後在 UI Thread 以 Timer 定時將資料顯示在 UI 上,這種方法比較麻煩。
透過控制項的 InvokeRequired 屬性及 Invoke(),可讓 Thread 直接存取控制項。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
delegate void SafeCall(string str);
Thread []myThreads= { null, null };
int sum = 0;
public Form1()
{
InitializeComponent();
}
void safeControl() //無參數
{
if (textBox1.InvokeRequired)
{
sum++;
if (sum > int.MaxValue)
sum = 0;
MethodInvoker ivk = new MethodInvoker(safeControl);
textBox1.Invoke(ivk, new object[] { });
}
else
textBox1.AppendText("myThreads[1]: " +
sum.ToString() + "\r\n");
}
void myFunc()
{
while (true)
{
safeControl();
Thread.Sleep(500);
}
}
// 用 InvokeRequired 判斷 textBox1 存取權
// 如果是 true,就表示現在是 UI thread 在控制
// 就要由 textBox1 呼叫一次 SafeCall
// false 就表示為外部 thread 存取,可直接使用 textBox1
void safeControl_param(string str) //有參數
{
if (textBox1.InvokeRequired)
{
SafeCall ivk = new SafeCall(safeControl_param);
textBox1.Invoke(ivk, new object[] { str });
}
else
textBox1.AppendText(str + "\r\n");
}
// myThreads[0] 的 method
// 呼叫 safeControl_param
void myFunc_param()
{
Random rd = new Random();
int num;
while (true)
{
num = rd.Next(1, 101);
safeControl_param("myThreads[0]: " + num.ToString());
Thread.Sleep(500);
}
}
// 用 button1 產生 myThreads[0]
private void button1_Click(object sender, EventArgs e)
{
Thread thd;
// 如果 myThreads[0] 已經存在,就要停止 myThreads[0]
// 用 myFunc_param 重新產生一個 myThreads[0],並啟動
if (myThreads[0] != null)
{
myThreads[0].Abort();
myThreads[0].Join();
}
thd = new Thread(new ThreadStart(myFunc_param));
myThreads[0] = thd;
thd.Start();
}
// 用 button2 產生 myThreads[1]
private void button2_Click(object sender, EventArgs e)
{
Thread thd;
if (myThreads[1] != null)
{
myThreads[1].Abort();
myThreads[1].Join();
}
thd = new Thread(new ThreadStart(myFunc));
myThreads[1] = thd;
thd.Start();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
foreach(var item in myThreads)
if (item != null)
item.Abort();
}
}
}
另一種寫法
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
Thread[] myThreads = { null, null };
int sum = 0;
public Form1()
{
InitializeComponent();
}
void safeControl_param(string str) //有參數
{
textBox1.Invoke(new Action (() =>
{
textBox1.AppendText(str);
}
));
}
// this.Invoke 配合 new Action
void myFunc_param()
{
Random rd = new Random();
int num;
while (true)
{
num = rd.Next(1, 101);
//也能寫成函式來來呼叫:safeControl_param(num.ToString());
this.Invoke(new Action(() =>
{
textBox1.AppendText("myThreads[0]: "+num.ToString()+"\r\n");
}
));
Thread.Sleep(500);
}
}
void safeControl() //無參數
{
sum++;
if (sum > int.MaxValue)
sum = 0;
this.Invoke((MethodInvoker)delegate
{ textBox1.AppendText( "myThreads[1]: " +
sum.ToString() + "\r\n"); }
);
}
void myFunc()
{
while (true)
{
////此段程式也能寫成函式來來呼叫:safeControl();
sum++;
if (sum > int.MaxValue)
sum = 0;
this.Invoke((MethodInvoker)delegate
{
textBox1.AppendText("myThreads[1]: " +
sum.ToString() + "\r\n");
}
);
Thread.Sleep(500);
}
}
// 有參數的 myFunc_param Thread
private void button1_Click(object sender, EventArgs e)
{
Thread thd;
if (myThreads[0] != null)
{
myThreads[0].Abort();
myThreads[0].Join();
}
thd = new Thread(new ThreadStart(myFunc_param));
myThreads[0] = thd;
thd.Start();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
foreach (var item in myThreads)
if (item != null)
item.Abort();
}
// 沒有參數的 myFunc Thread
private void button2_Click(object sender, EventArgs e)
{
Thread thd;
if (myThreads[1] != null)
{
myThreads[1].Abort();
myThreads[1].Join();
}
thd = new Thread(new ThreadStart(myFunc));
myThreads[1] = thd;
thd.Start();
}
}
}
第三種寫法
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
delegate void SafeCall(string str);
Thread []myThread= { null, null };
int sum;
public Form1()
{
InitializeComponent();
}
void myFunc2()
{
MethodInvoker ivk = new MethodInvoker(safeControl);
while (true)
{
sum++;
if (sum > int.MaxValue)
sum = 0;
this.Invoke(ivk, new object[] { });
Thread.Sleep(500);
}
}
void safeControl() //無參數
{
textBox1.AppendText(sum.ToString() + "\r\n");
label1.Text = sum.ToString() ;
}
void myFunc1()
{
SafeCall ivk = new SafeCall(safeControl_param);
Random rd = new Random();
int num;
while (true)
{
num = rd.Next(1, 101);
this.Invoke(ivk, new Object[] { num.ToString()+"\r\n" });
Thread.Sleep(500);
}
}
private void safeControl_param(string str)
{
textBox1.AppendText(str);
label1.Text = str;
}
private void button1_Click(object sender, EventArgs e)
{
Thread thd = new Thread(myFunc1);
myThread[0] = thd;
thd.Start();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
foreach (var item in myThread)
if (item != null)
item.Abort();
}
private void button2_Click(object sender, EventArgs e)
{
Thread thd = new Thread(myFunc2);
myThread[1] = thd;
sum = 0;
thd.Start();
}
}
}
執行緒同步
如果有多個 thread 會同時存取相同的資源,會造成內容一致性的問題。有四個方法,可處理 synchronization 問題
synchronized code region -> 最常用
當 thread A 執行時,thread B 必須等待 A 完成後,才能執行該區塊
manual synchronization
使用 .Net Framework 的 Mutex, Semaphore, EventWaitHandle, AutoResetEvent, ManualResetEvent
synchronized contexts
使用 SynchronizationAttribute 設定 ContextBoundObject
當物件進入或離開由 ContextBoundObject 定義的內容時,會強制執行規則
使用 System.Collections.Concurrent 的集合與類別
Critical Section: thread 存取的共用資源(物件、資料、變數、設備),同一時間只有一個 thread 能使用
lock()
互斥鎖: public object locker = new object();
lock(locker)
以 locker 鎖定 critical section
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
Thread thd1=null, thd2=null;
object locker = new object();
bool fg1 = false, fg2=false;
public Form1()
{
InitializeComponent();
}
// 當 Form1 載入時,就啟動兩個 thread
private void Form1_Load(object sender, EventArgs e)
{
thd1 = new Thread(func1);
thd2 = new Thread(func2);
thd1.Start();
thd2.Start();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
if (thd1 != null)
thd1.Abort();
if (thd2 != null)
thd2.Abort();
}
// 無窮迴圈
// 當 fg1 為 true,就 lock locker 物件,執行 critical section 區塊
// 完成後將 fg1 改為 false
void func1()
{
while(true)
{
if(fg1)
lock(locker)
{
for(int i=0;i<10;i++)
{
textBox1.Invoke(new Action(() =>
{
textBox1.AppendText("Thread 1\r\n");
}));
Thread.Sleep(500);
}
fg1 = false;
}
}
}
void func2()
{
while (true)
{
if (fg2)
lock (locker)
{
textBox1.Invoke(new Action(() =>
{
textBox1.AppendText("Thread 2\r\n");
}));
fg2 = false;
}
}
}
// 將 fg1 改為 true
private void button1_Click(object sender, EventArgs e)
{
if (!fg1)
fg1 = true;
}
private void button2_Click(object sender, EventArgs e)
{
if (!fg2)
fg2 = true;
}
}
}
Monitor
method | 說明 |
---|---|
Enter(a, [b]) | 取得並鎖定互斥鎖 a,a為 Object,b 為 Boolean。當 b 為 true,表示已經取得互斥鎖 a,否則為 false |
Exit(a) | 釋放互斥鎖 a |
IsEntered(a) | 判斷 thread 是否已經取得互斥鎖 a |
Pulse(a) | 通知等候的 thread,互斥鎖 a 已改變狀態 |
PulseAll(a) | 通知等候 queu 的所有 threads,互斥鎖 a 已改變狀態 |
TryEnter(a,b,c) | 嘗試在時間 b 取得互斥鎖 a,並回傳結果 c。 a: Object, b: Int32 or TimeSpan, c: Boolean |
TryEnter(a[,b]) | 嘗試在時間 b 取得互斥鎖 a。 a: Object, b: Int32 or TimeSpan |
TryEnter(a[,b]) | 嘗試取得互斥鎖 a,並回傳結果 b。 a: Object, b: Boolean |
Wait(a[,b[,c]]) | 釋放互斥鎖 a ,並在時間 b 內,嘗試重新取得互斥鎖 a。如果無法取得,就會進入等候 queue a: Object, b: Int32 or TimeSpan, c: Boolean |
Enter, Exit 要搭配使用,否則會造成 a 無法釋放或 deadlock
Pulse() 只能被正在鎖定互斥鎖的 thread 呼叫
critical section
Monitor.Enter(locker);
.
.
.
Monitor.Exit(locker);
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
Thread thd1 = null, thd2 = null;
object locker = new object();
bool fg1 = false, fg2 = false;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
thd1 = new Thread(func1);
thd2 = new Thread(func2);
thd1.Start();
thd2.Start();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
if (thd1 != null)
thd1.Abort();
if (thd2 != null)
thd2.Abort();
}
void func1()
{
while (true)
{
if (fg1)
{
Monitor.Enter(locker);
for (int i = 0; i < 10; i++)
{
textBox1.Invoke(new Action(() =>
{
textBox1.AppendText("Thread 1\r\n");
}));
Thread.Sleep(500);
}
Monitor.Exit(locker);
fg1 = false;
}
}
}
void func2()
{
while (true)
{
if (fg2)
{
Monitor.Enter(locker);
textBox1.Invoke(new Action(() =>
{
textBox1.AppendText("Thread 2\r\n");
}));
Monitor.Exit(locker);
fg2 = false;
}
}
}
private void button1_Click(object sender, EventArgs e)
{
if (!fg1)
fg1 = true;
}
private void button2_Click(object sender, EventArgs e)
{
if (!fg2)
fg2 = true;
}
}
}
Semaphore
5 人去店裡繳費,但只有 3 個櫃檯
一般來說臨櫃劉成為:取號碼牌、有空的櫃檯時叫號、沒有空的櫃檯就等待
Semaphore 提供機制協調多個 thread 的同步處理
// 有三個號誌數量,最多可接受3個 thread 要求號誌
Semaphore smphore = new Semaphore(3,3);
// 有0個號誌數量,最多可接受3個 thread 要求號誌
// 一開始,所有 thread 都在等待
Semaphore smphore = new Semaphore(0,3);
// 釋放 3 個號誌
smphore.Release(3);
// 沒有參數,表示釋放先前取得的號誌
smphore.Release();
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
Semaphore smphore;
Thread[] thds=new Thread[5];
delegate void SafeCall(string str);
public Form1()
{
InitializeComponent();
}
void safeControl(string str)
{
textBox1.AppendText(str);
}
void func(object param)
{
int no = (int)param;
string str;
SafeCall ivk = new SafeCall(safeControl);
str = String.Format("第{0}位在排隊...\r\n", no);
textBox1.Invoke(ivk, new Object[] { str});
// 所有 threads 會停在這裡等待 semaphore
smphore.WaitOne();
str = String.Format("第{0}位正在繳費...\r\n", no);
textBox1.Invoke(ivk, new Object[] { str });
Thread.Sleep(1000);
str = String.Format("第{0}位繳費結束...\r\n", no);
textBox1.Invoke(ivk, new Object[] { str });
smphore.Release();
// 結束時,釋放 semaphore
}
private void Form1_Load(object sender, EventArgs e)
{
// 載入時,產生 semaphore
smphore = new Semaphore(0, 3);
}
private void button1_Click(object sender, EventArgs e)
{
// 產生並啟動 5 個 threads
for (int i = 0; i < thds.Length; i++)
thds[i] = new Thread(
new ParameterizedThreadStart(func));
for (int i = 0; i < thds.Length; i++)
thds[i].Start(i + 1);
textBox1.AppendText("尚未開始營業...\r\n");
Thread.Sleep(3000);
textBox1.AppendText("開始營業...\r\n");
smphore.Release(3);
}
}
}
沒有留言:
張貼留言