美文网首页C++多线程程序员
3.linux多线程编程。

3.linux多线程编程。

作者: 和蔼的zhxing | 来源:发表于2019-01-07 15:10 被阅读44次

    也是最近看YOLOV3的源码的时候接触到这里,demo()函数里是用到多线程编程的。我一开始是把线程这里是略掉的,后来发现实际上检测的函数就是通过线程来组织的,所以不得不看这里的知识,大部分的参考这篇文章,用自己的语言理解一遍写下来。

    1. 进程和线程。

    • 以下是原文摘录加注释:

    进程是程序执行时的一个实例,即它是程序已经执行到何种程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。(所以说一个进程是可以分配系统资源的一个基本单位,进程之间是互相独立的,一个进程崩溃后,其他的进程可以不受其影响。)

    线程是进程的一个执行流(所以可以简单的理解进程是由线程组成,这些线程也可以独立运行,但是可以共享一个进程中的全部资源。),是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

    进程——资源分配的最小单位,线程——程序执行的最小单位。

    进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。


    2. 使用线程的理由。

    从上面可以总结出线程和进程的区别:进程具有独立的地址空间,线程没有单独的地址空间(同一进程里的不同线程共享进程的地址空间)。
    下面的内容摘自原博客:


    使用多线程的理由之一是和进程相比,它是一种非常"节俭"(消耗资源少)的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间(省时),而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

    使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
    上面说的有点总结一下:
    1. 线程之间花费的资源更小,更省时。
    2. 多线程之间通信更加方便(线程之间是共享数据空间的)
    数据共享带来的一个问题是同一个变量不能同时被两个线程所修改,所有子程序中声明为static的数据有可能给多线程的程序带来毁灭性的打击,在书写时要特别注意这一点!!

    除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

    • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。

    • 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。感觉这个好高级

    • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。


    上面的大部分都是摘抄来的,主要是写了两个问题:

    1. 什么是线程,和进程之间的区别。
    2. 线程的优点及为何使用线程。

    3. 有关线程操作的一些函数。

    #include<pthread.h>
    
    int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg);
    int pthread_join (pthread_t tid, void ** status);
    pthread_t pthread_self (void);
    int pthread_detach (pthread_t tid);
    void pthread_exit (void *status);
    

    下面一个一个介绍:

    1. #include<pthread.h>
      首先得包含头文件,这个头文件就在/usr/include/文件夹下,我昨天用gcc编译的时候一直提示找不到这个文件,绝对路径写上去也不行。

    后来这样不行我也没办法了,找了个暂时的解决方案,把文件名改成.cpp后缀用g++编译器就可以了。
    1. int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg);
      创建成功时会返回0
      参数解释:
    • pthread_t
      这个类型是一个usigned long int好像,这个在头文件 pthreadtypes.h里有定义。当创建成功时。

    • const pthread_attr_t *attr
      创建线程的属性,比如优先级,初始栈的大小等,一般使用其默认值NULL就可以。

    • void *(*func) (void *)
      函数指针,当指定的函数被创建之后,将执行这个函数。

    • void *arg
      线程将执行的函数的参数,如果要传入多个参数的话,则需要封装到结构体中。

    1. int pthread_join (pthread_t tid, void ** status);
      用于等待某个线程退出,成功则返回0,否则返回Exxx(一个正数)。
    • pthread_t tid
      指定要等待的线程的ID。
    • void ** status
      如果不是NULL的话,那么线程的返回值存储在status指向的空间中。(所以这个里面存的是status的地址?status指向的空间才是结果?,用二级指针是这个原因吧)
    1. pthread_t pthread_self (void);
      用于返回当前线程的ID。
    2. int pthread_detach (pthread_t tid);
      用于指定线程变为分离状态,就像进程脱离终端变为后台进程似得。成功则返回0,不成功则返回Exxx(正数)。如果线程退出,则其所有资源被释放。如果不是分离状态,那么线程必须保留其ID。
    3. void pthread_exit (void *status);
      用于来终止线程,可以指定返回值,以便其他线程通过pthread_join来获取该线程的返回值。
    • `void status
      用来指定线程终止的返回值。

    代码实验。
    我们用上面说的这些知识来实现这样一个功能:


    1. 有一int型全局变量g_Flag初始值为0;
    2. 在主线称中起动线程1,打印“this is thread1”,并将g_Flag设置为1
    3. 在主线称中启动线程2,打印“this is thread2”,并将g_Flag设置为2
    4. 线程序1需要在线程2退出后才能退出
    5. 主线程在检测到g_Flag从1变为2,或者从2变为1的时候退出

    我们先来满足前三点要求:

    #include<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include<errno.h>
    #include<unistd.h>
    #include<iostream>
    
    using std::cout;
    using std::endl;
    
    int g_flag=0;
    
    void *thread1(void *);
    void *thread2(void *);
    
    int main(int argc,char **argv)
    {
        cout<<"enter main"<<endl;
        pthread_t tid1,tid2;    //创建两个线程ID
        int rc1=0,rc2=0;
        rc2=pthread_create(&tid1,NULL,thread2,NULL);      //创建线程,ID和函数指针都给了
        if(rc2!=0)
        {
            cout<<"create thread2 error"<<endl;
        }
        rc1=pthread_create(&tid2,NULL,thread1,NULL);
        if(rc1!=0)
        {
            cout<<"create thread1 error"<<endl;
        }
        //sleep(1);
    
        cout<<"leave main"<<endl;
        
    
        return 0;
    }
    
    void *thread1(void *arg)
    {
        cout<<"enter thread1"<<endl;
        cout<<"this is thread1,  g_flag:"<<g_flag
        <<"\tand the thread ID is"<<(unsigned int)pthread_self()<<endl;
        g_flag=1;
        cout<<"this is thread1,  g_flag:"<<g_flag
        <<"\tand the thread ID is"<<(unsigned int)pthread_self()<<endl;
        cout<<"leave thread1"<<endl;
        pthread_exit(0);
    }
    
    void *thread2(void *arg)
    {
        cout<<"enter thread2"<<endl;
        cout<<"this is thread2,  g_flag:"<<g_flag
        <<"\tand the thread ID is"<<(unsigned int)pthread_self()<<endl;
        g_flag=2;
        cout<<"this is thread2,  g_flag:"<<g_flag
        <<"\tand the thread ID is"<<(unsigned int)pthread_self()<<endl;
        cout<<"leave thread2"<<endl;
        pthread_exit(0);
    }
    

    编译执行:

    zhxing@ubuntu:~/code/C_CPP/pthread$ g++ pthreadtest.cpp -lpthread
    zhxing@ubuntu:~/code/C_CPP/pthread$ ./a.out
    

    注意编译命令之后的-lpthread参数是必须的,否则创建线程可能失败,主要原因是:

    pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库。参考:https://www.cnblogs.com/wjlx2014/p/3643116.html

    这样执行的结果可能是下面的几种情况:

    enter main
    enter thread1
    this is thread1,  g_flag:0      and the thread ID is2291455744
    this is thread1,  g_flag:1      and the thread ID is2291455744
    leave thread1
    enter thread2
    this is thread2,  g_flag:1      and the thread ID is2299848448
    this is thread2,  g_flag:2      and the thread ID is2299848448
    leave thread2
    leave main
    
    enter main
    leave main
    

    我的电脑上编译出来运行的是第二种情况,这是比较好理解,线程是否来得及进行取决于主函数main何时退出,所以我们把注释掉的sleep(1)打开的话,让main函数做1s的延时,这样编译运行就是第一种情况了。

    4. 线程之间的互斥。

    第四点要求,线程1需要在线程2退出之后才退出,似乎是我们在线程1推出前加入线程2(利用pthread_join())就可以了。
    但是实际上不是这样的,g_flag是一个全局变量,两个线程不可以同时对其操作,需要加锁进行保护,对这样的全局变量互斥访问才可以,这就是互斥锁


    互斥锁:
    使用互斥锁(互斥)可以使线程按顺序执行。通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多个线程。互斥锁还可以保护单线程代码。


    互斥锁的相关函数:

    #include <pthread.h> 
    
    int pthread_mutex_lock(pthread_mutex_t * mptr); 
    int pthread_mutex_unlock(pthread_mutex_t * mptr);
    

    对临界资源操作之前需要先使用pthread_mutex_lock加锁,操作完之后再使用pthread_mutex_unlock来解锁,而且之前需要声明一个pthread_mutex_t变量,这个是前面两个函数的参数,具体的代码看第五节。

    5. 线程之间的同步。

    这一节基本上都是复制的参考博客中的内容,因为对这里确实以前没怎么听过,写完代码可能能理解一点。

    第五点要求:主线程在检测到g_Flag从1变为2,或者从2变为1的时候退出。
    这就需要使用到线程同步技术,线程同步需要条件变量。

    条件变量:


    使用条件变量可以以原子方式阻塞线程(原子操作:不可被中断的一个或者一系列操作,这些操作要一次性执行完毕,或者一个都不执行。),直到某个特定条件为真为止。条件变量始终与互斥锁一起使用。对条件的测试是在互斥锁(互斥)的保护下进行的。

    如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作:

    • 唤醒
    • 再次获取互斥锁
    • 重新评估条件

    在以下情况下,条件变量可用于在进程之间同步线程:

    • 线程是在可以写入的内存中分配的
    • 内存由协作进程共享

    相关的函数:

    #include <pthread.h>
     
    int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr); 
    int pthread_cond_signal(pthread_cond_t *cptr); 
    //Both return: 0 if OK, positive Exxx value on error
    

    pthread_cond_wait用于等待某个特定的条件为真,pthread_cond_signal用于通知阻塞的线程某个特定的条件为真了。在调用者两个函数之前需要声明一个pthread_cond_t类型的变量,用于这两个函数的参数。

    为什么条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的呢?因为“某个特性条件”通常是在多个线程之间共享的某个变量。互斥锁允许这个变量可以在不同的线程中设置和检测。

    pthread_cond_wait一般来说只是唤醒等待某个条件变量的一个线程,如果需要唤醒所有等待某个条件变量的进程,需要调用:
    int pthread_cond_broadcast (pthread_cond_t * cptr);

    默认情况下,阻塞的线程会一直等待,直到某个条件变量为帧,但是肯定是可以设置最大的阻塞时间的:
    int pthread_cond_timedwait (pthread_cond_t * cptr, pthread_mutex_t *mptr, const struct timespec *abstime);

    如果最大阻塞时间到了,条件变量还未成真,仍然返回,返回值为ETIME(这是个正数)。

    6. 最终的代码。

    通过前面的介4,5节的介绍,我们来完善第3节中的代码使得其可以满足所有的条件,看着代码可以更好理解4,5节中的内容。

    /**
     * 是否熟悉POSIX多线程编程技术?如熟悉,编写程序完成如下功能:
      1)有一int型全局变量g_Flag初始值为0;
      2)在主线称中起动线程1,打印“this is thread1”,并将g_Flag设置为1
      3)在主线称中启动线程2,打印“this is thread2”,并将g_Flag设置为2
      4)线程序1需要在线程2退出后才能退出
      5)主线程在检测到g_Flag从1变为2,或者从2变为1的时候退出 
     **/
    
    #include<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include<errno.h>
    #include<unistd.h>
    #include<iostream>
    
    using std::cout;
    using std::endl;
    
    typedef void* (*fun)(void *);
    
    int g_flag=0;
    
    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    static pthread_cond_t cond=PTHREAD_COND_INITIALIZER;    
    /**
     * 这两个都是在pthread.h里面定义的类型,不是一个简单的类型,
     * 而是一个复合类型,具体的形式可以去头文件里看一下,这里可以
     * 使用其宏定义初始化。
    */
    
    
    void *thread1(void *);
    void *thread2(void *);
    
    int main(int argc,char **argv)
    {
        cout<<"enter main"<<endl;
        pthread_t tid1,tid2;    //创建两个线程ID
        int rc1=0,rc2=0;
        rc2=pthread_create(&tid2,NULL,thread2,NULL);      //创建线程,ID和函数指针都给了
        if(rc2!=0)
        {
            cout<<"create thread2 error"<<endl;
        }
        rc1=pthread_create(&tid1,NULL,thread1,&tid2);
        if(rc1!=0)
        {
            cout<<"create thread1 error"<<endl;
        }
    
        pthread_cond_wait(&cond,&mutex);     //等待满足条件的cond 
        sleep(1);
    
        cout<<"leave main"<<endl;
        
        exit(0);
    }
    
    void *thread1(void *arg)
    {
        cout<<"enter thread1"<<endl;
        cout<<"this is thread1,  g_flag:"<<g_flag
        <<"\tand the thread ID is"<<(unsigned int)pthread_self()<<endl;
    
        pthread_mutex_lock(&mutex);    //加锁
        if(g_flag==2)
            pthread_cond_signal(&cond);      
            //这里准备改变g_flag的值了,用于通知阻塞的线程
        g_flag=1;
    
    
        cout<<"this is thread1,  g_flag:"<<g_flag
        <<"\tand the thread ID is"<<(unsigned int)pthread_self()<<endl;
        
        pthread_mutex_unlock(&mutex);
        pthread_join(*(pthread_t*)arg,NULL);
        cout<<"leave thread1"<<endl;
    
        pthread_exit(0);
    }
    
    void *thread2(void *arg)
    {
        cout<<"enter thread2"<<endl;
        cout<<"this is thread2,  g_flag:"<<g_flag
        <<"\tand the thread ID is"<<(unsigned int)pthread_self()<<endl;
        pthread_mutex_lock(&mutex);    //加锁。标志是mutex
        if(g_flag==1)
        {
            pthread_cond_signal(&cond);
            //这里准备改变g_flag的值了,用于通知阻塞的线程
        }
        g_flag=2;
    
        cout<<"this is thread2,  g_flag:"<<g_flag
        <<"\tand the thread ID is"<<(unsigned int)pthread_self()<<endl;
        pthread_mutex_unlock(&mutex);    //解锁
        cout<<"leave thread2"<<endl;
        pthread_exit(0);  //离开当前线程
    } 
    

    编译运行结果:

    enter main
    enter thread1
    this is thread1,  g_flag:0      and the thread ID is281450240
    this is thread1,  g_flag:1      and the thread ID is281450240
    enter thread2
    this is thread2,  g_flag:1      and the thread ID is289842944
    this is thread2,  g_flag:2      and the thread ID is289842944
    leave thread2
    leave thread1
    leave main
    

    基本满足了我们要求,4,5节讲的我大概懂了50%,我现在的用途也用不上了,所以先放在这里吧,以后真正用到了再详细看吧!!

    这破东西竟然写了一天多,感觉去看darknet的源码了!

    相关文章

      网友评论

        本文标题:3.linux多线程编程。

        本文链接:https://www.haomeiwen.com/subject/ugqjzftx.html