请教多线程的两个问题

abcmn1234 2016-07-01 10:14:55
我用的vc#2012 + .net framework 4.0,碰到多线程的两个问题,请教一下,谢谢。线程使用new Thread创建出来的。

第一个是,如果有一个共享变量,只在一个线程中写,在另外一个线程中读,读到数据的实时性要求不高,这次读的是写前的值,再多试几次就能拿到写后的值就可以了。这个情况下,不加锁是否可以,因为加锁要多一个lock代码写着挺烦的。 之所以有这个问题,是不知道在读的时候,会不会因为cache line回写,将刚刚写的值冲掉了。

第二个问题,当我在主线程用Abort函数结束线程B的时候,如果线程B此时正好在lock函数里面,这种情况下,线程b被结束后,被lock的资源会被自动释放吗,谢谢。
...全文
461 28 打赏 收藏 举报
写回复
用AI写文章
28 条回复
切换为时间正序
请发表友善的回复…
发表回复
microyzy 2016-07-03
  • 打赏
  • 举报
回复
11楼上面的那个例子,个人认为是需要lock的。 一个比较宏观的例子,新线程有可能没有被实际执行而主线程先行执行,虽然可能性极低但这种极端情况是可能的。 从原理上说需不需要lock取决于你需不需要原子操作,实际上即使是基本类型变量的操作,只要是共享变量,加锁会是一个好的选择。或者说如果你不确定需不需要加锁,那就请加锁,与读写无关。 即使在机器指令的层面也不能保证基本类型变量操作是原子操作,特别是在多处理器的系统上。一个例子是lz说的cache line的问题。 而如果你需要原子操作但不去加锁,程序移植,编译环境更新,os系统变化硬件环境变化都可能会引发问题。 当然如果是你说我的程序只考虑目前的诸如单核环境,桌面客户端程序之类的,不考虑同步也不那么重要,99.99%可能都不会出现问题。但在服务器程序上,为了那0.01%加锁是值得的。 个人见解。
abcmn1234 2016-07-01
  • 打赏
  • 举报
回复
引用 10 楼 sp1234 的回复:
所有的编程者都知道 Abort 通常应该禁用。如果你要使用 Abort,我希望你在了解了这个常识之后再任性地用它!以免引起不必要的麻烦。
谢谢,用你的代码试验了一下,确实,在新线程中try catch一下,然后在主线程中abort后,那个mm的lock是自动释放的。 ps,记住我,我以后不用abort了,编辑器自动提示的函数,然后我就没有了解的直接用了,任性了,:)
wanghui0380 2016-07-01
  • 打赏
  • 举报
回复
至于你问的另外一个问题,会不会锁“资源”,我们说得看是什么资源,如果是独占性质的资源,就得注意释放,比如你独占了一个文件,程序崩溃你没释放,那么就来问题了 比如说 你线程里 做 File.openwrite 他独占了这个文件,你要不释放他,别人可用不了
abcmn1234 2016-07-01
  • 打赏
  • 举报
回复
受教了,我这里确实没有必要使用lock的,因为一个线程读写,另外一个线程只读,其实和是否lock一点关系都没有的。 关于我担心的硬件上的cache line的问题,对于共享变量a,只要他被cache的话,无论哪个线程访问,都是指向了同一个cache,所以,不会存在问题的。 我之前碰到过某特别硬件,同一个变量,可能会被map到两个不同的cacheline中,这种情况,会导致只读线程的cacheline的回写,覆盖了读写线程刚刚完成的写操作。在x86 cpu中,应该是不会存在这样的情况。
wanghui0380 2016-07-01
  • 打赏
  • 举报
回复
另外lock主要用在多线程写上 比如 1个读,多个写,这样才会lock他,不然多个写入顺序就乱了(当然如果你不需要保证写入顺序,也可以不加锁,只要资源本身不是独占型滴也不会异常,只是写入顺序随机了)
wanghui0380 2016-07-01
  • 打赏
  • 举报
回复
对与你问题,老p是对的,简单的int这样的类型,不需要加锁,他不会有问题 而对于队列,列表这类东西因为存在添加/移除,所以需要使用lock以免item的不可预料的变动
wanghui0380 2016-07-01
  • 打赏
  • 举报
回复
看上去是生产/消费型的 如果是这样建议使用线程安全类列表,如果生产者远大于消费者,注意限流保证,以免积压过多 同时如果是我个人会改成可限流阻塞队列,而不是仅仅一个简单类型变量 比如
 BlockingColletion<int> _blockQueue=new  BlockingColletion<int>(bounderCapactiy:1);//因为你每次只要1个值,所以我限流1,如果这个值没有用掉,该类自己会保持lock不用你管理
而接收线程里则会用可取消滴Task去订阅这个队列(目前的语法环境,本身建议弃用Theah线程,而使用Task代替)
Task t=new Task(()=>{
  while(!cancelToken.IsCancellationRequested) //如果没收到取消指令
{
    foreach(var item in __blockQueue.getConsumingEnumerable()) //取出队列里的缓存的值
{
    //这个item就是上次你压入队列的值,因为上面限流了,实际上你也指取得到上次的,不过你这次你已经取了,所以生产者可以继续加
}
}
  
});
t.start()
  • 打赏
  • 举报
回复
另外,我看出来,你对 lock 语句有着极端错误的理解。 lock 语句块中没有什么“资源”,不要扯那个问题。假设写下面这个测试代码
using System;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        private static object flag = new object();
        private static long x;
        private static string y;

        static void Main(string[] args)
        {
            var h = new Thread(t);
            h.Start();
            Thread.Sleep(1000);
            h.Abort();
            Console.WriteLine("x={0}, y={1}", x, y);
            Console.WriteLine();
            Console.ReadLine();
        }

        static void t()
        {
            try
            {
                lock (flag)
                {
                    x = DateTime.Now.Ticks;
                    y = x.ToString();
                    Thread.Sleep(10000);
                }
            }
            catch
            {
                Console.WriteLine("catch操作");
            }
        }
    }
}
这里显然在读取x、y的时候不需要lock。而且实际上在方法 t 中也其实根本不需要写 lock。因为根本不存在数值得一致性冲突问题,用什么 lock 呢? 而你为什么画蛇添足地总是纠结lock?我猜你是纠结所谓的“资源”这个又响亮有没有意义的词儿。 假设代码写成
using System;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        private static object flag = new object();
        private static long x;
        private static string y;

        static void Main(string[] args)
        {
            var h = new Thread(t);
            h.Start();
            Thread.Sleep(1000);
            lock (flag)
            {
                Console.WriteLine("x={0}, y={1}", x, y);
            }
            Console.WriteLine();
            Console.ReadLine();
        }

        static void t()
        {
            try
            {
                lock (flag)
                {
                    x = DateTime.Now.Ticks;
                    y = x.ToString();
                    Thread.Sleep(10000);
                }
            }
            catch
            {
                Console.WriteLine("catch操作");
            }
        }
    }
}
这里主线程会被阻塞10秒钟之后才能打印输出结果。这个时候是锁了什么x、y资源了么? lock 就是根据 flag 变量而做的互斥操作,跟 lock 语句块中的什么“资源”没有半点关系。当 lock 语句块结束,或者因为异常而结束了语句块的时候,lock 锁依赖的阻塞就会结束,就允许另一个等待 Monitor 通知的线程进入。整个lock过程只跟 flag 有关,不用纠结多余的什么“资源”,这个机制也不可能那么低效率地去处理什么资源。
  • 打赏
  • 举报
回复
所有的编程者都知道 Abort 通常应该禁用。如果你要使用 Abort,我希望你在了解了这个常识之后再任性地用它!以免引起不必要的麻烦。
  • 打赏
  • 举报
回复
基本上,禁用 Abort。因为Abort是让线程 B崩溃的异常操作,或者是根本不起任何作用,总之Abort 操作是禁用的。正常的做法是设置一个变量作为 flag,然后所谓“线程B”中的代码在必要的地方来if判断这个flag标志,然后比较优雅地“自己结束”。 假设你允许上述代码在任何地方胡乱抛出异常,不考虑后果,那么lock 语句块自然就跳出了,其它的依据 mm 作为标识的 lock 自然就可以进入。
wdh123love 2016-07-01
  • 打赏
  • 举报
回复
引用 7 楼 abcmn1234 的回复:
[quote=引用 4 楼 sp1234 的回复:] 你的程序没有 lock 的必要。 Abort 并不能正常结束线程,只是触发线程崩溃异常。所谓“被lock的资源”这个概念是无厘头的,因为lock语句块并没有锁什么“资源”。有些人就是爱滥用“资源”这个字眼儿。在lock语句块中,没有什么锁什么“资源”,也就谈不上释放什么资源的说法。
谢谢,比如说在线程B中,正在执行下面的代码 lock(mm) { ...//正在这里执行的时候 } 这个时候主线程调用了 B.Abort,然后,主线程(或者新开一个新线程)又想执行上述代码,是可行的吗,或者说,这里的lock能够进去吗[/quote] 理论上来说B.Abort这个函数应该没那么水,应该会自动处理你说的这些问题~~
abcmn1234 2016-07-01
  • 打赏
  • 举报
回复
引用 4 楼 sp1234 的回复:
你的程序没有 lock 的必要。 Abort 并不能正常结束线程,只是触发线程崩溃异常。所谓“被lock的资源”这个概念是无厘头的,因为lock语句块并没有锁什么“资源”。有些人就是爱滥用“资源”这个字眼儿。在lock语句块中,没有什么锁什么“资源”,也就谈不上释放什么资源的说法。
谢谢,比如说在线程B中,正在执行下面的代码 lock(mm) { ...//正在这里执行的时候 } 这个时候主线程调用了 B.Abort,然后,主线程(或者新开一个新线程)又想执行上述代码,是可行的吗,或者说,这里的lock能够进去吗
abcmn1234 2016-07-01
  • 打赏
  • 举报
回复
引用 3 楼 xdashewan 的回复:
[quote=引用 2 楼 abcmn1234 的回复:] 谢谢,是的,我并不care中间的几个值,可能要好几秒我才会写一次值,而在100毫秒级别我就去读取了,我唯一担心的是,硬件上读buffer的cache line回写,是否可能冲掉刚刚写入的值。是否应该加一个violet关键词?
我并不清楚你硬件的cache line回写会产生怎样的效果,所以关于这一点你需要自己打一下日志进行实际的测试才能得出结论[/quote] 嗯,我用的就是普通的x86架构的64位cpu,就是pc笔记本主流产品,我是忽然脑洞大开的担心,是否有这样的问题。比如说,还有另外一个变量b,刚好和读buffer在同一个cacheline上,那么,当这个b触发回写的话,会不会将这个值也同时写回memory中,在很小概率下,冲掉了刚刚写入的值。 估计难以用日志,我感觉这个问题假如真的存在的话,也是概率非常非常小的,估计很难重现。
  • 打赏
  • 举报
回复
不管你 care 还是“不 care”所谓的“中间的几个值”,你要注意,你说的是“一个共享变量”,而人家纠结的是“中间的几个值”。这其实就是偷换概念。 假设你就是读取一个变量,你不去 lock,你的变量会“一半是这个值、一半又是那个值”吗?完全不会的。因此不管是读多写少,还是写多读少,都根本不用纠结什么lock,不用纠结“care还是不care”。 而假设你要连续读取(比如说)6个变量,你要求这6个变量必须是一次性管理线程操作写入的,也就是一致性,这时候才可能“care”。而这个时候使用lock就是正确的做法,跟什么“队列”毫无关系。因为你要的就是最终变量值(并不要求保留变量值的过往数据),而不是历史任务数据,怎么可能绕到什么队列上去? 所以,这里本身是很简单的东西,不要画蛇添足地抄一堆无关的概念进来。 你要读取一个变量的当时的值,那么无需 lock。如果你要读取多个数值前后操作的一致性(例如在foreach语句遍历一个集合时,不能让集合大小改变,否则就会异常),这个时候才需要 lock。
  • 打赏
  • 举报
回复
你的程序没有 lock 的必要。 Abort 并不能正常结束线程,只是触发线程崩溃异常。所谓“被lock的资源”这个概念是无厘头的,因为lock语句块并没有锁什么“资源”。有些人就是爱滥用“资源”这个字眼儿。在lock语句块中,没有什么锁什么“资源”,也就谈不上释放什么资源的说法。
xdashewan 2016-07-01
  • 打赏
  • 举报
回复
引用 2 楼 abcmn1234 的回复:
谢谢,是的,我并不care中间的几个值,可能要好几秒我才会写一次值,而在100毫秒级别我就去读取了,我唯一担心的是,硬件上读buffer的cache line回写,是否可能冲掉刚刚写入的值。是否应该加一个violet关键词?
我并不清楚你硬件的cache line回写会产生怎样的效果,所以关于这一点你需要自己打一下日志进行实际的测试才能得出结论
abcmn1234 2016-07-01
  • 打赏
  • 举报
回复
引用 1 楼 xdashewan 的回复:
读线程在无论何种场合频率都大于写线程时,不加lock问题也不大,否则会出现写线程连写若干次而读线程只读到最后一次的情况,即使加了lock同样会出现,除非你不care中间的几个值。如果care一般都是写线程写入队列,读线程从队列中读取数据,而不是一个共享变量
谢谢,是的,我并不care中间的几个值,可能要好几秒我才会写一次值,而在100毫秒级别我就去读取了,我唯一担心的是,硬件上读buffer的cache line回写,是否可能冲掉刚刚写入的值。是否应该加一个violet关键词?
xdashewan 2016-07-01
  • 打赏
  • 举报
回复
读线程在无论何种场合频率都大于写线程时,不加lock问题也不大,否则会出现写线程连写若干次而读线程只读到最后一次的情况,即使加了lock同样会出现,除非你不care中间的几个值。如果care一般都是写线程写入队列,读线程从队列中读取数据,而不是一个共享变量
angel6709 2016-07-01
  • 打赏
  • 举报
回复
1. 可以不使用锁 2.abort 不会吧锁释放
abcmn1234 2016-07-01
  • 打赏
  • 举报
回复
引用 19 楼 abcmn1234 的回复:
其实,我的原始问题是这样的,诸位帮忙看看怎么做比较好。 我有两个线程,一个是界面主线程记为A,一个是后台线程记为B,后台线程的任务是在恰当的时候截屏/图片分析/然后将分析结果写入共享变量。在主线程A中,会根据界面的变化或者是用户输入,让B重新开始新的分析,或者让B停止干活(比如此时界面变化,再继续分析已经没有任何意义,反而会出错),这个过程是开始新分析,停止干活,开始新分析,停止干活,,,如此串行的,不会出现同时两个新分析的要求。 我一开始的想法,担心总是频繁创建退出线程耗时,在一开始就创建了一个后台线程,后台线程有个永不停止的大循环,用semaphore来触发开始干活的动作。当有需要的时候,在主线程中设置flag,让后台现在停止干活。 伪代码如下。 while (true) { sem.WaitOne(); while(true) { //具体的任务,就是在不停的截屏,不停的分析 //中间不少地方会调用this.BeginInvoke(委托),使得后台线程的状态在界面文本框中显示出来。 //超时退出 if (timeout in the internal while loop) break; //如果主线程说不要干活了,那就跳出来,(我现在知道这里不需要lock了) if (flag) break; } } 然后,问题来了,在主线程中设置flag为true后,为了避免分析结果出错,我得接下来等待后台线程break后,才能切换界面。这里应该用什么办法等待呢,第一个想到的是再加一个共享变量flag2,然后主线程死循环等着。可是,在后台线程中,可能正在调用this.BeginInvoke(委托),这个时候,岂不是后台线程也在等待主线程的空闲,于是,两者就死锁了? 这个依赖于this.BeginInvoke(委托)的表现,我还没有试验过,到底是否这样的。 由于之前没有机器试验,而且上述flag就变成2个了,容易出错,所以,我就想换一个思路,在每次需要的时候,都新建一个线程,然后不需要的时候,就强制结束这个线程,也就是我之前说abort的缘由,这样,逻辑上很清晰,就是到底多大程度的影响性能还不明确。现在知道abort不应该使用了,就不知道有没有其他的方法。
我试了第一种情况,两者不会死锁的,只是后台线程的显示会等到主线程完成后才会显示出来。 想了一下,为了简化逻辑,还是准备每次新建一个线程,然后用flag的方法关闭线程,请问,就资源性能来说,影响大吗,谢谢。
加载更多回复(8)

111,128

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Creator Browser
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧