LInux下C++多线程编程总结

本文深入探讨了C++在Linux环境下多线程编程的精髓,包括NPTL库的优势、线程模型、线程创建与管理、POSIX信号量、互斥锁和条件变量的使用,以及如何避免死锁,是理解和掌握多线程编程不可或缺的指南。

C++多线程与多进程编程

  1. Linux下的多线程库:NGPT和NPTL,NPTL比linuxThread效率更高,且符合POSIX编程规范,因此通常都是用POSIX下的线程库:pthread标准

    NPTL的实现包括三个内容,创建线程和结束线程;读取和设置线程属性;POSIX线程同步的方式:POSIX 信号量,互斥条件和条件变量。

  2. 线程模型

    线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体。根据调度者的身份,线程可以分为用户线程和内核线程两种

    • 内核线程:运行在内核空间,由内核来调度

    • 用户线程:运行在用户空间,由线程库来调度。

    当进程的一个内核线程获得CPU的使用权时,他就加载并运行了一个用户线程,因此,内核线程相当于用户线程的运行“容器”,一个进程可以拥有M个内核线程和N个用户线程,M<=N。

    根据这个比值,有三种情况:完全在用户空间实现多线程;完全在内核空间实现多线程;双层调度。

    完全在用户空间实现的线程无需内核的支持,内核甚至不知道这些线程的存在。线程库负责管理所有的线程,使用longjmp来切换线程的执行,使他们看起来像是“并发”执行的,但实际上内核仍然是把整个进程作为最小单位来调度的,所以内核线程就是进程本身。

    完全在内核空间实现的线程将创建,调度的任务都交给了内核,线程库无需执行管理任务。

  3. Linux下的线程库最有名的就是LInuxThreads和NPTL,现在默认是NPTL。LinuxThreads线程库的内核线程是使用clone系统调用创建进程模拟的,clone系统调用和fork系统调用过程类似,不过可以指定创建的子进程与调用进程共享相同的虚拟地址空间。用进程来模拟线程有许多缺点:

    1. 每个线程拥有不同的PID

    2. LInux信号处理函数是基于进程的,现在一个进程内所有线程都能且必须处理信号。

    3. 系统的最大线程数就是最大进程数

    LinuxThreads还提供管理线程,负责接收系统发送的信号,终止线程,阻塞线程和回收线程堆栈等工作,效率低下。

    NPTL库在linux内核的完善下应运而生,Linux内核提供了真正的内核线程,NPTL有如下优点:

    1. 内核线程不再是一个进程,进程的线程可以运行在不同的CPU上,由内核调度。

    2. 不存在管理线程,回收终止等工作都由内核完成

    3. 线程的同步由内核完成,隶属于不同进程的线程之间也能共享互斥锁,实现跨进程的同步。

  4. 线程创建

    #include <pthread.h>
    int pthread_create(pthread_t *thread,const pthread_attr_t *attr,vid *(*start_routine)(void* ),void *arg);

    pthread_t为一个无符号长整型,为线程的编号;arg参数用于设置线程的属性,NULL为默认。

    该函数成功时返回0,失败返回错误码。

  5. 线程调度与停止

    线程一旦创建好,内核就可以调度内核线程来执行start_routine函数指向的函数了。线程退出最好使用pthread_exit,确保安全,干净的退出。

    #include <pthread.h>
    void pthread_exit(void *retval);//retval向回收者传递退出信息。
  6. 线程的回收

    一个进程中的所有线程都可以通过调用pthread_join函数来回收其他线程,即等待其他线程结束,这类似于回收进程的wait和waitpid系统调用。

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

    thread参数是目标的标识符,成功返回0,失败返回错误码:

    EDEADLK:可能引起死锁,比如两个线程互相对对方调用pthread_join,或者线程对自身调用pthread_join
    EINVAL:目标线程是不可回收的,或者已经有其他线程在回收该线程
    ESRCH:目标线程不存在
  7. 线程的终止与取消

    一个线程可能出现异常,我们希望手动终止他,可以通过如下函数:

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

    成功返回0,失败返回错误码。

    不过,接受到取消请求的目标线程可以决定是否允许取消以及如何取消,

    #include <pthread.h>
    int pthread_setcancelstate(int state,int *oldstate);
    int pthread_setcanceltype(int type,int *oldtype);

    这两个函数的第一参数分别设置线程的取消状态和取消类型;第二个参数则分别记录线程原来的取消状态和取消类型。更多细节请看API

  8. 线程的属性

    #include <pthread.h>
    #define __SIZEOF_PTHREAD_ATTR_T 36
    typedef union{
        char __size[__SIZEOF_PTHREAD_ATTR_T];
        long int __align;
    }pthread_attr_t

    各种线程属性全部包含在这个字符数组中,可以通过一系列API来操作pthread_attr_t,以便获取和设置线程的属性:

    1.pthread_attr_init 功能: 对线程属性变量的初始化。

    2.pthread_attr_setscope 功能: 设置线程 __scope 属性...

    3.pthread_attr_setdetachstate 功能: 设置线程detach...

    4.pthread_attr_setschedparam 功能: 设置线程sched...

    5.pthread_attr_getschedparam 功能: 得到线程优先级。

    线程的属性如下:

    ​
     typedef struct
    {
        int detachstate; //线程的分离状态
        int schedpolicy; //线程的调度策略
        struct sched schedparam;//线程的调度参数
        int inheritsched; //线程的继承性
        int scope; //线程的作用域
        size_t guardsize; //线程栈末尾的警戒缓冲区大小
        int stackaddr_set; //线程栈的设置
        void* stackaddr; //线程栈的启始位置
        size_t stacksize; //线程栈大小
    }pthread_attr_t;

    都有相应的函数进行设置。

  9. POSIX信号量:处理多线程的同步问题

    3种处理先吃之间同步问题的机制:POSIX信号量,互斥量和条件变量

    LInux中,信号量有两种,第一种是进程间通信使用的 System V IPC,另一种就是POSIX信号量:

    #include <semaphore.h>
    //初始化信号量
    int sem_init(sem_t *sem, int pshared,unsigned value);
    //销毁信号量
    int sem_destroy(sem_t *sem);
    //以原子操作的方式将信号量的值+1
    int sem_post(sem_t *sem);
    //以原子操作的方式将信号量的值-1
    int sem_wait(sem_t *sem);
    //与sem_wait相同,不过始终立即返回,如果未成功返回-1并将errno置为EAGAIN
    int sem_trywait(sem_t *sem);
    //关闭命名信号量
    int sem_close(sem_t *sem);
    //命名一个信号量
    sem_t *sem_open(const char *name, int flag);
    
    

    信号量的使用:(重点)

    1.建立两个线程,两个线程分别将自己的整形数i从1递增到100,并规定两个线程的整形不能超过5

    #include <bits/stdc++.h>
    #include <pthread.h>
    #include <semaphore.h>
    #include <unistd.h>//提供对POSIX操作系统API的访问权限的头文件
    #include <windows.h>
    
    //using namespace std;
    const int INF=100;
    sem_t sem_1,sem_2;//两个信号量
    using std::cout;
    using std::endl;
    /*
        思路:因为最大差值不能超过5,先初始化两个信号量为5,如果一个数+1,在wait自己sem的
        同时post另一个sem,表示你可以多加一点,因为差值可以从[-5,5],所以必须post另一个sem才满足条件
    */
    void* run1(void *arg){
        int i;//变量
        for(i=0;i<=INF;i++){
            sem_wait(&sem_1);//原子操作-1
            usleep(500*1000);
            cout<<"the thread 1 print "<<i<<endl;
            sem_post(&sem_2);//原子操作+1
        }
        pthread_exit(0);
    }
    void* run2(void *arg){
        int i;
        for(i=0;i<=INF;i++){
            sem_wait(&sem_2);
            usleep(500*1000);
            cout<<"the thread 2 print "<<i<<endl;
            sem_post(&sem_1);
        }
        pthread_exit(0);
    }
    
    signed main(){
        sem_init(&sem_1,0,5);
        sem_init(&sem_2,0,5);
        pthread_t tid1=5000,tid2=10000;
        pthread_create(&tid1,NULL,run1,NULL);
        pthread_create(&tid2,NULL,run2,NULL);
        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);
        sem_destroy(&sem_1);
        sem_destroy(&sem_2);
        return 0;
    }
    
    

    2.经典线程问题:生产者消费者问题

    #include <bits/stdc++.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <windows.h>
    #include <semaphore.h>
    
    const int maxn=101;
    struct Stack{
        int a[maxn];
        int top;
        void init(){
            memset(a,0,sizeof(a));
            top=0;
        }
        void push(int num){
            a[top++]=num;
        }
        int pop(){
            return a[top--];
        }
    }Q;
    
    sem_t Full,Empty;
    void* push(void *arg){
        while(true){
            sem_wait(&Full);
            int num=rand()%100000;
            printf("producter has produce an integer %d,now the top is %d\n",num,Q.top);
            Q.push(num);
            usleep(500*1000);
            sem_post(&Empty);
        }
        pthread_exit(0);
    }
    void *pop(void *arg){
        while(true){
            sem_wait(&Empty);
            printf("comsumer has comsume an integer %d,now the top is %d\n",Q.pop(),Q.top);
            usleep(500*1000);
            sem_post(&Full);
        }
        pthread_exit(0);
    }
    signed main(){
        Q.init();
        sem_init(&Full,0,100);
        sem_init(&Empty,0,0);
        pthread_t tid1,tid2;
        pthread_create(&tid1,NULL,push,NULL);
        pthread_create(&tid2,NULL,pop,NULL);
        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);
        sem_destroy(&Full);
        sem_destroy(&Empty);
        return 0;
    }
    
    
  10. 互斥锁

    互斥锁可以用于保护关键代码段,这有点向二进制的POSIX信号量

    互斥锁具有以下特点:

    `·原子性:把一个互斥锁定义为一个原子操作,这意味着操作系统保证了如果一个线程锁定了互斥锁,则没有其他线程可以在同一时间成功锁定这个互斥量。`

    `·唯一性:如果一个线程锁定一个互斥量,在它接触锁定之前,没有其他线程可以锁定这个互斥量。`

    `·非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用CPU资源),直到第一个线程解锁,第二个线程则被唤醒并继续执行,同时锁定这个互斥量`

    互斥锁的使用:

    1. 在访问共享资源后临界区域前,对互斥锁进行加锁;

    2. 在访问完成后释放互斥锁导上的锁。在访问完成后释放互斥锁导上的锁;

    3. 对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

 

```c++
// 初始化一个互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex, 
						const pthread_mutexattr_t *attr);

// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,
// 直到互斥锁解锁后再上锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 调用该函数时,若互斥锁未加锁,则上锁,返回 0;
// 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
int pthread_mutex_trylock(pthread_mutex_t *mutex);

// 当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量
// 原语允许绑定线程阻塞时间。即非阻塞加锁互斥量。
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);

// 对指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 销毁指定的一个互斥锁。互斥锁在使用完毕后,
// 必须要对互斥锁进行销毁,以释放资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
```

还有一个常用的方法就是:将互斥锁封装成对象,构成自动锁,这样生命周期结束的时候锁自动销毁

```c++
class CAutoMutex
{
public:
    CAutoMutex()
    {
        mutex = PTHREAD_MUTEX_INITIALIZER;
        pthread_mutex_lock(&mutex);
    }
    ~CAutoMutex()
    {
        pthread_mutex_unlock(&mutex);
    }
private:
    pthread_mutex_t mutex;
};
```

死锁产生:对已经加锁的普通锁再次加锁,会构成死锁;两个对象按照不同的顺序申请互斥锁

```
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <windows.h>
#include <bits/stdc++.h>

pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
int a,b;
const int INF=100;
void* run(void *arg){
    //锁b
    int x=0;
    while(x<=INF){
        pthread_mutex_lock(&mutex_b);
        printf("the chile thread got mutex b,waiting fo mutex a\n");
        usleep(500*1000);
        b++;
        pthread_mutex_lock(&mutex_a);
        a+=b++;
        pthread_mutex_unlock(&mutex_a);
        pthread_mutex_unlock(&mutex_b);
    }
    pthread_exit(NULL);
}
signed main(){
    pthread_mutex_init(&mutex_a,NULL);
    pthread_mutex_init(&mutex_b,NULL);
    pthread_t tid;
    pthread_create(&tid,NULL,run,NULL);
    //向a锁加锁
    int x=0;
    while(x<=INF){
    pthread_mutex_lock(&mutex_a);
    printf("the father thread got mutex a,waiting for mutext b\n");
    a++;
    pthread_mutex_lock(&mutex_b);
    b+=a++;
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
    }
    pthread_join(tid,NULL);
    pthread_mutex_destroy(&mutex_a);
    pthread_mutex_destroy(&mutex_b);
    return 0;
}

```
  1. 条件变量

    与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直 到某特殊情况发生为止。通常条件变量和互斥锁同时使用

    条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步 的一种机制,主要包括两个动作:

    • 一个线程等待"条件变量的条件成立"而挂起;

    • 另一个线程使 “条件成立”(给出条件成立信号)。

    更加清晰的说法就是,条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的 时候,唤醒等到这个共享数据的线程。

    #include <pthread.h>
    // 初始化条件变量
    int pthread_cond_init(pthread_cond_t *cond,
    						pthread_condattr_t *cond_attr);
    
    // 阻塞等待
    int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
    
    // 超时等待
    int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,
    						const timespec *abstime);
    
    // 解除所有线程的阻塞
    int pthread_cond_destroy(pthread_cond_t *cond);
    
    // 至少唤醒一个等待该条件的线程
    int pthread_cond_signal(pthread_cond_t *cond);
    
    // 唤醒等待该条件的所有线程
    int pthread_cond_broadcast(pthread_cond_t *cond); 
    
  2.  

     

     

     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值