iT邦幫忙

0

C# Task陣列加上一般陣列傳入的問題

各位IT達人好
我有關於TASK傳入陣列的問題想詢問有何解決方案
我宣告了一個陣列
所有陣列也都在同一個方法裡面
但是不知道為什麼
在task外,可以正常的看到a[]這些陣列的內容
但是在task裡面,陣列的內容就完全空白,沒有任何值被帶入
想請問是有什麼步驟錯了嗎
還請各位幫忙解答,謝謝

            string[] a = new string[11];
            a[1] = textBox11.Text;
            a[2] = textBox12.Text;
            a[3] = textBox13.Text;
            a[4] = textBox14.Text;
            a[5] = textBox15.Text;
            a[6] = textBox16.Text;
            a[7] = textBox17.Text;
            a[8] = textBox18.Text;
            a[9] = textBox19.Text;
            a[10] = textBox20.Text;

            for (int i = 1; i < 10; i++)
            {
                if (Directory.Exists(a[i]))
                {
                    Task[] task = new Task[11];
                    task[i] = Task.Factory.StartNew(() =>
                    {
                        MessageBox.Show(a[i]);
                    });
                }
            }

2 個回答

3
小碼農米爾 Mir
iT邦研究生 1 級 ‧ 2020-05-24 10:55:06
最佳解答

線上測試: https://dotnetfiddle.net/t0crNs

我測了一下這段程式。

string[] a = new string[]
{
    "A", "B", "C"
};

Task[] task = new Task[3];

for (int i = 0; i < 3; i++)
{
    task[i] = Task.Factory.StartNew(() =>
    {
		try
		{
			Console.WriteLine(a[i]);
		}
		catch(Exception ex)
		{
			Console.WriteLine(ex.Message);
		}
    });
}

結果如下,在 Task 中出現了 Exception。

Index was outside the bounds of the array.
Index was outside the bounds of the array.
Index was outside the bounds of the array.

原因

首先 Task.Factory.StartNew() 會開三個新的執行續
去執行 () => {} 這個匿名方法

在方法內會用到 i 這個變數
由於 i 不是匿名方法的參數或在方法內宣告的
所以會共用外面 for 迴圈的 i 變數
和 JS 的 Scope Chain 是一樣的概念

接著因為這三段程式是非同步執行
所以 for 迴圈會先跑完
接著才執行三個 Task

由於 for 已經跑完
所以三個 Task 內讀到的 i 都會是 3
因此最後才會出現索引超出陣列範圍的錯誤


解法

要解決 i 被共用這個問題,有兩個做法

1. 在 for 迴圈內用另一個變數把 i 存起來

for (int i = 0; i < 3; i++)
{
	var index = i;
    task[i] = Task.Factory.StartNew(() =>
    {
		Console.WriteLine(a[index]);
    });
}

2. 使用可傳入參數版本的 StartNew

for (int i = 0; i < 3; i++)
{
    task[i] = Task.Factory.StartNew((index) =>
    {
		Console.WriteLine(a[(int)index]);
    }, i);
}

結果

A
C
B
看更多先前的回應...收起先前的回應...
fish iT邦新手 5 級 ‧ 2020-05-24 14:32:44 檢舉

簡單來說
因為() => {}這個是新建立方法
a[i]不在方法內
必須要重新帶入數值至方法
可以這樣理解嗎

最穩的解法應該是您說的解法2
直接把i由函數傳入task這樣對嗎

理解正確
解法1、2 都可以,結果一樣
挑一個喜歡的
/images/emoticon/emoticon39.gif

fish iT邦新手 5 級 ‧ 2020-05-24 22:54:36 檢舉

謝謝你耐心的詳細解答

stevenno2 iT邦新手 5 級 ‧ 2020-05-30 15:00:05 檢舉

雖然這個例子不會,建議使用方法 2 ,避免"執行緒安全"問題。

0
凱大
iT邦見習生 ‧ 2020-05-28 06:39:39

我是不清楚你理解了什麼 但是.....

for (int i = 1; i < 10; i++)
{
    if (Directory.Exists(a[i]))
    {
        //這裡的意思是你每一次都產生一個可存放11個Task 的Array
        Task[] task = new Task[11];
        //然後你在這裡只讓其中一個index也就是當下的配置一個 Task
        //以及 這裡其實並不需要用到 Task.Factory.StartNew
        //下面會繼續解釋1
        task[i] = Task.Factory.StartNew(() =>
        {
            //關於這裡 a[i]的問題請看下方解釋2
            MessageBox.Show(a[i]);
        });
    }
}

解釋1:
排除掉上述邏輯很詭異的情況
實際上你指需要另行宣告一個方法例如像是

public Task ShowMessageAsync(string msg) {
  MessageBox.Show(msg);
  return Task.CompletedTask;
}

然後把程式碼中解釋一下面那段code改成

task[i] = ShowMessageAsync(a[i]); 

即可
解釋2:
雖然我是不清楚你是什麼理由會在使用 ui的情況下還得進行非同步 (webform ?)
但不太建議你在非同步的task中調用 MessageBox (webform 有這個嗎? 就算有這樣調用也不合適)
以及 其實你只要用Linq 來做的話我想應該不至於發生這個問題
整段code 其實你指需要寫這樣

// ShowMessageAsync就是上面的方法
Task.WhenAll(a.Select(text => ShowMessageAsync(text)));
// 甚至可以寫成這樣
Task.WhenAll(a.Select(ShowMessageAsync));

你可以不需要知道總共有多少要做這件事情 反正全部都要做
你也可以用async / await來取代Task.Factory.StartNew (這不是常態用法)
最後 你可以不需要自己建立一個陣列來存儲結果
Task.WhenAll 會把你帶入的所有 Task 通通都等到執行完成後才會繼續往下進行
當然別忘了要在前面加上 await 宣告這裡要等到全部運行完

附帶一提: 如果你遇到一個必須提供無參數方法的方法 ex: Task.Run
但你又有需要用的參數必須帶入
在 c# 的匿名方法其實已經能夠有效的解決方法外帶入的變數 背後理論很複雜 這裡就不加追述
如果真的有哪個情境會導致這個方法失效的話
還有另一種方法就是

Func<Task> CreateTask(string message) {
    return () => MessageBox.Show(message);
}

這個做法我還沒失敗過, 到js上也一樣掛保證不會miss message XD

fish iT邦新手 5 級 ‧ 2020-05-29 07:01:52 檢舉

凱大你好
謝謝你的回應
那個messagebox是我測試用的
然後加上TASK是為了想讓他在背景運行
for的問題....後面也有想到,不知道為啥這樣做...所以就改成這樣

            Task[] task = new Task[11];

            for (int i = 1; i < 10; i++)
            {
                if (Directory.Exists(fn[i]))
                {
                    var j = i;
                    task[i] = Task.Factory.StartNew((f) =>
                    {
                        a(Convert.ToString(f));
                    }, fn[i]);
                    textBox30.AppendText(Convert.ToString(NowDateTime) + "  開始" + fn[i]+ "\r\n");
                }
            }

因為沒寫過C#,也沒太多程式的基礎
所以寫法都拼拼湊湊起來的
像是TASK也都沒用過,試寫下去報錯還找不到問題點(在這之前用THREAD,結果發現TASK更簡便,在4.5^還有更簡單的TASK.RUN)
所以寫法才這麼奇怪

不過程式已經完成了,也能正常運行
下次有機會我會代你說的做法進去看看
謝謝你們的回答~

我要發表回答

立即登入回答