V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
leebs
V2EX  ›  C

一个线程的小程序

  •  
  •   leebs · 2018-10-17 19:36:04 +08:00 · 2657 次点击
    这是一个创建于 2010 天前的主题,其中的信息可能已经有所发展或是发生改变。
    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    int counter;
    
    void *thr_fun(void *arg)
    {
            for(int i = 0; i < 10; i++) {
                    printf("%lu %d\n", pthread_self(), ++counter);
            }
    
            return NULL;
    }
    
    int main()
    {
            pthread_t tid1, tid2;
    
            pthread_create(&tid1, NULL, thr_fun, NULL);
            pthread_create(&tid2, NULL, thr_fun, NULL);
    
            sleep(5);
    
            return 0;
    }
    

    某一次的输出结果:

    140031959435008 1
    140031959435008 3
    140031959435008 4
    140031959435008 5
    140031959435008 6
    140031959435008 7
    140031959435008 8
    140031959435008 9
    140031959435008 10
    140031959435008 11
    140031951042304 2
    140031951042304 12
    140031951042304 13
    140031951042304 14
    140031951042304 15
    140031951042304 16
    140031951042304 17
    140031951042304 18
    140031951042304 19
    140031951042304 20
    

    不是多线程嘛,为啥这输出结果先是第一个进程全部打印完,
    然后打印第二个进程的结果;

    counter = 2 时的++ 明显是第二个进程执行的,
    那为啥后面的就是等第一个线程执行完再执行第二个线程;

    12 条回复    2018-10-20 11:28:32 +08:00
    wevsty
        1
    wevsty  
       2018-10-17 19:45:25 +08:00
    估计是 printf 内部的实现存在锁。
    所以线程 2 在 counter 自增完毕进入 printf 的时候就停住了,下一次循环就变 12 了。
    Tyanboot
        2
    Tyanboot  
       2018-10-17 19:47:33 +08:00 via Android
    多线程不加锁是这样的。调度是不确定的。

    第一个线程输出一次之后切换到第二个线程,第二个线程++之后没输出直接切换回第一个线程继续执行呗。

    多跑几次结果都不一样的。甚至有可能 printf 的字符串都是交叉的。
    xenme
        3
    xenme  
       2018-10-17 20:20:08 +08:00 via iPhone
    更可能是一楼的情况 printf 内部又一些 lock 导致 thread 占用更多资源
    Philippa
        4
    Philippa  
       2018-10-17 21:33:57 +08:00 via iPhone
    觉得是输出流被锁定了,但因为初始化的原因两个线程交换了一次,1 然后切换到另一个线程输出 2 但却没能成功到达输出流而是回到 1 继续输出完最后才轮到 2。
    kljsandjb
        5
    kljsandjb  
       2018-10-17 21:39:25 +08:00
    可能是 printf 内有锁,第一个线程先持有了,第二个线程此时插入了,但没拿到锁所以内部只做了一次++counter 然后阻塞没有返回,此时 counter 是 2,线程 1 持有锁继续打印后面的++counter
    innoink
        6
    innoink  
       2018-10-17 22:00:52 +08:00 via Android
    调度是不确定的。不能假设在函数执行过程中不被调度走。
    lrxiao
        7
    lrxiao  
       2018-10-17 22:19:23 +08:00
    你这个 counter 又不是 atomic 的。。。
    调度无法确定
    printf by POSIX 标准是 MT-safe
    xylophone21
        8
    xylophone21  
       2018-10-18 09:53:28 +08:00
    就是线程的不确定性,说 printf 有锁的,给一个优雅的伪代码看看,怎么实现一个函数**返回**了,还能保证上次调用的线程**优先**被调度。
    wevsty
        9
    wevsty  
       2018-10-18 13:05:17 +08:00
    @xylophone21

    void thread()

    {

    while(thread_lock.try_lock() == false)

    {

    sleep_ms(10);

    }

    //do something

    thread_lock.unlock();

    }

    如果 thread 函数被多线程同时循环调用,第一个进入 thread 函数的线程显然可以更优先的被调度。
    当然,这也只是一种实现方法而已,线程锁也完全可以根据线程 id 去优先调度,技术上完全可以实现。

    printf 实际上就是向 stdout 写入数据,没有锁的情况下线程调动应该是完全无序的甚至不能保证输出一行不被打断,针对 LZ 提出的现象,锁这个解释其实是合理的。

    p.s
    MT-Safe 只代表,多线程的情况下这个函数可以被安全的调用,并不代表这个函数本身是原子化的或者不可重入的,因此这个函数本身或者其他函数可以干扰 MT-Safe 函数的行为。
    https://www.gnu.org/software/libc/manual/html_node/POSIX-Safety-Concepts.html
    原文写的很清楚,Being MT-Safe does not imply a function is atomic, nor that it uses any of the memory synchronization mechanisms POSIX exposes to users. It is even possible that calling MT-Safe functions in sequence does not yield an MT-Safe combination.
    xylophone21
        10
    xylophone21  
       2018-10-18 19:32:12 +08:00
    @wevsty

    0. 楼主的问题是为什么线程 A 总是优先被调度 ,而不是打印不会乱。先确定这个前提我们理解是否一致。

    1. 你的代码确实可以保证线程 A 以更高的优先级被调度到,但我并不认为它是一个优雅的实现,它依赖 sleep 降低线程被调度到的概率。其他实现方式都不可避免的都会导致线程 B 先进入函数的情况下,进行一段时间的等待,降低性能,这是我认为不优雅的关键。当然,如果你有需求,那没办法。所以问题的关键变成了这个需求是否合理,我们是否要在这么底层的位置,定义这么一个特性。(假设如果在 Posix 层面对这种行为做了定义,则这个需求是合理的,必要的,只是我暂时还没有想到)

    2. “ MT-Safe 只代表,多线程的情况下这个函数可以被安全的调用”,可以被安全的调用,不代表他对线程有偏好,AB 两个线程一起多次调用,只要打印不相互干扰,都算“被安全的调用”,不一定要 A 先调完 B 再调用,AB 交叉调用也是安全的。你给出的英文原文只是说他可以不是原子的,线程偏好方面的描述,抱歉我没看出来。所以我不认为 MT-Safe 有定义线程方面的偏好,从字面以上 Multi Thread Safe,也看不出这个意思。
    wevsty
        11
    wevsty  
       2018-10-18 22:04:09 +08:00
    @xylophone21

    0、线程 A 被优先调度是从现象的出来的结论,我们在讨论的是为什么会出现这样的现象。如果线程是完全无序执行(无任何同步手段)的,我认为出现的结果将会乱,应该不算偏题太远。

    1、你提出的问题是函数返回了以后保证上次调用的线程被优先调度,这代码在这个意义上并没有问题。
    优雅的实现是很主观的问题,不同的目的下一般是需要取舍的。
    不过既然提到了那还是转头再来看看 printf 这个函数的需求。
    1.1、printf 是一个同步 I/O 函数,目的是格式化信息输出到 stdout,stdout 在一个进程里只有一个,所以不可避免的存在写入冲突,如果实现者想保证 printf 尽可能的输出正确的结果而不是输出一堆无规律的东西的话,锁是必要的。
    1.2、printf 作为一个 I/O 函数,运行速度本身就不能认为是一个常数(因为取决于 I/O 性能),并且一般来说 I/O 的操作相对是比较慢的,并不是一个需要追求高性能的场合。
    如果一定要强调速度,那么内核在不同的线程之间来回调度的开销显然比让一个线程先执行完的开销大,实现平均调度让内核在多线程之间来回切换反而是拖慢速度。
    printf 并不需要刻意的让调用的线程公平的抢占 stdout 这个资源,所以没有必要实现顺序锁,所以互斥锁是很自然的选择。

    至于最后导致的某个线程优先被调用的结果,这个结果本身不是必要的,可能只是实现上导致的副产品。

    p.s 是回复上面提到的线程安全问题,所以我不解释了。
    xylophone21
        12
    xylophone21  
       2018-10-20 11:28:32 +08:00
    @wevsty
    至于最后导致的某个线程优先被调用的结果,这个结果本身不是必要的,可能只是实现上导致的副产品。

    这个结论我认可,既然是副产品,我是否可以理解为这一大堆解释不必然造成“优先被调用的结果”?

    问题是这个副产品才是楼主的问题。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5204 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 07:26 · PVG 15:26 · LAX 00:26 · JFK 03:26
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.