需要帮助跟踪线程冻结

发布于 2024-11-24 07:48:35 字数 4770 浏览 4 评论 0原文

我正在使用此问题中描述的调用:跨线程/原子检查同步?

我需要创建一个任何线程都可以调用的方法调用程序,它将在执行过程中的特定给定点在主执行线程上执行。

我最终使用了 Invoker 类的实现: 我知道这在锁定方面可能不是最有效的,但理论上它的工作方式与 Thread.MemoryBarrier() 非常相似,例如 SLaks 建议的方式。

编辑:根据 MRAB 的建议。

public class Invoker
{
    private Queue<Action> Actions { get; set; }

    public Invoker()
    {
        this.Actions = new Queue<Action>();
    }

    public void Execute()
    {
        Console.WriteLine("Executing {0} actions on thread {1}", this.Actions.Count, Thread.CurrentThread.ManagedThreadId);

        while (this.Actions.Count > 0)
        {
            Action action;

            lock (this.Actions)
            {
                action = this.Actions.Dequeue();
            }

            action();
        }

        Console.WriteLine("Executed, {0} actions left", this.Actions.Count);
    }

    public void Invoke(Action action, bool block = true)
    {
        if (block)
        {
            Console.WriteLine("Invoking");
            SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);

            lock (this.Actions)
            {
                this.Actions.Enqueue(delegate
                {
                    try
                    {
                        action();
                        Console.WriteLine("Actioned");
                    }
                    catch
                    {
                        Console.WriteLine("Exception thrown by action");
                        throw;
                    }
                    finally
                    {
                        semaphore.Release();
                        Console.WriteLine("Released");
                    }
                });
            }

            Console.WriteLine("Enqueued");

            Console.WriteLine("Waiting on thread {0}", Thread.CurrentThread.ManagedThreadId);
            semaphore.Wait();
            Console.WriteLine("Waited");
            semaphore.Dispose();
        }
        else
        {
            this.Actions.Enqueue(action);
        }
    }
}

许多 Console.WriteLine 可以帮助我跟踪我的冻结情况,无论此日志记录是否存在,这种情况都会发生(即,它们不对冻结负责,可以作为罪魁祸首而被丢弃)。

冻结发生在以下场景中:

  1. 执行线程在循环中运行(调用Invoker.Execute)。
  2. 在另外 2 个线程上,相对同时调用 2 个方法(调用 Invoker.Invoke)。
  3. 第一个方法可以正常工作并被调用,但第二个方法在“等待”后(即 semaphore.Wait() 之后)冻结。

示例输出:

Executing 0 actions on thread 1
Executed, 0 actions left
Executing 0 actions on thread 1
Executed, 0 actions left
Invoking
Enqueued
Waiting on thread 7
Executing 1 actions on thread 1
Actioned
Released
Executed, 0 actions left
Waited
Invoking
Enqueued
Waiting on thread 8

怀疑发生的情况是执行线程以某种方式阻塞,因此不执行第二个排队操作,并且不释放信号量(semaphore. Release()),从而不允许执行继续进行。

但这非常奇怪(在我看来),因为执行是在信号量阻塞之外的另一个线程上执行的,所以它不应该阻塞,对吗?

我试图构建一个测试用例来重现上下文环境中的问题,但我无法让它重现。我将其发布在这里作为我之前解释的 3 个步骤的说明。

static class Earth
{
    public const bool IsRound = true;
}

class Program
{
    static Invoker Invoker = new Invoker();

    static int i;

    static void TestInvokingThread()
    {
        Invoker.Invoke(delegate { Thread.Sleep(300); }); // Simulate some work
    }

    static void TestExecutingThread()
    {
        while (Earth.IsRound)
        {
            Thread.Sleep(100); // Simulate some work

            Invoker.Execute();

            Thread.Sleep(100); // Simulate some work
        }
    }

    static void Main(string[] args)
    {
        new Thread(TestExecutingThread).Start();

        Random random = new Random();

        Thread.Sleep(random.Next(3000)); // Enter at a random point

        new Thread(TestInvokingThread).Start();
        new Thread(TestInvokingThread).Start();
    }
}

输出(如预期发生的那样):

Executing 0 actions on thread 12
Executed, 0 actions left
Executing 0 actions on thread 12
Executed, 0 actions left
Invoking
Enqueued
Waiting on thread 13
Invoking
Enqueued
Waiting on thread 14
Executing 2 actions on thread 12
Actioned
Released
Waited
Actioned
Released
Waited
Executed, 0 actions left
Executing 0 actions on thread 12
Executed, 0 actions left
Executing 0 actions on thread 12

实际问题:此时我要问的是,是否有经验丰富的线程程序员可以看到 Invoker 中的逻辑错误类可能永远使其阻塞,因为我认为不可能发生这种情况。同样,如果您可以说明一个使其阻塞的测试用例,我可能可以找到我的错误所在。 我不知道如何隔离问题。

注意:我很确定这并不是因为其特殊性而真正被视为质量问题,但我主要是发帖是绝望的求助,因为这是业余编程,我没有同事可以问。 经过一天的尝试和错误,我仍然无法修复它。

重要更新:我刚刚在第一次调用时也出现了这个错误,不一定只在第二。因此,它确实可以在调用者中自行冻结。但如何呢?在哪里?

I'm using an invocation described in this question: Synchronization accross threads / atomic checks?

I need to create an method invoker that any thread can call, which will execute on the main executing thread at a specific given point in its execution.

I ended up using this implementation of the Invoker class:
I am aware that is might not be the most efficient in terms of locking, but it's theoretically working in a similar enough way than Thread.MemoryBarrier(), such as the one SLaks suggested.

EDIT: With MRAB's suggestions.

public class Invoker
{
    private Queue<Action> Actions { get; set; }

    public Invoker()
    {
        this.Actions = new Queue<Action>();
    }

    public void Execute()
    {
        Console.WriteLine("Executing {0} actions on thread {1}", this.Actions.Count, Thread.CurrentThread.ManagedThreadId);

        while (this.Actions.Count > 0)
        {
            Action action;

            lock (this.Actions)
            {
                action = this.Actions.Dequeue();
            }

            action();
        }

        Console.WriteLine("Executed, {0} actions left", this.Actions.Count);
    }

    public void Invoke(Action action, bool block = true)
    {
        if (block)
        {
            Console.WriteLine("Invoking");
            SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);

            lock (this.Actions)
            {
                this.Actions.Enqueue(delegate
                {
                    try
                    {
                        action();
                        Console.WriteLine("Actioned");
                    }
                    catch
                    {
                        Console.WriteLine("Exception thrown by action");
                        throw;
                    }
                    finally
                    {
                        semaphore.Release();
                        Console.WriteLine("Released");
                    }
                });
            }

            Console.WriteLine("Enqueued");

            Console.WriteLine("Waiting on thread {0}", Thread.CurrentThread.ManagedThreadId);
            semaphore.Wait();
            Console.WriteLine("Waited");
            semaphore.Dispose();
        }
        else
        {
            this.Actions.Enqueue(action);
        }
    }
}

The many Console.WriteLine are there to help me track my freezing, which happens regardless of whether or not this logging is present (i.e., they are not responsible of the freezing and can be discarded as culprits).

The freezing occurs in a scenario where:

  1. An execution thread runs in a loop (calling Invoker.Execute).
  2. On 2 other threads, 2 methods are invoked relatively simultaneously (calling Invoker.Invoke).
  3. The first method works and gets invoked fine, but the second one freezes after "Waiting", that is, after semaphore.Wait().

Example output:

Executing 0 actions on thread 1
Executed, 0 actions left
Executing 0 actions on thread 1
Executed, 0 actions left
Invoking
Enqueued
Waiting on thread 7
Executing 1 actions on thread 1
Actioned
Released
Executed, 0 actions left
Waited
Invoking
Enqueued
Waiting on thread 8

What I suspect to be happening is that the execution thread somehow blocks, hence not executing the second enqueued action, and not releasing the semaphore (semaphore.Release()), and thus not allowing the execution to proceed.

But that is extremely weird (in my opinion), since the execution is on another thread than the semaphore blocking, and so it shouldn't block, right?

I've tried to build a test case that reproduces the problem out of the context environment, but I can't get it to reproduce. I post it here as an illustration of the 3 steps I explained earlier.

static class Earth
{
    public const bool IsRound = true;
}

class Program
{
    static Invoker Invoker = new Invoker();

    static int i;

    static void TestInvokingThread()
    {
        Invoker.Invoke(delegate { Thread.Sleep(300); }); // Simulate some work
    }

    static void TestExecutingThread()
    {
        while (Earth.IsRound)
        {
            Thread.Sleep(100); // Simulate some work

            Invoker.Execute();

            Thread.Sleep(100); // Simulate some work
        }
    }

    static void Main(string[] args)
    {
        new Thread(TestExecutingThread).Start();

        Random random = new Random();

        Thread.Sleep(random.Next(3000)); // Enter at a random point

        new Thread(TestInvokingThread).Start();
        new Thread(TestInvokingThread).Start();
    }
}

Output (as supposed to occur):

Executing 0 actions on thread 12
Executed, 0 actions left
Executing 0 actions on thread 12
Executed, 0 actions left
Invoking
Enqueued
Waiting on thread 13
Invoking
Enqueued
Waiting on thread 14
Executing 2 actions on thread 12
Actioned
Released
Waited
Actioned
Released
Waited
Executed, 0 actions left
Executing 0 actions on thread 12
Executed, 0 actions left
Executing 0 actions on thread 12

The actual question: What I'm asking, at this point, is if any experienced threading programmer can see a logical mistake in the Invoker class that could ever make it block, as I see no possible way of that happening. Similarly, if you can illustrate a test case that makes it block, I can probably find where mine went wrong. I don't know how to isolate the problem.

Note: I'm quite sure this is not really regarded as a quality question for its specificity, but I'm mostly posting as a desperate cry for help, as this is hobby programming and I have no coworker to ask. After a day of trial and error, I still can't fix it.

Important update: I just had this bug occur on the first invocation too, not necessarily only on the second. Therefore, it can really freeze by itself in the invoker. But how? Where?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

陪你到最终 2024-12-01 07:48:35

我认为你应该在入队和出队时锁定操作。我偶尔会在这里遇到空引用异常:

this.Actions.Dequeue()();

可能是因为竞争条件。

我还认为排队的代码不应该处理信号量,而应该将其留给排队线程:

        Console.WriteLine("Invoking");
        SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);

        this.Actions.Enqueue(delegate
        {
            action();
            Console.WriteLine("Actioned");
            semaphore.Release();
            Console.WriteLine("Released");
        });

        Console.WriteLine("Enqueued");

        Console.WriteLine("Waiting");
        semaphore.Wait();
        Console.WriteLine("Waited");
        semaphore.Dispose();

同样是因为竞争条件。

编辑:我想到,如果操作由于某种原因引发异常,信号量将不会被释放,所以:

                this.Actions.Enqueue(delegate
                {
                    try
                    {
                        action();
                        Console.WriteLine("Actioned");
                    }
                    catch
                    {
                        Console.WriteLine("Exception thrown by action");
                        throw;
                    }
                    finally
                    {
                        semaphore.Release();
                        Console.WriteLine("Released");
                    }
                });

这可能是问题所在吗?

编辑:修改时是否锁定 this.Actions

出队时:

            Action action;
            lock (this.Actions)
            {
                action = this.Actions.Dequeue();
            }
            action();

和入队时:

            lock (this.Actions)
            {
                this.Actions.Enqueue(delegate
                {
                    ...
                });
            }

I think you should lock on Actions when enqueuing and dequeuing. I occasionally had a null-reference exception here:

this.Actions.Dequeue()();

probably because a race condition.

I also think that the enqueued code should not dispose of the semphore, but just leave that to the enqueuing thread:

        Console.WriteLine("Invoking");
        SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);

        this.Actions.Enqueue(delegate
        {
            action();
            Console.WriteLine("Actioned");
            semaphore.Release();
            Console.WriteLine("Released");
        });

        Console.WriteLine("Enqueued");

        Console.WriteLine("Waiting");
        semaphore.Wait();
        Console.WriteLine("Waited");
        semaphore.Dispose();

again because of race conditions.

EDIT: It has occurred to me that if the action throws an exception for some reason, the semaphore won't be released, so:

                this.Actions.Enqueue(delegate
                {
                    try
                    {
                        action();
                        Console.WriteLine("Actioned");
                    }
                    catch
                    {
                        Console.WriteLine("Exception thrown by action");
                        throw;
                    }
                    finally
                    {
                        semaphore.Release();
                        Console.WriteLine("Released");
                    }
                });

Could that be the problem?

EDIT: Are you locking this.Actions when modifying it?

When dequeuing:

            Action action;
            lock (this.Actions)
            {
                action = this.Actions.Dequeue();
            }
            action();

and enqueuing:

            lock (this.Actions)
            {
                this.Actions.Enqueue(delegate
                {
                    ...
                });
            }
挽清梦 2024-12-01 07:48:35

知道了。这是我的程序中一个非常深的多层死锁,因此我无法轻松地重现它。不过,我会将 MRAB 的答案标记为已接受,因为它可能是由调用者本身引起的锁定的真正原因。

Got it. It was a really deep multi-layer deadlock across my program, hence why I couldn't reproduce it easily. I will mark MRAB's answer as accepted however, for it might have been a real cause of locks caused by the Invoker itself.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文