C# - 多个线程启动一个线程

时间:2017-11-18 04:26:56

标签: c# multithreading

我有3个并行运行的3个线程。他们每个人都将Neg的{​​{1}}或Fourthclass增加“1”。完成3个主题后,如果Fourclass.Pos > Fourclass.Neg,它将运行Terminal4

问:我如何只运行Terminal4一次。因为在Fourthclass.Terminal4();中放置Terminal1-2-3将运行Terminal4 3次。

这就是我所做的:

public class Firstclass
{
    static int Pos = 1;
    static int Neg = 0;
    public static void Terminal1()
    {
        if (Pos > Neg)
        {
            Fourthclass.Pos += 1;
         // Fourthclass.Terminal4();
        }
    }
}
public class Secondclass
{
    static int Pos = 1;
    static int Neg = 0;
    public static void Terminal2()
    {
        if (Pos > Neg)
        {
            Fourthclass.Pos += 1;
         // Fourthclass.Terminal4();
        } 
    }
}
public class Thirdclass
{
    static int Pos = 1;
    static int Neg = 0;
    public static void Terminal3()
    {
        if (Pos > Neg)
        {
            Fourthclass.Neg += 1;
         // Fourthclass.Terminal4();
        }
    }
}
public static class Fourthclass
{
    public static int Pos = 0;
    public static int Neg = 0;
    public static void Terminal4()
    {
        if (Pos > Neg)
        {
            Console.WriteLine("Pos = {0} - Neg = {1}", Pos, Neg);
            Console.WriteLine();
        }
        else { Console.WriteLine("fail"); }
    }
}
class Program
{
    static void Main(string[] args)
    {
        Thread obj1 = new Thread(new ThreadStart(Firstclass.Terminal1));
        Thread obj2 = new Thread(new ThreadStart(Secondclass.Terminal2));
        Thread obj3 = new Thread(new ThreadStart(Thirdclass.Terminal3));
        obj1.Start();
        obj2.Start();
        obj3.Start();
    }
}

1 个答案:

答案 0 :(得分:3)

原始答案

通过...这些增量不是线程安全的,它们可能会遇到ABA problem而忽略线程可见性问题。

对于该问题,请使用InterlokedInterlocked.IncrementInterlocked.Decrement会照顾它。

现在,要使代码块只运行一次,请保留一个int变量,如果它确实运行则为1,如果没有,则0。然后使用Interlocked.CompareExchange

int didrun;

// ...

if (Interlocked.CompareExchange(ref didrun, 1, 0) == 0)
{
    // code here will only run once, until you put didrun back to 0
}

当然还有其他方法。这个只是非常多才多艺。

附录:好的,它的作用......

Interlocked.CompareExchange将查看传递的变量(didrun)的值,将其与第二个参数(0)进行比较,如果匹配,则会将变量更改为值第一个参数(1)[因为它可能会改变变量,你必须通过ref传递它]。返回值是它在变量中找到的值。

因此,如果它返回0,则表示它找到0,这意味着它确实将值更新为1。现在,下次调用这段代码时,变量的值为1,因此Interlocked.CompareExchange返回1,并且线程不会进入块。

好的,为什么不使用bool因为线程可见性。线程可能会更改变量的值,但此更新可能仅在CPU缓存中发生,而对其他线程不可见...... Interlocked可以解决该问题。只需使用Interlocked即可。 MSDN是你的朋友。

无论您使用的是ThreadPoolTask还是async/await还是普通的Threads,这都行不通。我提到这是因为我想建议使用那些......

指向Threading in C#的偷偷摸摸的链接。

扩展答案

在评论中,您会询问不同的行为:

  

Terminal4的冷却时间直到下一次运行

好吧,如果有一个冷静(我理解为一段时间)你不仅需要存储代码运行的任何内容,而且还需要存储它最后一次的时间。

现在,条件不能仅仅在没有运行的情况下运行"但是如果它还没有运行,如果它从上一次运行到现在的时间段大于冷却时间#34;则运行#34;

我们必须检查多件事,这是一个问题。现在检查将不再是原子的(来自拉丁语 atomus ,这意味着不可分割,来自 a - "不是" + tomos "切割,切片,体积,部分")。

这是相关的,因为如果检查不是原子的,我们就会回到ABA问题。

我将用这个案例来解释ABA问题。如果我们编码以下内容:

1. Check if the operation has not run (if it has not go to 4)
2. Get the last time it ran
3. Compute the difference from the last run to now (exit if less than cool down)
4. Update the last run time to now
5. Run code

两个线程可能会执行以下操作:

 |
 t  Thread1: Check if the operation has not run (it has)
 i  Thread2: Check if the operation has not run (it has)
 m  Thread2: Get the last time it ran
 e  Thread1: Get the last time it ran
 |  Thread1: Compute the difference from the last run to now (more than cool down)
 v  Thread2: Compute the difference from the last run to now (more than cool down)
    Thread2: Update the last run time to now
    Thread2: Run code
    Thread1: Update the last run time to now
    Thread1: Run code

如你所见,他们都是Run code

我们需要的是在单个atomic操作中检查和更新的方法,这样检查操作将改变另一个线程的结果。这就是我们Interlocked所获得的。

Interlocked如何设法做到这一点超出了问题的范围。可以说有一些特殊的CPU指令。

我建议的新模式如下(伪代码):

bool canRun = false;
DateTime lastRunCopy;
DateTime now = DateTime.Now;
if (try to set lastRun to now if lastRun is not set, copy lastRun to lastRunCopy)
{
    // We set lastRun
    canRun = true;
}
else
{
    if ((now - lastRunCopy) < cooldown)
    {
        if (try to set lastRun to now if lastRun = lastRunCopy, copy lastRun to lastRunCopy)
        {
            // we updated it
            canRun = true;
        }
    }
    else
    {
            // Another thread got in
    }
}
if (canRun)
{
    // code here will only run once per cool down
}

注意我已经用&#34来表达操作;如果X是Z,则尝试将X设置为Y,将X复制到W&#34;这是Interlocked.CompareExchange的工作方式。

有一个原因我把它留在伪代码中,那就是DateTime is not an atomic type

为了使代码有效,我们必须使用DateTime.Ticks。对于未设置的值,我们将使用0(00:00:00.0000000 UTC,0001年1月1日),这是您需要担心的,因为它可以在几千年的时间内降温。

此外,当然,我们会使用Interlocked.CompareExchange的重载long,因为DateTime.Ticks属于该类型。

注意:啊,我们将使用TimeSpan.Ticks进行冷却。

代码如下:

long lastRun = 0;
long cooldown = TimeSpan.FromSeconds(1).Ticks; // Or whatever, I do not know.

// ...

bool canRun = false;
long lastRunCopy = 0;
long now = DateTime.Now.Ticks;
lastRunCopy = Interlocked.CompareExchange(ref lastRun, now, 0);
if (lastRunCopy == 0)
{
    // We set lastRun
    canRun = true;
}
else
{
    if ((now - lastRunCopy) < cooldown)
    {
        if (Interlocked.CompareExchange(ref lastRun, now, lastRunCopy) == lastRunCopy)
        {
            // we updated it
            canRun = true;
        }
        else
        {
            // Another thread got in
        }
    }
}
if (canRun)
{
    // code here will only run once per cooldown
}

或者,如果你想把它归结为一个条件:

long lastRun = 0;
long cooldown = TimeSpan.FromSeconds(1).Ticks; // Or whatever, I do not know.

// ...

long lastRunCopy;
var now = DateTime.Now.Ticks;
if
(
    (lastRunCopy = Interlocked.CompareExchange(ref lastRun, now, 0)) == 0
    || now - lastRunCopy < cooldown
    && Interlocked.CompareExchange(ref lastRun, now, lastRunCopy) == lastRunCopy
)
{
    // code here will only run once per cooldown
}

正如我所说,Interlocked.CompareExchange是多才多艺的。虽然如你所见,你仍然需要考虑要求。