我怎样才能成为线程安全的?

时间:2018-03-20 10:02:13

标签: c# multithreading thread-safety

我正在寻找一个应用程序,我将处理几个集成并需要它们在线程中运行。我需要线程“向母舰报告(又称主循环)”。摘录:

class App
{
    public delegate void StopHandler();
    public event StopHandler OnStop;

    private bool keepAlive = true;

    public App()
    {
        OnStop += (() => { keepAlive = false; });

        new Thread(() => CheckForStop()).Start();
        new Thread(() => Update()).Start();

        while (keepAlive) { }
    }

    private void CheckForStop()
    {
        while (keepAlive) if (Console.ReadKey().Key.Equals(ConsoleKey.Enter)) OnStop();
    }

    private void Update()
    {
        int counter = 0;

        while (keepAlive)
        {
            counter++;

            Console.WriteLine(string.Format("[{0}] Update #{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), counter));

            Thread.Sleep(3000);
        }
    }
}

这里的问题是变量keepAlive。通过它的使用它不是线程安全的。我的问题是我如何才能使其线程安全。

如果Update使用while(true)代替keepAlive而事件OnStop中止线程,会变得安全(r)吗?

4 个答案:

答案 0 :(得分:1)

使用对象和this method

class App
{
    public delegate void StopHandler();
    public event StopHandler OnStop;
    private object keepAliveLock = new object();
    private bool keepAlive = true;

....
    private void Update()
    {
        int counter = 0;

        while (true)
        {
            lock(keepAliveLock)
            {
                 if(!keepAlive) 
                      break;
            }
            counter++;

            Console.WriteLine(string.Format("[{0}] Update #{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), counter));

            Thread.Sleep(3000);
        }
    }
}

请注意,每次访问keepAlive都需要锁定(由lock语句环绕)。处理死锁情况。

答案 1 :(得分:0)

就问题的具体措辞和示例代码(关于keepAlive)而言,只需使用volatile(对于简单的读取权限bool })。至于是否有其他方法可以改善您的代码,这是另一个故事。

private volatile bool keepAlive = true;

对于简单类型和访问,例如bool,那么volatile就足够了 - 这可以确保线程不会缓存该值。

bool的读取和写入 Atomic ,如C# Language Spec中所示。

  

以下数据类型的读写是原子的:bool,char,   byte,sbyte,short,ushort,uint,int,float和reference types。

<强>此外

  

CLI规范的第I.6节,第12.6.6节说明:“符合CLI   应保证对正确对齐的内存进行读写访问   所有的位置都不大于本机字大小是原子的   对某个位置的写访问大小相同。

还值得查看volatile (C# Reference)

  

volatile关键字表示字段可能被修改   多个线程同时执行。是的领域   声明的volatile不受编译器优化的限制   假设由单个线程访问。这确保了最多   字段中始终存在最新值。

答案 2 :(得分:0)

我个人会改变这一点,等待bool变为false以使用ManualResetEvent。还使用System.Timers.Timer进行更新而不是循环:

private ManualResetEvent WaitForExit;
private ManualResetEvent WaitForStop;

public App()
{
    WaitForExit = new ManualResetEvent(false);
    WaitForStop = new ManualResetEvent(false);

    new Thread(() => CheckForStop()).Start();
    new Thread(() => Update()).Start();

    WaitForExit.WaitOne();
}

private void CheckForStop()
{
    while (true) 
        if (Console.ReadKey().Key.Equals(ConsoleKey.Enter)) 
        {
            WaitForStop.Set();
            break;
        }
}

private void Update()
{
    int counter = 0;

    Timer timer = new Timer(3000);
    timer.Elapsed += Timer_Elapsed;
    timer.AutoReset = true;
    timer.Start();

    WaitForStop.WaitOne();

    timer.Stop();

    WaitForExit.Set();
}

private int counter = 1;
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
    Console.WriteLine(string.Format("[{0}] Update #{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), counter++));
}

答案 3 :(得分:0)

正如您自己注意到的那样,您在线程之间共享的可变keepAlive变量会导致您的头痛。我的建议是删除它。更一般地说:

停止在线程之间共享可变状态

基本上所有多线程问题都来自共享可变状态的线程。

在执行打印的对象上设置keepAlive私有实例变量。让该类实例化它自己的线程,并将发送给该​​对象的所有消息放在ConcurrentQueue上:

class Updater
{
    // All messages sent to this object are stored in this concurrent queue
    private ConcurrentQueue<Action> _Actions = new ConcurrentQueue<Action>();

    private Task _Task;
    private bool _Running;
    private int _Counter = 0;

    // This is the constructor. It initializes the first element in the action queue,
    // and then starts the thread via the private Run method:
    public Updater()
    {
        _Running = true;
        _Actions.Enqueue(Print);
        Run();
    }

    private void Run()
    {
        _Task = Task.Factory.StartNew(() =>
        {
            // The while loop below quits when the private _Running flag
            // or if the queue of actions runs out.
            Action action;
            while (_Running && _Actions.TryDequeue(out action))
            {
                action();
            }
        });
    }

    // Stop places an action on the event queue, so that when the Updater
    // gets to this action, the private flag is set.
    public void Stop()
    {
        _Actions.Enqueue(() => _Running = false);
    }

    // You can wait for the actor to exit gracefully...
    public void Wait()
    {
        if (_Running)
            _Task.Wait();
    }

    // Here's where the printing will happen. Notice that this method
    // puts itself unto the queue after the Sleep method returns.
    private void Print()
    {
        _Counter++;

        Console.WriteLine(string.Format("[{0}] Update #{1}",
            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), _Counter));

        Thread.Sleep(1000);

        // Enqueue a new Print action so that the thread doesn't terminate
        if (_Running)
            _Actions.Enqueue(Print);
    }
}

现在我们只需要一种方法来停止线程:

class Stopper
{
    private readonly Updater _Updater;
    private Task _Task;

    public Stopper(Updater updater)
    {
        _Updater = updater;
        Run();
    }

    // Here's where we start yet another thread to listen to the console:
    private void Run()
    {
        // Start a new thread
        _Task = Task.Factory.StartNew(() =>
        {
            while (true)
            {
                if (Console.ReadKey().Key.Equals(ConsoleKey.Enter))
                {
                    _Updater.Stop();
                    return;
                }
            }
        });
    }

    // This is the only public method!
    // It waits for the user to press enter in the console.
    public void Wait()
    {
        _Task.Wait();
    }
}

将它们粘合在一起

我们现在真正需要的是main方法:

class App
{
    public static void Main(string[] args)
    {
        // Instantiate actors
        Updater updater = new Updater();
        Stopper stopper = new Stopper(updater);

        // Wait for the actors to expire
        updater.Wait();
        stopper.Wait();

        Console.WriteLine("Graceful exit");
    }
}

进一步阅读:

上述封装线程可变状态的方法称为Actor Model

前提是,所有线程都由它们自己的类封装,并且只有该类可以与线程交互。在上面的示例中,这是通过将Action放在并发队列上然后逐个执行它来完成的。