【Linux】线程基本知识概述

来源:互联网 发布:apache artemis 编辑:程序博客网 时间:2024/05/16 07:37
本文内容概述:
1.线程的基本概念(包括线程的定义,线程之间的共享资源和私有资源);
2.基本函数(包括线程的创建,终止,等待,可分离和可切换,当然其中还会涉及互斥锁方面的内容等等)。

1.线程的基本概念:
   在前边的学习中,我们知道,进程是在各自独立的地址空间中运行,如果需要共享资源,则要进行进程间的通信,比如管道,消息队列,信号量,共享内存这些 ,所以完成通信是比较困难的。而线程是进程内的一个执行分支,顾名思义,线程是在进程内部执行,占用着进程的地址空间,所以,线程之间的通信是比较容易的---定义一个公共的buf,然后就能直接进行通信。
   在Linux系统下,是没有线程 这个概念的,都是用进程去模拟线程。
   我们看到的进程有可能是线程,所以,我们将线程称为轻量级进程。
由于线程是进程内部的执行流,所以线程就会共享一些进程的信息,当然也会有自己私有的信息:
线程共享进程的以下资源:
   1)文件描述符信息;
   2)每种信号的处理方式(由于线程是进程内部的执行流,所以每种信号的处理方式也是共享的);
   3)当前工作目录;
   4)各种id信息,比如用户id和组id信息。
线程也会有自己私有的信息:
   1)线程id;
   2)线程的上下文信息(每个线程都必须有自己的上下文信息,存储在自己的PCB中,当然有些操作系统中会     有一种存储线程信息的TCB结构体)
   3)栈空间
   4)调度优先级
   5)errno变量
      在Linux下的C api函数发生异常时,一般会将errno变量(在errno.h文件中)赋予一个整形值,不同的值代表不同的含义,可以通过输出的值来查看出错的信息。
比如:printf("%d",errno);就可以输出异常时的错误码
    6)信号屏蔽字信息,这里的 信号屏蔽字就有点类似于文件的权限信息中的umask。
2.基本函数:
(1)线程的创建

(2)线程的终止:
    线程的终止方法是有3种:
     a.直接在线程函数中return,但是在main函数中return,是表示整个进程的终止;
     b.采用pthread_cancel函数让同一进程中的其他的线程去终止;

程序举例: 
#include<stdio.h>#include<pthread.h>#include<unistd.h>void* pthreadRun(void* arg){    int count = 3;    while(count--)    {        printf("new pthread,tid:%lu,pid:%d\n",pthread_self(),getpid());    }}int main(){    pthread_t tid = 0;    int ret = pthread_create(&tid,NULL,pthreadRun,NULL);    if(ret != 0)    {        perror("pthread_create");        return -1;    }    if(ret != 0)    {        perror("pthread_detach");        return -1;    }    sleep(1);   if(pthread_cancel(tid)!= 0)    {        printf("cancel failed\n");        return 1;    }    int exitCode;    pthread_join(tid,(void**)&exitCode);    printf("new pthread exit code:%d\n",exitCode);    return 0;}

这段程序的运行结果是:


解释: 由于在主线程中已经sleep了1秒之后,才终止新线程的,由于在1秒之内,新线程早已运行完成,所以,就会终止失败。

如果将终止新线程之前的sleep去掉,程序的运行结果究竟是什么呢?

解释:新线程没有执行完成,就被主线程终止了,此时是终止成功的,所以退出码就是-1.

c.使用pthread_exit()自己终止自己。


(3)线程的等待:


(4)线程的可分离性:
    一个线程是可结合的或者是可分离的。一个可结合的线程可以被与之在同一个进程中的其他线程所终止 ;而一个可分离的线程不需要主线程的等待,它的存储器等资源会在它运行结束时被操作系统回收。我们都知道,线程是进程内部的一个执行分支,为什么这么说?因为进程内的线程共享一个地址空间,所以在被分离的线程没有执行完成时,主线程不要退出,否则新的线程无法继续执行。主线程不可以等待被分离的线程。

程序举例:
#include<stdio.h>#include<pthread.h>#include<unistd.h>void* pthreadRun(void* arg){    int count = 3;    while(count--)    {        printf("new pthread,tid:%lu,pid:%d\n",pthread_self(),getpid());        sleep(1);    }}int main(){    pthread_t tid = 0;    int ret = pthread_create(&tid,NULL,pthreadRun,NULL);    if(ret != 0)    {        perror("pthread_create");        return -1;    }    usleep(1000);    ret = pthread_detach(tid);    if(ret != 0)    {        perror("pthread_detach");        return -1;    }     if(pthread_cancel(tid)!= 0)    {        printf("cancel failed\n");        return 1;    }    int exitCode;    if(0 == pthread_join(tid,(void*)&exitCode))    {        printf("wait success.\n");        printf("new pthread exit code:%d\n",exitCode);    }    else    {        printf("wait failed\n");    }    return 0;}
程序运行结果:


解释:从运行结果我们可以看出,我们不可以等待已经被分离的线程,但是可以终止已经分离的线程。

(5)线程的切换:
线程切换的条件:
   a.一个时间片的完成;
   b.操作系统模式的切换(内核态向用户态的切换)。
时间片的结束,会进行线程的切换,这个是比较容易理解的。
为什么操作系统模式的切换就会进行线程的切换?因为线程(在linux系统下,线程也被认为是进程,也有自己的PCB)的PCB是在内核中,只有内核态(也就是系统)才可以进行查看线程的信息,即就是访问PCB。我们知道,系统调用都是在内核态进行完成的,printf函数的底层调用的就是系统调用,所以我们采用printf函数进行显示数据的时候,就可能完成线程的切换。
所以,写一个程序,制造线程的切换。
#include<stdio.h>#include<pthread.h>#include<unistd.h>static int count = 0;void* pthreadRun(void* arg){    int val = 0;    int i = 0;    while(i < 5000)    {        val = count;        printf("new pthread,tid:%lu,pid:%d,count:%d\n",pthread_self(),getpid(),count);        count = val + 1;        i++;    }    return NULL;}int main(){    pthread_t tid1 = 0;    pthread_t tid2 = 0;    int ret = pthread_create(&tid1,NULL,pthreadRun,NULL);    if(ret != 0)    {        perror("pthread_create");    return -1;    }    ret = pthread_create(&tid2,NULL,pthreadRun,NULL);    if(ret != 0)    {        perror("pthread_create");        return -1;    }    pthread_join(tid1,NULL);    pthread_join(tid2,NULL);    printf("count:%d\n",count);    return 0;}

运行结果介于5000到10000之间不等。
如果我们给临界资源加锁,运行完代码之后解锁。代码如下:
#include<stdio.h>#include<pthread.h>#include<unistd.h>static int count = 0;pthread_mutex_t myLock = PTHREAD_MUTEX_INITIALIZER;void* pthreadRun(void* arg){    int val = 0;    int i = 0;    while(i < 3000)    {       pthread_mutex_lock(&myLock);        val = count;        printf("new pthread,tid:%lu,pid:%d,count:%d\n",pthread_self(),getpid(),count);        count = val + 1;        i++;        pthread_mutex_unlock(&myLock);    }    return NULL;}int main(){    pthread_t tid1 = 0;    pthread_t tid2 = 0;    int ret = pthread_create(&tid1,NULL,pthreadRun,NULL);    if(ret != 0)    {        perror("pthread_create");    return -1;    }    ret = pthread_create(&tid2,NULL,pthreadRun,NULL);    if(ret != 0)    {        perror("pthread_create");        return -1;    }    pthread_join(tid1,NULL);    pthread_join(tid2,NULL);    pthread_mutex_destroy(&myLock);    printf("count:%d\n",count);    return 0;}

这样对临界资源进行加锁之后,就可以保证全局变量最终可以加至6000.
关于互斥锁的函数的声明:
1)初始化和销毁函数

2)申请和释放锁资源函数


为什么加锁就可以防止线程之间相互干扰?
   那是因为加锁就可以保证锁之间的代码是一个原子操作(要么执行就能执行完,要么不执行,也就是非0即1的状态)。
那么lock()和unlock()函数是如何实现的呢?
方法一:

解释:我们知道mutex = 0;这一步并不是原子操作,如果两个线程同时调用lock函数,都判断出mutex > 0,然后其中一个线程将mutex置为0,另一个线程并不知道此事,也将mutex置为0,于是两个线程都以为自己获得了锁资源,这样就发生了冲突。所以这种办法是不可行的。
执行赋值操作需要几步呢?
步骤一:将mutex的数据从内存读到寄存器;
步骤二:通过CPU内的运算器将mutex进行赋值;
步骤三:将mutex的值重新写回内存。
方法二:


解释:由于方法一中的赋值操作不是原子操作,导致上述方法不可取。为了解决这个问题,大多体系结构提供了swap或者exchange命令,将寄存器和内存里的值进行交换,就保证了原子操作。
lock函数 语句解释:
将al寄存器中的值放置为0;
将al寄存器和内存中的mutex的值进行交换;
......
如果执行了xchgb指令之后,又有一个线程申请锁资源,此时它看到的mutex就是0,表示不可以申请到资源;
如果执行了movb指令之后,又有一个线程申请锁资源,此时哪个线程获得锁资源,就看自己的优先级或者权限等等问题了。
所以,这个方法就是lock函数和unlock函数里执行的操作了。
关于线程的基本知识就先整理到这里~~


4 0
原创粉丝点击