linux线程及线程同步

linux线程

linux原本没有线程,后来在windows多线程编程影响下linux内核开发者在进程基础上在功能上做出了类似windows线程的linux版本的线程,linux线程归根到底还是进程,只不过是轻量级的进程,开销比真正进程要小得多,大家还是要明白linuxwindows在线程方面功能虽然类似,但是底层实现是非常不同的。


linux进程大概实现原理

在进程的基础上创建线程,原本进程PCB将会随着线程数量的增加被均分,原本进程的主人将会退化成主线程,但是这些线程所有的PCB所占总和相当于原来进程PCB空间。内核在任务切换时只识别PCB因此线程在效率上相比进程没有什么优势,对于内核而言线程就是进程,唯一的好处就是线程之间通信、同步比进程要方便的多,进程间共享的资源也更多,在系统开销方面也更小,在编写程序时能够更加灵活,例如原来全局变量父子进程不能共享,在线程上线程之间可以共享全局变量,但是有利有弊,像原来进程中经常使用的perror exit函数在多线程内并不能随意使用,如果某个线程调用exit函数,就会像原来杀死进程一样,杀死PCB中所有线程,杀伤范围太大


资源分配

主线程和子线程
共享:

  • .text
  • .bss
  • .data
  • 动态库加载区
  • 环境变量
  • 命令行参数

进程通信:可采取全局变量、堆的形式
不共享:

  • 栈(每个线程栈区是独立的,记录着每个线程各自独立的栈信息,因此线程通信不能通过局部变量进行通信)

多进程和多线程的区别

  • 始终共享的资源
    • 代码
    • 文件描述符
    • 内存映射区 --mmap
  • 线程共享
    • 全局变量
  • 线程节省资源
  • 在编译时需要在编译选项中增加-lpthread,调用标准线程库

线程操作相关函数

获得线程号

#include <pthread.h>
pthread_t pthread_self(void);

线程号比较大,打印输出或者相关操作可采取长整型对应付

创建进程

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                  void *(*start_routine) (void *), void *arg);
  • 参数说明:
    • thread:线程号的传出参数,用来存储线程号,类型为·pthread_t *·类型
    • attr:线程属性无高级用法情况下填写NULL即可
    • void (*start_routine) (void ):看起来比较复杂其实是一个参数为void *类型,返回值为void *类型的函数指针
    • arg:是第三个函数的具体参数
  • 返回值:
    • 成功返回0,失败返回错误号
    • perror()不能使用该函数打印错误信息
      注意:
  • 主线程先退出(使用exitreturn形式),子线程会被强制结束
    下面是程序
#include<pthread.h>
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
void *func(void* arg)
{
    int i;
    printf("this is son thread\n");
    for(i=0;i<5;i++)
    {
        printf("i is %d\n",i);
        sleep(1);
    }

}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,func,NULL);
    printf("son thread id is %ld,father thread is %ld\n",id,pthread_self());
    sleep(2);
    return 0;
}

通过程序运行结果,我们可以看到父线程结束退出时,尽管子线程还没运行结束,但是随着父线程的结束子线程被迫结束了,

root@ThinkPad:/home/image/mycode/linux/pthread# ./mypthread 
son thread id is 140527533901568,father thread is 140527542400832
this is son thread
i is 0
i is 1
  • 线程共享全局变量(小例程)
#include<pthread.h>
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int num;
void *func(void* arg)
{
    int i;
    for(i=0;i<5;i++)
    {
        num++;
        printf("son: num is %d\n",num);
        sleep(1);
    }

}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,func,NULL);
    for(int i=0;i<5;i++)
    {
        num++;
        printf("father:num is %d\n",num);
        sleep(2);
    }
    return 0;
}

程序运行结果显示父子线程对全局变量的操作能影响对方,说明全局变量他们是共享的

root@ThinkPad:/home/image/mycode/linux/pthread# ./mypthread 
father:num is 1
son: num is 2
son: num is 3
father:num is 4
son: num is 5
son: num is 6
father:num is 7
son: num is 8
father:num is 9
father:num is 10

线程打印错误信息

在进程里面,根据函数返回值,判断某些操作执行失败,然后调用perror函数就可以打印错误信息,但是多线程挤在同一区域,错误信息各个线程是共享的即使有错误发生,打印出来错误信息,也不见得是本个线程自身的错误信息可能是别的线程有错误发生还没来得及打印,被别的线程抢先打印了,所以线程的错误信息不能调用perror函数打印,那么怎样打印线程的错误信息呢?,我们查看pthread_create函数帮助手册,手册在返回值这章介绍说,如果函数执行成功则返回0,执行失败则返回错误号,打印错误信息我们可以通过错误号去进一步寻找错误发生的具体原因,由于错误号各个线程是相互独立的,所以根据错误号检索出来的错误信息就跟线程对应上了。具体做法如下:
使用函数:

 #include <string.h>
 char *strerror(int errnum);

参数说明:

  • errnum:错误号

函数返回值

  • char* :错误号对应错误信息

单个线程退出

 #include <pthread.h>
 void pthread_exit(void *retval);
  • 参数说明
    • retval: void *类型,这个参数就是线程结束时向外传递的信息,例如进程结束exit(0),参数0其实就是进程退出时的返回值,我们使用waitpid函数时,如果设置了进程退出状态的参数,这个参数打印出来就对应exit传入的数值,retval这个参数同理,可以让别的线程获得自己结束时的相关信息,需要说明的是,这个参数是指针类型,不能使用栈指针,因为线程结束对应栈信息被回收,所以这个参数必须指向全局变量或者堆
  • 返回值

注意:
线程退出一定不要用exitexit针对进程,如果在线程调用,那么在原来进程空间里所有的线程将都会被干掉,杀伤范围太大。

扫描二维码关注公众号,回复: 1789323 查看本文章
#include<pthread.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
void *func(void *arg)
{
    int i;
    for(i=0;i<5;i++)
    {
        printf("son i is %d\n",i);
        sleep(1);
    }
}

int main(int argc,char *argv[])
{
    pthread_t pid;
    pthread_create(&pid,NULL,func,NULL);
    pthread_exit(NULL);
    for(int i=0;i<5;i++)
    {
        printf("father:i is %d\n",i);
        sleep(1);
    }
    return 0;
}

程序运行结果表明,父线程打印部分没有执行,证明父线程提前结束,福线程退出没有造成子线程强制退出,这是因为父线程退出行为不是由return或者exit造成,杀伤范围没有那么大,。没有造成子线程的退出

root@thinkpad:/softcode/linux/pthread# ./mypthread 
son i is 0
son i is 1
son i is 2
son i is 3
son i is 4

阻塞等待线程退出函数

 #include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

函数参数:

  • thread:线程号
  • retval:线程退出时的传出参数,这个参数在这里是个传出参数,二级指针,指向内存和pthread_exit函数指向内存是一致的
    一般用法如下:
void *ptr;
pthread_join(thread,&ptr);

小例程

#include<pthread.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int num=100;
void *func(void *arg)
{
    int i;
    for(i=0;i<5;i++)
    {
        if(i==2)
            pthread_exit(&num);
        printf("son i is %d\n",i);
        sleep(1);
    }
}

int main(int argc,char *argv[])
{
    pthread_t pid;
    pthread_create(&pid,NULL,func,NULL);
    void *ptr;
    pthread_join(pid,&ptr);
    printf("ptr is %d\n",*(int *)ptr);
    for(int i=0;i<5;i++)
    {
        printf("father:i is %d\n",i);
        sleep(1);
    }
    return 0;
}

程序运行结果显示父线程等待子线程退出后,在执行父线程以后的代码,说明这个函数是阻塞性质的,而且获得了指定线程的退出时对外传递的相关信息

root@thinkpad:/softcode/linux/pthread# ./mypthread 
son i is 0
son i is 1
ptr is 100
father:i is 0
father:i is 1
father:i is 2
father:i is 3
father:i is 4

线程分离

#include <pthread.h>
int pthread_detach(pthread_t thread);

作用:可以指定线程结束时自己回收自己的资源,不用再使用pthread_join函数
参数:

  • 线程号

返回值:

  • 成功返回0
  • 失败返回错误号

在创建线程就设置分离属性

在极端情况下有时候创建了线程,还没为其设置分离属性,线程就退出了,所以我们可以在线程创建时就可以其创建分离属性,具体方法如下:
前面在线程创建时属性参数设置为NULL,这里我们可以利用这个属性为线程设置分离属性

  • 创建线程属性变量:pthread_attr_t attr
  • 对线程属性变量进行初始化
    int pthread_attr_init(pthread_attr_t *attr)
  • 设置线程分离属性
    int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate)
    参数:
    attr :线程属性变量
    detachstate:
    PTHREAD_CREATE_DETACHED(分离)
    PTHREAD_CREAT_JOINABLE(非分离)
  • 释放线程属性占用资源函数
    int pthread_attr_destroy(pthread_attr_t *attr)

杀死线程

#include <pthread.h>
int pthread_cancel(pthread_t thread);

作用:杀死指定线程
参数:

  • 线程号

返回值:

  • 成功返回0
  • 失败返回错误号

注意:
被指定要杀死的线程必须执行一次系统调用才能被杀死,如果线程内部处理函数没有系统调用函数,该线程不能被杀死,如果必须要结束该线程,可调用pthread_testcancel()函数,该函数无实际意义,但是执行的是系统调用,可以作为线程线程结束点。

#include<pthread.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int num=100;
void *func(void *arg)
{
    int i;
    for(i=0;i<5;i++)
    {
        printf("son i is %d\n",i);
        sleep(1);
    }
}

int main(int argc,char *argv[])
{
    pthread_t pid;
    pthread_create(&pid,NULL,func,NULL);
    void *ptr;
    sleep(2);
    pthread_cancel(pid);
   // pthread_join(pid,&ptr);
    //printf("ptr is %d\n",*(int *)ptr);
    for(int i=0;i<5;i++)
    {
        printf("father:i is %d\n",i);
        sleep(1);
    }
    pthread_exit(NULL);
}

子线程执行两次就被父线程杀死,因为子线程存在printf函数,执行时调用内核sprintf,所以存在线程终止点,子线程被顺利杀死

root@thinkpad:/softcode/linux/pthread# ./mypthread 
son i is 0
son i is 1
father:i is 0
father:i is 1
father:i is 2
father:i is 3
father:i is 4

线程同步

为了说明线程同步的意义,我们先看一个小例程,该例程无实际意义,只是为了放大要说明的问题

#include<pthread.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#define MAX 10000
int num=0;
void *func1(void *arg)
{
    int i;
    for(i=0;i<MAX;i++)
    {
        int cur;
        cur=num;
        cur++;
        usleep(10);
        num=cur;
        printf("ID is %ld,func1:num is %d\n",pthread_self(),num);
    }
    return NULL;
}
void *func2(void *arg)
{
    int i;
    for(i=0;i<MAX;i++)
    {
        int cur;
        cur=num;
        cur++;
        usleep(10);
        num=cur;
        printf("ID is %ld,func2:num is %d\n",pthread_self(),num);

    }
    return NULL;
}


int main(int argc,char *argv[])
{
    pthread_t pid1,pid2;
    pthread_create(&pid1,NULL,func1,NULL);
    pthread_create(&pid2,NULL,func2,NULL);

    pthread_join(pid1,NULL);
    pthread_join(pid2,NULL);
    return 0;
}

上面的例程中,每个线程都对全局变量num自加一万次,因此理论上num值最终应该是20000,但是程序运行结果却不是这样,下面是运行结果:

ID is 140548474279680,func2:num is 10001
ID is 140548474279680,func2:num is 10002
ID is 140548482672384,func1:num is 10002
ID is 140548482672384,func1:num is 10003
ID is 140548474279680,func2:num is 10003
ID is 140548474279680,func2:num is 10004
ID is 140548482672384,func1:num is 10004
ID is 140548482672384,func1:num is 10005
ID is 140548474279680,func2:num is 10005
ID is 140548482672384,func1:num is 10006
ID is 140548474279680,func2:num is 10006
ID is 140548482672384,func1:num is 10007
ID is 140548474279680,func2:num is 10007
ID is 140548482672384,func1:num is 10008
ID is 140548482672384,func1:num is 10009
root@thinkpad:/softcode/linux/pthread# 

这是因为,当一个线程运行时,虽然for循环中i的值已经执行过了,但在for循环体中执行相关操作时CPU被抢夺了,赋值动作未完成,另一个线程取得的num值并不是经过增加num值,虽然i的值有序增加,但num因各种阴差阳错并没有真正累加到自身。造成了数据最后偏小的缘故,这样就出现了一个很现实的问题,就是对共享资源的保护问题,这就需要线程执行时要考虑同步问题

线程同步思想

锁:多个线程对同一资源进行访问时,为确保每个线程对资源操作有效性,线程拿到资源时对资源加锁,其他线程不能对其进行操作,对资源操作结束后,开锁其他线程可以利用,使线程并发执行变成有序的顺序执行,使步调协调。


互斥锁

  • 互斥锁的类型
    创建一把锁:pthread_mutex_t mutex;
  • 互斥锁的特点
    • 多个线程访问共享数据的时候是串行的
  • 使用互斥锁缺点?
    • 效率低
  • 互斥锁使用步骤:
    • 创建互斥锁
    • 初始化互斥锁
    • 在线程访问共享数据处加锁
    • 线程访问共享数据结束后解锁
    • 所有线程使用完成后记得销毁锁
  • 互斥锁相关函数

    • 初始化互斥锁
    pthread_mutex_init(pthread_mutex_t *restrict mutex,
                        consst pthread_mutexattr_t *restrict attr)
    • 销毁互斥锁
    pthread_mutex_destroy(pthread_mutex_t *restrict mutex)
    • 加锁
    pthread_mutex_lock(pthread_mutex_t *restrict mutex);
    
      • mutex
        没有被上锁,当前线程会把这把锁锁上
        被锁上了:当前线程阻塞
        锁打开后线程解除阻塞
    • 尝试加锁
    pthread_mutex_trylock(pthread_mutex_t *restrict mutex);
    
      • 没有锁,当前线程会给这把锁加锁,返回0
        锁上了,线程不会阻塞,返回错误号
    • 解锁
      pthread_mutex_unlock(pthread_mutex_t *restrict mutex);

死锁

死锁就是线程自己锁定自己不能向下执行,造成死锁的原因:

  • 自己锁自己
    自己锁了自己两次,线程在第二次上锁位置卡死

  • 线程有两把锁
    有两把锁A1A2,线程1上了A1锁,线程2上了A2锁,线程1想访问A2锁定资源,线程2想访问A1锁定资源,造成互相阻塞

  • 解决死锁

    • 让线程按照一定的顺序访问共享资源
    • 在访问其他锁的时候,需要先将自己的锁解开
    • 使用trylock函数

读写锁

  • 创建一把锁
    pthread_rwlock_t rwlock;
  • 读写锁的特点
    • 读锁:对内存做读操作
    • 写锁:对内存做写操作
  • 读写锁的特性:
    • 线程A加读锁成功,又来了三个线程,做读操作,可以加锁成功
      • 读共享 -并行处理
    • 线程A加写锁成功,又来了三个线程,做读操作,三个线程阻塞
      • 写独占
    • 线程A加读锁成功,又来了B线程加写锁阻塞,又来了C线程加读锁阻塞
      • 读写不能同时
      • 写的优先级高
  • 读写锁的场景练习
    • 线程A加写锁成功,线程B请求读锁
      • 线程B阻塞
    • 线程A持有读锁,线程B请求写锁
      • 线程B阻塞
    • 线程A拥有毒素哦,线程B请求读锁
      • 线程B加锁成功
    • 线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁
      • B阻塞 C阻塞-写的优先级高
      • A解锁,B线程加锁成功,C继续阻塞
      • B解锁,C加读锁成功
    • 线程A持有写锁,然后线程B请求读锁,然后线程C请求写锁
      • 线程B阻塞,线程C阻塞
      • 线程A解锁,线程C加锁成功,线程B阻塞
      • 线程C解锁,线程B加锁成功
  • 读写锁适用场景
    • 互斥锁 - 读写串行
    • 读写锁:
      • 读:并行
      • 写:串行
    • 程序中读操作大于写操作的情况
  • 主要操作函数
    • 读写锁初始化函数
      pthread_rwlock_init(pthread_rwlock_t *restrick rwlock,const pthread_relockattr_t *restrick attr);
    • 销毁读写锁
      pthread_rwlock_destroy(pthread_rwlock_t * rwlock);
    • 加读锁
      pthread_rwlock_rdlock(pthread_rwlock_t * rwlock);
    • 尝试加读锁
      pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);
    • 加写锁
      pthread_rwlock_wrlock(pthread_rwlock_t * rwlock);
    • 尝试加写锁
      pthread_rwlock_trywrlock(pthread_rwlock_t * rwlock);
    • 解锁
      pthread_rwlock_unlock(pthread_rwlock_t * rwlock);

条件变量

  • 条件变量
    • 不是锁
    • 线程同步需要 条件变量+互斥锁
      • 互斥锁:保护一块共享数据
      • 条件变量:引起阻塞
        • 生产者和消费者模型
  • 粗糙的例子说明
    生产者生产烧饼,消费者购买烧饼,条件变量就是有无烧饼,有烧饼消费者可以买(线程不阻塞),没烧饼消费者等待(线程阻塞),但是烧饼是公共的大家都可以买(为避免冲突烧饼有人买了其他人不能再买(加互斥锁保护))
    - 条件变量的两个动作
    • 条件不满足,阻塞线程
    • 当条件满足,通知阻塞线程开始工作
  • 条件变量类型:pthread_cond_t;
  • 主要函数
    • 初始化一个变量
      pthread_cond_init(pthread_cond_t *restric cond,const pthread_condattr_t*restric attr);
    • 销毁一个变量
      pthread_cond_destroy(pthread_cond_t *cond);
    • 阻塞等待一个条件变量注意第二个参数为互斥锁类型
      pthread_cond_wait(pthread_cond_t *restric cond,pthread_mutex_t*restric mutex);

      • 阻塞线程
      • 将已上锁的mutex解锁
    • 唤醒至少一个阻塞在条件变量上的线程
      pthread_cond_signal(pthread_cond_t *cond);
    • 唤醒全部阻塞在条件变量上的线程
      pthread_cond_broadcast(pthread_cond_t *cond);

信号量(信号灯)

  • 头文件semaphore.h
  • 粗糙理解
    可以理解成一个车库,车库有N个停车位(信号量个数),当有人占了一个车位就会加锁(信号量减一),当车从车位离开(信号量加一),当车库没有停车位即信号量为0,在想占有停车位资源只能等待(阻塞)
  • 信号量类型
    • sem_t sem
    • 加强版互斥锁
  • 主要函数
    • 初始化信号量
      sem_init(sem_t *sem,int pshared,unsigned int value);

      • sem:信号量
      • pshared0:线程同步 1:进程同步
      • value:最多有几个线程操作共享数据
  • 销毁信号量
    sem_destroy(sem_t *sem);
  • 加锁
    sem_wait(sem_t *sem);
    调用一次相当于对于sem进行了一次减减操作,如果sem值为0,线程将会阻塞
  • 限时尝试加锁
    sem_timedwait(sem_t*sem,xxxxxx);
  • 尝试加锁
    sem_trywait(sem_t*sem)
  • 解锁
    sem_post(sem_t*sem)
    sem进行加加操作

—·

猜你喜欢

转载自blog.csdn.net/z961968549/article/details/80624011