线程知识总结

来源:互联网 发布:海岛奇兵22级兵种数据 编辑:程序博客网 时间:2024/06/05 10:49
一、线程的概念
线程是进程内部分一条执行序列,进程至少有一个线程。
可以通过线程库创建线程,我们将main函数执行虚了称之为主线程,
新生成的线程称之为函数线程。

二、线程与进程的区别
 1、进程是资源分配的最小单位,线程是处理器调度的最小单位。
 2、父子进程只共享文件描述符,线程之间共享全局数据、堆区数据和文件描述符 。
 3、创建线程所需要的资源比创建进程小。线程切换的效率更高。

三、线程的创建方式
 1、用户级(多对一模式)
 在我们多对一模式中,我们根据图就应该能够知道,我们的线程的调度时是由用户负责,我们的操作系统只在内核维护一张进程表,它看不到进程里面的线程,它将那些线程都看成一个进程。
 优点:切换线程时,不需要陷入内核。
      缺点:用户编程麻烦,而且当一个线程阻塞时,内核会觉得整个进程都阻塞了,会选择其他进程运行,但是其实这个进程中的其他线程并没有阻塞,但是操作系统会因为一个线程的阻塞而剥夺整个进程使用cpu的权力,把cpu分配给另外的就绪进程。
      2、内核级(一对一模式)
 在我们的一对一模式中,内核不仅维护了一张进程表,它也维护了一张线程表,内核能看到每个进程,同时它也能看到每个进程里的每个线程,并在内核中定义了不同的数据 结构去分别管理进程和线程。
 优点:编程简单,线程切换由内核负责,一个线程的阻塞也不会影响其他线程被阻塞。
      缺点:每次线程切换都需要陷入内核,这样效率会有所降低,而且内核的资源十分宝贵,但是我们内核管理线程,也需要建立线程表,管理每个线程需要TCB结构,会占用内核宝贵空间,线程的数量远远大于进程的数量,一旦内核空间耗尽,就会需要杀死其他的进程,会造成客户的不满,或者是系统性能的不稳定,操作系统可能崩溃。
 3、混合模式
    能结合两者的优点,我们将可能会阻塞的线程设为内核级,交给内核管理。同时这种多对多的模式,就算一个线程阻塞,也不会让整个进程都阻塞。比如我们一个进程里有5个线程,两个一组,另外三个一组,分别对应内核中的两个线程,这样就算有1个线程阻塞了,还有另外一个组线程可以运行。

四、如何使用线程库创建线程
int pthread_create(pthread_t *id,pthread_attr_t *attr,void*(*pthread_fun)(void*),void* arg)
id:线程标识符,必须在创建线程时设置,必须传地址。
attr:线程属性  NULL
pthread_fun :线程函数,指定创建函数线程之后执行的函数。
arg:指定给线程函数传递的参数   NULL
编译的时候:gcc -o main mian.c -lpthread  因为我们用到了线程库。
五、主线程和函数线程的关系 当我们的函数线程创建之后,两个就是独立的线程,谁先调度不一定,所有有可能造成函数线程还没有完后它的功能,主线程就结束了。当我们主线程结束的时候,我们的进程就会结束并且释放它所占有的资源,所有这样的话我们函数线程就完成嫩个不了它的任务了,我们要怎样解决这个问题呢?      1、int pthread_exit(pthread_t id, void* )功能:只结束调用这个函数的线程,如果是主线程调用,并不会因为它结束掉,然后调用exit然后整个进程的资源释放,我们的子线程还可以运行,等到线程都结束之后,它会自动调用exit结束掉这个进程。 2、int pthread_join(pthread_t id,void **retval);这个函数的功能就是等待指定的线程结束,我们的二级指针是带出函数进程的退出状态的。 注意:这里我们为什么会传二级指针呢? 因为一般退出信息都是字符串,字符串通过什么传递出来呢?当然是字符指针,但是因为我问要获得这个字符指针,所有就需要我们传一个值进去,那这个指针是什么类型呢?我们本来的类型是一级指针,我们都知道要想把在函数里对它的修改带出来,就得传指针,解引用,所以我们应该出传递的是指针的指针,也就是二级指针。讲清楚原理了,我们来看看我们的使用代码测试。
                                
打印结果:
                                 
我们会发现我们在线程中调用的pthread_exit里传入的error,可以通过pthread_join里传入的二级指针带出来。
六、线程之间的数据同步共享问题
我们主线程和函数线程之间的全局数据、堆区数据还有文件描述符是共享的,因为我们看过源码的小伙伴都知道,我们的文件描述符是在pcb中有一个struct file filp[]的那么一个数组,我们的线程共享它,这些都是共享的,那什么是不共享的呢——栈。大家仔细想一想函数栈帧的开辟和回退可能就能想到我们各个函数的正常运行前提
就是有独立栈帧的开辟和回退,各个函数互相不影响,不然数据就乱了。让我们写程序来测一下吧。
      1、测试全局数据和堆区数据
                       
执行结果:
                       
数据修改成功,所以可以证明我们的堆区和全局变量的数据是共享的。
2、测试文件描述符
      
我们可以看到我们在主线程中打开,但是在函数线程中却可以写入,所以我们知道不管在进程还是在线程中,我们的文件描述法都是共享的。
七、线程同步
      我们还是一样从代码中获得检验,从代码中学习函数的使用,我们要写的题目是:
      主线程等待用户输入,子线程统计用户输入的字符数,主线程每次输入后要等待子线程完成统计。
      
      1、信号量
         我们使用到我们操作信号量的几个函数:
         a)int sem_init(sem_t *sem, int shared, int val);
         b)int sem_wait(sem_t *sem);
         c)int sem_post(sem_t *sem);
         d)int sem_destroy(sem_t *sem);
         
         void fun()
         
         int main()
        执行结果:
       
      2、互斥锁
      pthread_mutex_t mutex;
      pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattrt_t *attr);
      pthread_mutex_lock(pthread_mutex_t *mutex);
      pthread_mutex_umloc(pthread_mutex_t *mutex);
       
       
       我们这样执行会有什么问题呢?
       
      
     我们会发现我们的子线程根本就没有运行,因为我们一解锁,子线程还没来得及锁上,又被子线程自己锁上了,所以我们需要再解锁后睡眠1s,保证另外一个线程获得锁。
     执行结果:
     
八、线程安全
     
     一个函数被重入的情况:
     a)多个线程同时执行这个函数
     b)函数自身调用自身
如果一个函数它是可重入的,表示它满足以下要求:
     a)不使用任何(局部)静态或者全局的非const变量。
     b)不返回任何(局部)静态或全局的非const变量的指针。
     c)仅依赖于调用方提供的参数
     d)不依赖任何单个资源的锁。
     e)不调用任何不可重入的函数。
     可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。
     strok_r是strok的可重入版本
     
九、线程的fork使用
     我们来探讨一下我们线程中如果调用fork,fork之前主进程中有锁,那我们fork之后锁会复制吗,怎样复制呢?
     我们先来推导一遍,如果我们锁是同一把锁,也就是锁是共享的,它并没有产生复制,那就不会产生死锁,函数线程释放锁之后,看调度算法决定谁先调度的时候因为需要这把锁发生了阻塞,其实就是我们的函数线程产生后,我们的操作系统调度算法决定了谁先调度,也就决定谁因为没有获得锁先阻塞,然后释放之后,开始先阻塞的进程拿到这把锁,等它释放锁之后,我们的另一个进程再去获取锁,然后释放,这样子,就不会发生死锁问题。第二种可能,就是锁会发生复制,也就是说,我们的锁在子进程中也会有一份,但是复制的话,是怎样复制呢,是给它新创建一个锁,赋予初始的解锁状态,还是会把主进程中锁的状态也一起复制下来呢?我们用代码来检测一下吧!
                    
                   
              执行结果:
                   
我们会发现我们的进程死锁了,这是什么原因呢?这说明这就符合了我们第二种猜想的第二种情况,会连同锁的装填都复制给锁。
       我们看一下我们这个要怎么解决。我们有一种方法是在我们fork之前把所有的锁都锁好,保证在fork的复制过程中我们复制过去的锁的状态
就是锁好的,这样我们fork之后我们就有每个进程里面都有锁了,而且锁都是锁上的。然后我们在fork之后,让两个函数都进行解锁,这样两把锁在两个进程中的时候都是没有锁上的。然后我们可以在各自的进程中分别使用这把锁了。
其实系统给我们提供了一个函数
       pthread_atfork(void(*prepare)(void),void (*father)(void),void (*child)(void));
       这个函数是一个注册函数,我们需要再fork创建进程之前将这个函数注册好,因为我们这个函数的作用就是在fork之前调用prepare,在fork分别在父子进程中调用father和child函数指针指向的函数。
       可能有细心的同学已经发现了一个问题,那就是我们的创建了一个子进程,为什么我们新创建的进程没有执行我们另外一个函数线程,但是我们复制之后的进程并没有执行函数线程,这里大家需要注意,我们创建一个新的进程的时候两个线程都复制了过去,但是我们只会执行调用fork的函数线程。
原创粉丝点击