同时运行代码处理多线程应用程序的最佳方法

时间:2013-12-21 18:45:52

标签: c# .net multithreading service

我有一个自动投注BOT。

我使用Windows服务和计时器在其自己的线程中每30秒启动一次作业,该线程从DB中下注,循环并放置它们。

然而,在某些事件中,当作业太长(超过30秒)时,我可以使用相同的BetPK(唯一ID)进行两次相同的投注,因为它与之前开始的同时运行线程。

我正在使用C#,NET 4,VS 2012。

当我在下注的作业运行时,我在表格中设置了一个“锁定”标志,然后在完成时取消设置。因此,如果另一个作业运行且作业被锁定,它将尽快返回。但这依赖于数据库和网络流量。

在C#中阻止由计时器线程启动的作业与先前启动的线程冲突的最佳方法是什么。我想我可以在服务控制器中设置一个标志来产生线程,所以如果一个工作正在运行,另一个工作就不会产生。

但是我想学习处理这种多重冲突的正确方法。由于2 LAY投注恰好在同一时间,我今天只损失了几百英镑。因为Bet只存在一条记录,所以最后一次下注的Betfair ID已经更新,所以在我查看Betfairs自己的页面之前我对这些副本一无所知。

我已经做过检查以确定在尝试下注之前是否已经下注,但是如果“placebet”方法在完全相同的时间在同一个Bet记录上运行,那么这是不好的。< / p>

任何帮助都非常感激。

由于

3 个答案:

答案 0 :(得分:1)

不,最好的解决方案是将锁保留在数据库中。该应用程序应尽可能无状态。你已经有了一个很好的解决方案。

锁定应用程序内部容易出错并且错误是灾难性的(死锁,应用程序停止工作直到手动重新启动)。使用数据库锁定要容易得多,并且可以恢复错误。

只需正确锁定数据库。问一个新问题,你在哪里发布你正在做什么的细节。我建议您XLOCK您正在处理的任何投注工作。这样他们只能执行一次。使用数据库锁和事务的强大功能来实现这一目标。到目前为止 比app-level线程更容易。

答案 1 :(得分:0)

你总是可以尝试实现像Redis(redis.io)这样提供内置POP功能(http://redis.io/commands/lpop)的数据库。 Redis有一个C#客户端,对于速度至关重要的任何类型的应用程序都非常有用,因为它将整个数据库保留在内存中。它还是单线程,可以轻松实现多消费类应用的分销商。

我还建议查看http://kkovacs.eu/cassandra-vs-mongodb-vs-couchdb-vs-redis,因为它列出了Redis和其他dbs的优缺点。可能会帮助您做出未来的数据库决策。

答案 2 :(得分:0)

老问题,我知道,但是我想把它扔到那里,任何偶然发现它的人。

C#(可能是VB.NET)为处理线程同步提供了几个很好的选择。您可以使用lock关键字阻止执行,直到给定锁定可用,或Monitor.TryEnter()如果要指定锁定超时(可能是立即)。

对于这些方法中的任何一种,您都需要一个用于锁定的对象。几乎任何物体都会这样做;如果你不同步访问某些对象本身(集合,数据库连接等等),你甚至可以实例化一次性object。对于轮询计时器,后者是典型的。

首先,确保您有一个用于同步的对象:

public class DatabasePollingClass {
    object PollingTimerLock = new object();
    ...

现在,如果您希望轮询线程无限期地阻止等待转弯,请使用lock关键字:

public class DatabasePollingClass {
    object PollingTimerLock = new object();
    ...

    protected void PollingTimerCallback() {
        lock (PollingTimerLock) {
            //Useful stuff here
        }
    }
}

一次只能在lock (PollingTimerLock)代码块中允许一个线程。所有其他线程将无限期地等待,然后在他们自己获得锁定后立即恢复执行。

但是,您可能不想要这种行为。如果您希望后续线程立即中止(或在短暂等待之后),如果另一个轮询线程仍在运行,则可以在获取锁定时使用Monitor.TryEnter()。但这需要稍微谨慎一点:

public class DatabasePollingClass {
    object PollingTimerLock = new object();
    ...

    protected void PollingTimerCallback() {
        if (Monitor.TryEnter(PollingTimerLock)) { //Acquires lock on PollingTimerLock object
            try {
                //Useful stuff here
            } finally {
                //Releases lock.
                //You MUST do this in a finally block! (See below.)
                Monitor.Exit(PollingTimerLock);
            }
        } else {
            Console.WriteLine("Warning: Polling timer overlap. Skipping.");
        }
    }
}

另外需要注意的是,与lock关键字不同,Monitor.TryEnter()要求您在完成锁定时手动释放锁定。为了保证发生这种情况,您需要将整个关键部分包装在try块中,并释放finally块中的锁。这是为了确保即使轮询方法失败或提前返回也会释放锁定。如果方法在没有释放锁的情况下返回,则程序将被挂起,因为没有其他线程可以获取锁。

另一个没有使用锁定机制的选项是配置你的Timer没有重复周期,即一次性定时器。在轮询方法结束时,您将丢弃旧的Timer,并设置一个新的(您还需要在finally块内执行此操作,以保证在方法结束时重置Timer) 。如果您希望自上次轮询的 end 以来的某个时间间隔轮询数据库,则此方法很有用。它是一个微妙的区别,但它也解决了并发轮询尝试的问题。

请注意,这是真正的简单线程并发示例。只要所有锁定都发生在与UI线程分开的线程上(消息泵本身可能成为争用的焦点),并且您只能锁定单个对象,那么您不应该过分担心死锁。那些调试真的很不愉快;症状通常是&#34;应用程序停止响应,现在您可以猜测哪些线程正在等待什么&#34;。