Приостановить поток, пока метод и все потоки внутри не закончат свою работу?

Я новичок в потоках, и мне было интересно, как их использовать для оценки в недетерминированном конечном автомате.

У меня есть метод, который вызывает другой метод:

public bool Evaluate2(string s)
{
    accepted = false;

    ThreadEval(s, StartState);            

    return accepted; 
}

переменная accepted является членом класса, и я использую ее, чтобы контролировать, когда другие потоки должны останавливаться.

void ThreadEval(string s, State q)
{            
    if (s.Length == 0 && q.IsFinal)
    {
        accepted = true;
        return;
    }

    bool found = true;            
    State current = q;

    for (int i = 0; found && !accepted && i < s.Length; i++)
    {
        found = false;
        foreach (Transition t in current.transitions)
            if (t.symbol == s[i])
            {
                Thread thread = new Thread(new ThreadStart(delegate { ThreadEval(s.Substring(i+1), t.to); }));
                thread.Start();
                found = true;
            }
     }
}

Каждое из моих состояний имеет набор переходов. Переход состоит из символа и состояния, в которое он может перейти, потребляя этот символ. Поэтому всякий раз, когда я нахожу возможный переход, я хочу создать новый поток и изучить остальную часть строки (без текущего символа)...

У меня сейчас 2 проблемы:

  • «Возврат принят» выполняется до того, как все потоки, созданные внутри ThreadEval, закончат их. Есть ли способ гарантировать, что он не вернется, пока эти потоки не закончатся? Я поставил Thread.Sleep(200) перед возвратом, и это сработало, но 200 мс может быть недостаточно для больших строк, и я также не хочу повышать значение, поэтому маленькие строки будут обрабатываться дольше, чем должны. .

  • Код в том виде, в котором он есть, приводил к некоторому исключению индексации... Я на 99,999% уверен, что он правильный, но он предотвратит сбой, только если я вызову подстроку, передающую значение i вместо i + 1... но если я вызову только i, он никогда не дойдет до конца строки и, в зависимости от конфигурации автомата, может привести к бесконечности петля. Я точно не знаю, как работают потоки, но подозреваю, что, возможно, какая-то параллельная обработка изменяет значение i перед нарезкой подстроки. Как я могу гарантировать, что всякий раз, когда я вызываю новый поток, я отбрасываю только текущий символ?

Если у кого-то есть какие-либо предложения о том, как использовать потоки с большей элегантностью, я был бы признателен, пока единственный способ передать параметры в функцию, которой был назначен поток, - это использовать делегат.


person nightshade    schedule 10.06.2014    source источник
comment
Посмотрите на Thread.Join. msdn.microsoft.com/en-us/library /95hbf2ta(v=vs.110).aspx   -  person user1937198    schedule 10.06.2014
comment
Этот метод, который вы предложили, говорит, что блокирует вызывающий поток до тех пор, пока поток не завершится, но позволяет сказать, что Evaluate2 находится в потоке t1, затем я блокирую его, пока не оценю t2 (ThreadEval), но затем t2 может создать t3 и закончить до того, как это сделает t3, что освободит t1 пойти, не так ли? Хотя t3 еще может создать t4 и так далее...   -  person nightshade    schedule 10.06.2014


Ответы (3)


Для блокировки до тех пор, пока поток t не завершится, вы можете использовать Thread.Join.

t.Join();

Это переводит основной поток в состояние IDLE до тех пор, пока поток t не завершится. Это означает, что вам придется отслеживать все потоки, созданные внутри цикла foreach, а затем присоединяться к ним один за другим.

Лучшим способом было бы использовать TPL Task<T> вместо прямого использования потоков. Ваш код будет выглядеть примерно так:

Task ThreadEval(string s, State q)
{
    //...

    List<Task> tasks = new List<Task>();

    for (int i = 0; found && !accepted && i < s.Length; i++)
    {
        found = false;
        foreach (Transition t in current.transitions)
            if (t.symbol == s[i])
            {
                tasks.Add(
                    Task.Run(
                        () => await ThreadEval(s.Substring(i+1), t.to)));
                found = true;
            }
     }

    return Task.WhenAll(tasks);
}

await ThreadEval(...);
  1. Измените подпись, чтобы она возвращала Task вместо void
  2. Создайте список всех запущенных задач
  3. Task.WhenAll создаст новую задачу, которая будет помечена как завершенная, когда все задачи в списке tasks будут отмечены как завершенные. Верните эту задачу.

Затем вызывающий объект await ThreadEval.

person dcastro    schedule 10.06.2014
comment
вау, этот Parallel.ForEach потрясающий, решил обе проблемы за один раз, так как первый вызов ThreadEval не был в другом потоке... спасибо - person nightshade; 10.06.2014

Написал небольшой пример, чтобы помочь вам понять, как это можно сделать https://dotnetfiddle.net/2Djdh7

По сути, вы можете использовать метод Join() для ожидания завершения потока, а с помощью метода ForEach() для списка потоков вы можете дождаться завершения всех потоков в одной строке.

public class Program
{
    public static void Main(string[] args)
    {
        List<int> list = new List<int>();
        list.AddRange(new int[] {10, 200, 300, 400, 234 });

        // create a bunch of threads
        List<Thread> threads = new List<Thread>();
        list.ForEach(x => threads.Add(new Thread(() => ThreadMethod(x))));

        // start them
        threads.ForEach(x => x.Start());

        // wait for them to finish
        threads.ForEach(x => x.Join());

        // this will not print untill all threads have completed
        Console.WriteLine("Done");
    }

    private static void ThreadMethod(int i)
    {
        Thread.Sleep(i);
        Console.WriteLine("Thread: " + i);
    }
}

Выход:

Thread: 10
Thread: 200
Thread: 234
Thread: 300
Thread: 400
Done
person JensB    schedule 10.06.2014
comment
в threads.Add(new Thread(() => ThreadMethod(i))); что это значит? могу ли я заменить своего делегата теми ()? - person nightshade; 10.06.2014
comment
Вы также можете написать new Thread(t => ThreadMethod(x))), () — это просто способ написания лямбда-запроса без объявления переменной делегата/объекта, поскольку в этом случае она нам не понадобится. - person JensB; 10.06.2014

Думаю, здесь вам поможет шаблон async/await. Затем вы можете сделать что-то вроде:

List<Task> runningEvals = new List<Task>();

async Task ThreadEval(string s, State q)
{            
    if (s.Length == 0 && q.IsFinal)
    {
        Task.WaitAll(runningEvals.ToArray()); // wait for all tasks to finish
        runningEvals.Clear();

        accepted = true;
        return;
    }

    bool found = true;            
    State current = q;

    for (int i = 0; found && !accepted && i < s.Length; i++)
    {
        found = false;
        foreach (Transition t in current.transitions)
            if (t.symbol == s[i])
            {
                // start a task and add it to the "running tasks" list
                var task = Task.Run(async () => await ThreadEval(s.Substring(i+1), t.to));
                runningEvals.Add(task);
                found = true;
            }
     }
}

ВНИМАНИЕ: Это не проверенный код (и определенно не "сохраняющий потоки" из-за "общих List<Task>"), но он должен только указать вам направление.

Это можно назвать «асинхронным» через:

await ThreadEval(s, StartState);

или если вы не можете пройти "асинхронно до конца" (и только тогда):

ThreadEval(s, StartState).Wait();
person Christoph Fink    schedule 10.06.2014