Linux_线程控制

POSIX线程库

  • 这个库是Linux的原生线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都以“pthread_”打头的

  • 要使用这些函数库,要通过引入头文

  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项 (例:g++ test.cc -o test -lpthread

创建新线程

函数原型:

#include 

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

此函数将创建一个新的线程,并返回线程ID

  • pthread_t *thread:返回线程id(输出型参数,一个无符号整型)
  • const pthread_attr_t *attr:设置线程属性,NULL标识使用默认属性
  • void *(*start_routine) (void *):线程执行时的回调方法
  • void *arg:线程创建成功后,该参数会赋给回调函数的那个参数
  • 返回值:成功返回0,失败返回错误码

使用案例:

#include 
#include 
#include 
using namespace std;

void *startRoutine(void *args)
{
    char *name = (char *)args;
    while (true)
    {
        cout << name << " 正在运行" << endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");
    while (true)
    {
        cout << "主线程正在运行,"
             << "我创建的新线程ID是:" << tid << endl;
        sleep(1);
    }
}

当主线程执行到pthread_creat(),就会创建新线程,新线程会跳转到startRoutine函数开始执行,主线程得到pthread_creat()的返回值后继续向下执行

也就是我们把进程的代码划分为两部分,一部分给主线程执行,一部分给新线程执行

g++ -o mythread mythread.cpp -lpthread

./mythread

此时,就有两个线程在运行

Linux_线程控制_第1张图片

-L:表示查看轻量级进程

LWP:轻量级进程ID

可以看到,两个线程的PID相同都是23266,LWP不同

线程空间的理解

获取线程tid和LWP

从上面的案例可以看出几个线程之间可以用tid区分,也可以用LWP区分,在讲解它们的区别之前,我们先看看如何在代码中获取自己的tid和LWP

获取自己的线程tid

#include 

pthread_t pthread_self(void);
  • 返回值:当前线程的线程ID

使用案例:

#include 
#include 
#include 
using namespace std;
int main()
{
        cout << "主线程tid:" << (void*)pthread_self() << endl;
}

获取当前线程的LWP

我们可以通过syscall()获得当前线程的LWP

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include 
#include    /* For SYS_xxx definitions */

int syscall(int number, ...);

使用:

#define _GNU_SOURCE
#include 
#include 
using namespace std;
void *startRoutine(void *args)
{
    cout << "新线程PID:" << getpid() << endl;
    cout << "新线程LWP:" << syscall(SYS_gettid) << endl;
    return nullptr;
}

int main()
{
    cout << "主线程PID:" << getpid() << endl;
    cout << "主线程LWP:" << syscall(SYS_gettid) << endl;
    pthread_t tid;
    pthread_create(&tid, nullptr, startRoutine, nullptr);
    sleep(1);
}
Linux_线程控制_第2张图片

可以看到,主线程的LWP就是PID,新线程的LWP是此值+1;

理解pthread_t

从上面的案例我们可以看到pthread_create返回的tid和线程的LWP并不相同

那么它们分别是什么呢?

事实上Linux操作系统只提供了轻量级进程的概念,LWP就是轻量级进程的ID

通过clone这样的系统调用接口可以在进程中创建一个轻量级进程

#define _GNU_SOURCE
       #include 

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, 
          .../* pid_t *ptid, void *newtls, pid_t *ctid */ );

但我们用户需要的是一个线程的概念

所以,pthread这样的库应运而生,这个库向上为用户提供线程的概念,pthread_create、pthread_self、pthread_exit……这样的接口;向下在内部调用clone……这些系统调用接口

在Linux中,用户级线程库和内核LWP是1 : 1

同时,这个库也被赋予了管理线程的责任

为了描述一个个线程,pthread库定义了struct thread这样的结构体

struct thread
{
    pid_t tid;
    void* stack;//线程私有栈
    ...
};

每创建一个新的线程,pthread库就会创建一个这样的结构体变量

pthread_create()获得的pthread_tpthread_self()返回的,就是一个这个结构体变量的首地址

Linux_线程控制_第3张图片

除了创建这个线程结构体,还会开辟这个线程的局部存储和私有栈空间

线程的私有栈空间

当创建一个新的线程,线程中新建变量也一定要有它们的存储位置

假如是在堆或者全局数据区创建变量,依然可以和主线程公用同一块空间;

但是在栈中的数据如果还要严格遵守进栈出栈原则,那绝对不能让几个不同的线程共享同一个栈,所以每创建一个新的线程,就要为这个线程提供独立的栈空间,而新线程栈空间的起始地址就保存在了struct prhread中。

线程局部存储

如果直接创建一个全局变量,那么所有的线程将共享这个全局变量

如下代码创建了三个新线程,让它们对一个全局变量进行++操作

#include 
#include 
#include 

int globalVal = 0;
void *startRoutine(void *args)
{
    char *name = (char *)args;
    int cnt = 3;
    while (cnt--)
    {
        cout << name << "[" << pthread_self() << "], globalVal["
             << &globalVal << "]: " << globalVal++ << endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1, nullptr, startRoutine, (void *)"thread1");
    pthread_create(&tid2, nullptr, startRoutine, (void *)"thread2");
    pthread_create(&tid3, nullptr, startRoutine, (void *)"thread3");
}
Linux_线程控制_第4张图片

可以看到,不同的线程访问的全局变量是同一个地址,++操作都会累计上去

此时我们在全局变量前加上__thread关键字

#include 
#include 
#include 

__thread int globalVal = 0;
void *startRoutine(void *args)
{
    ...
Linux_线程控制_第5张图片

可以看到,每个线程的globalVal有着不同的地址,也就是说每个线程都有一个自己的globalVal,也只会对自己的globalVal进行累加计数

线程的退出

pthread_join

当我们创建一个子进程,父进程必须waitpid()等待子进程退出

同样的,当我们创建了一个新线程,主线程也必须“等待”,接收新线程的返回值,回收它的资源

注意:线程函数执行结束退出后,ps -aL将检测不到新线程,但是新线程的资源仍然是没有释放的,所以主线程一定要主动进行join,否则会造成内存泄露

#include 

int pthread_join(pthread_t thread, void **retval);
  • thread:等待的线程的ID

  • retval:获取线程的退出码(输出型参数)

  • 返回值:成功返回0,失败返回错误码

线程的退出方法

线程退出共有3种方式:

1. return

在新线程对应函数中使用return可以直接返回void*的退出码

使用案例:

void *startRoutine(void *args)
{
    char *name = (char *)args;
    int cnt = 5;
    while (cnt--)
    {
        cout << name << "[" << pthread_self() << "] 正在运行" << endl;
        sleep(1);
    }
    cout << "新线程退出了..." << endl;
    return (void *)666;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");
    void *ret;
    pthread_join(tid, &ret);
    cout << "join success, retval: " << (long long)ret << endl;
    while (true)
    {
        cout << "主线程[" << pthread_self() << "]正在运行," << endl;
        sleep(1);
    }
}
Linux_线程控制_第6张图片

2. pthread_exit()

在新线程中使用pthread_exit函数可以退出

#include 

void pthread_exit(void *retval);

同样也是传入一个void*的退出码

注意:不可使用exit(),任何一个线程调用exit都表示整个进程退出

使用案例:

void *startRoutine(void *args)
{
    char *name = (char *)args;
    int cnt = 5;
    while (cnt--)
    {
        cout << name << "[" << pthread_self() << "] 正在运行" << endl;
        sleep(1);
    }
    cout << "新线程退出了..." << endl;
    pthread_exit((void *)666);
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");
    void *ret;
    pthread_join(tid, &ret);
    cout << "join success, retval: " << (long long)ret << endl;
    while (true)
    {
        cout << "主线程[" << pthread_self() << "]正在运行," << endl;
        sleep(1);
    }
}
Linux_线程控制_第7张图片

3. pthread_cancel()

在主线程中使用pthread_cancel函数可以让指定tid的线程退出

#include 

int pthread_cancel(pthread_t thread);

用此方式退出的线程退出码默认是-1,对应一个宏:PTHREAD_CANCELED

void *startRoutine(void *args)
{
    char *name = (char *)args;
    int cnt = 5;
    while (cnt--)
    {
        cout << name << "[" << pthread_self() << "] 正在运行" << endl;
        sleep(1);
    }
    cout << "新线程退出了..." << endl;
    return (void *)666;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");
    sleep(3); // 代表主线程运行了3秒
    pthread_cancel(tid);
    void *ret;
    pthread_join(tid, &ret);
    cout << "join success, retval: " << (long long)ret << endl;
    while (true)
    {
        cout << "主线程[" << pthread_self() << "]正在运行," << endl;
        sleep(1);
    }
}
Linux_线程控制_第8张图片

线程异常

当一个子进程退出后,父进程可以通过waitpid获取子进程退出时的信号和退出码,父进程依然可以正常运行

但当一个新线程发生异常,主线程还能正常运行吗?

我们运行如下代码

void *startRoutine(void *args)
{
    char *name = (char *)args;
    int cnt = 3;
    while (cnt--)
    {
        cout << name << "[" << pthread_self() << "] 正在运行" << endl;
        sleep(1);
    }
    // 引发段错误
    int *a = nullptr;
    *a = 10;
    return (void *)666;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");
    while (true)
    {
        cout << "主线程[" << pthread_self() << "]正在运行," << endl;
        sleep(1);
    }
}
Linux_线程控制_第9张图片

可以看到,当新线程发生了段错误,主线程也同时退出了,也就是说整个进程整体异常退出了

同一个进程中的任意执行流发生异常,进程就会退出

所以说多线程的方式相对于多进程,代码健壮性、鲁棒性是更低的

同时我们也可以理解,pthread_join是不需要获取线程退出信号的,因为线程的异常就是进程的异常,主线程获取信号也没有意义

线程分离

我们在主线程join新线程是要做两件事:

  • 获取新线程的退出码
  • 回收新进程的资源

在新线程退出之前,主线程会一直阻塞在join的地方

如果我们不关心线程的返回值,那join就成为了一种负担

这个时候,我们可以告诉系统,当线程退出的时候自动释放资源

#include 

int pthread_detach(pthread_t thread);

它可以在主线程调用,也可以在新线程pthread_detach(pthread_self());

注意:它会把目标线程从joinable的状态转为分离状态,

一旦处于分离状态,再pthread_join就会join失败并返回错误码

void *startRoutine(void *args)
{
    pthread_detach(pthread_self());
    char *name = (char *)args;
    cout << name << "[" << (void *)pthread_self() << "]" << endl;
    return nullptr;
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1, nullptr, startRoutine, (void *)"thread1");
    pthread_create(&tid2, nullptr, startRoutine, (void *)"thread2");
    pthread_create(&tid3, nullptr, startRoutine, (void *)"thread3");
    
    sleep(1); // 防止新线程还没来的及detach,主线程就完成join,影响实验结果
    
    // 此时再join就会失败
    int n1 = pthread_join(tid1, nullptr);
    int n2 = pthread_join(tid1, nullptr);
    int n3 = pthread_join(tid1, nullptr);
    cout << n1 << ": " << strerror(n1) << endl;
    cout << n2 << ": " << strerror(n2) << endl;
    cout << n3 << ": " << strerror(n3) << endl;
}
Linux_线程控制_第10张图片

可以看到,pthread_join确实返回了错误码22

注意:代码中的sleep(1);必须有,否则无法得出实验结果,因为有可能新线程还没来的及detach,主线程就完成了join

所以,建议在主线程中对新线程进行detack:

void *startRoutine(void *args)
{
    char *name = (char *)args;
    cout << name << "[" << (void *)pthread_self() << "]" << endl;
    return nullptr;
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1, nullptr, startRoutine, (void *)"thread1");
    pthread_create(&tid2, nullptr, startRoutine, (void *)"thread2");
    pthread_create(&tid3, nullptr, startRoutine, (void *)"thread3");
    // 将新线程detach
    pthread_detach(tid1);
    pthread_detach(tid2);
    pthread_detach(tid3);

    // 此时再join就会失败
    int n1 = pthread_join(tid1, nullptr);
    int n2 = pthread_join(tid2, nullptr);
    int n3 = pthread_join(tid3, nullptr);
    cout << n1 << ": " << strerror(n1) << endl;
    cout << n2 << ": " << strerror(n2) << endl;
    cout << n3 << ": " << strerror(n3) << endl;
    sleep(1); // 防止新线程打印之前进程就退出
}
Linux_线程控制_第11张图片

此处sleep的理解:
如果新线程退出,对主线程没有影响;

如果主线程先退出,就代表进程退出,不管新线程是否还在执行,整个进程都将结束

所以,一般我们分离线程,对应的主线程不要退出(常驻内存的进程)

线程分离,也就意味着主线程不管这个线程的死活

所以我们可以把线程分离看作是第4种退出方式

线程的互斥

相关背景概念

  • 临界资源:

    被多个线程执行流所共享的资源叫做临界资源

    在线程局部存储这一部分的案例中,如果不加__thread关键字,那么globalVal就是临界资源,因为3个线程都对其进行了读取和写入

  • 临界区:

    每个线程内部,访问临界资源的代码,叫做临界区

    cout << globalVal;++globalVal;

  • 互斥:

    任何时刻,保证有且之后一个执行流进入临界区,访问临界资源

  • 线程调度:

    系统会为每个线程分配一个时间片

    当线程在CPU上执行了一段时间后,会被系统剥离下来,从而执行运行队列的下一个线程,于此同时在CPU寄存器上的一些临时数据会被保存到当前进程的PCB中,当此线程下一次被调度到时,这些数据又会被同步回CPU寄存器中,让程序继续执行

  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

存在的问题

我们模拟实现一个多线程售票系统,从而理解临界资源和互斥等概念,同时引出后面的加锁方式

#include 
#include 
#include 
#include 
#include 
int ticket = 10000;
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);//代表买票的过程
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

其中ticket代表总的票数

创建4个线程同时进行抢票,每抢到一张票,打印当前的票数,并对ticket进行–

当线程检测到票数减为0,退出线程

但是实际执行的时候,却出现了一些问题:

Linux_线程控制_第12张图片

可以看出,我们的票有10000张,实际卖出的票却有1003张,最后甚至出现了第负数张票

这就是临界资源出现了问题

我们分析一下:

代码中的临界资源就是记录总票数的ticket

临界区有:

  • printf("%s sells ticket:%d\n", id, ticket);(读取临界区)
  • ticket--;(对临界区进行写入)
  • if (ticket > 0)(读取临界区进行逻辑判断)

我们想想这样的场景:

当票数只剩一张时,2号线程被调度到,开始在CPU上执行

if (ticket > 0)判断成功后开始买票任务,在此过程,2号线程的时间片到了,但是还没有执行ticket--操作

当切换为4号线程,ticket还是1,再次进入if代码块,在–前,再次被切换走

1号,3号也是如此

当再执行回2号线程,打印ticket = 1;再将ticket减为0;if判断后此线程退出

切换到4号线程,打印ticket = 0;再将ticket减为-1;if判断后此线程退出

1号,3号也是如此

上面的4号,1号,3号线程买出的最后0、-1、-2张票就是因此而来

除此之外,在同一条代码执行的过程中也会造成异常:

我们知道,CPU执行的基本单元是并非一条C语言代码,而是一条汇编指令

(在一条汇编代码执行结束前当前线程无法被剥离)

如:一个–操作由三条汇编指令完成

  1. load:将变量从内存加载到寄存器中
  2. sub:对寄存器中的值进行-1操作
  3. store:将寄存器中的值存回内存中

假设当前1号线程执行到了ticket--;操作,要将ticket从801减到800

当执行完load操作、sub操作,当前线程的时间片结束了,在剥离过程中,寄存器中的800被存到进程控制块中

其它线程开始进行抢票操作……

当再次回到1号线程时,ticket已经减到了400

但是一旦1号线程开始执行,首先会把进程控制块中的寄存器数据同步到寄存器

800被写入寄存器后,继续执行store操作,也就意味着tciket从400张一下又变回了800张,冥冥之中又多卖了400张票

虽然在整个if代码块中,–操作所占的时间比率极少,发生这样的问题的概率也极小,很难测出实验结果,但是问题依然是存在的

解决方式:互斥锁

为了保证不会出问题,我们必须让临界区的代码互斥,当一个线程在访问临界区的时候,不允许其他线程进行访问,进行阻塞等待,直到那个线程完成临界区资源的访问

pthread库为我们提供的可以执行这样操作的互斥锁mutex

#include 
//定义锁(如果使用此宏初始化,则不需要pthread_mutex_init())
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex,
                       const pthread_mutexattr_t * attr);
//申请锁(阻塞式)
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//释放锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

使用方式:

假设在我们的主线程要创建三个线程,而这三个线程都要访问同一个临界资源,所以我们要使用互斥锁的机制,让三个线程访问临界资源是互斥的

首先,我们要在主线程创建一把锁,供线程的代码使用

  1. 定义一个pthread_mutex_t的全局锁变量(让主线程和新线程的函数都能看到)

    pthread_mutex_t mutex;

  2. 初始化这个锁

    pthread_mutex_init(mutex, nullptr);

然后在新线程代码中,临界区的开始和结束进行申请锁和释放锁

  1. 在临界区开始的地方申请锁

    pthread_mutex_lock(mutex);

  2. 在临界区结束的地方释放锁

pthrad_mutex_unlock(mutex);

最后在新线程退出,这个锁不在需要时,进行释放

  1. 在join三个线程后销毁这个锁

    pthread_mutex_destroy(mutex);

Linux_线程控制_第13张图片

加锁后的抢票代码:

#include 
#include 
#include 
#include 
#include 
int ticket = 1000;
pthread_mutex_t mutex;
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        pthread_mutex_lock(&mutex); // 加锁
        if (ticket > 0)
        {
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex); // 解锁
            usleep(1000); // 代表买票的过程
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}
int main()
{
    // 使用某个锁之前先进行初始化,下面这些线程会用到这个锁
    pthread_mutex_init(&mutex, nullptr);
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    // 使用锁结束,销毁锁
    pthread_mutex_destroy(&mutex);
}
Linux_线程控制_第14张图片

此时便让几个线程互斥,不会再出现上面的问题

互斥锁原理

加锁原理

当1号线程要进入临界区时,调用pthread_mutex_lock(mutex);,表示申请了mutex这个互斥锁,假设此时这个线程被调度器切走;

此时,当2号线程要进入临界区时,也会调用pthread_mutex_lock(mutex);来申请锁资源,但是此时mutex这个锁资源早已被1号线程申请走,所以只能阻塞在这里,直到被调度走

……

当1号线程再次被调度,会接着执行临界区的代码,执行结束后,调用pthread_mutex_unlock(mutex);释放锁;

此时再调用2号线程时,由于1号线程已经释放了锁资源,所以2号线程可以申请锁,从而进入临界区进行执行

凭借这套机制,就可以保证任意两个线程不会同时进入临界区,保证了线程的互斥

mutex底层

此时,又出现了一个问题,既然这个锁是供多个线程使用的,那么它自己也就成为了一个临界资源,会不会有问题呢?我们看一下pthread_mutex_lock()是如何解决的

Linux_线程控制_第15张图片

如上是lock的一种实现方式的汇编代码

一般的计算机CPU都支持一条汇编代码:xchgb(交换内存和寄存器中的数据)

代码翻译:

lock:

  1. 将寄存器al中的值置为0;

  2. 使用xchgb命令,将mutex的值移到al寄存器,内存中的mutex置为0

  3. 如果此时al为1,则继续向下执行临界区代码;

    如果是0,则挂起等待

  4. 当线程被唤醒,返回开头,继续执行lock

unlock:

  1. 将内存中的mutex置为1

加锁过程:

当mutex初始化完成,会被置为1

假设线程1申请锁的时候只执行了第一条代码,将寄存器al置为0,便被调度器调走,此时,线程1并没有申请锁成功

此时线程2开始执行,将内存中的mutex置为0,将寄存器中的值置为1,此时线程2也被切走,但是线程2已经申请锁成功

此时切到线程3,同样,也是将寄存器置为0,但此时从内存中交换过来的却是0,再向下执行分支的时候,便会挂起等待

当再次回到线程1,继续向下执行,情况也会和线程3一样,挂起阻塞

当回到线程2,因为它此时寄存器中的值是1,便会继续向下执行临界区的代码,完成后,执行unlock,将内存中的mutex置为1,唤醒等待锁的线程

继续执行线程3的时候,它被唤醒,回到lock开头再执行一遍lock的代码,此时从内存中交换上来的是1,便表示它申请锁成功了,可以继续向下执行临界区代码

可以看到,即使考虑了在lock内部发生线程切换,但凭借xchgb这样的交换执行,依然不会引起问题

互斥锁的封装

我们尝试将互斥锁封装为C++类的模式,进行调用;

同时使用RAII的技术,让申请的锁能够自动释放

通过我们手动实现,也可以更方便的理解C++为我们提供的线程库底层是怎样的

Lock.hpp

#pragma once
#include 
#include 

//封装一个互斥锁的类,内部提供lock/unlock的接口
class Mutex
{
private:
    pthread_mutex_t lock_;//互斥锁
public:
    Mutex()//构造自动初始化锁
    {
        pthread_mutex_init(&lock_, nullptr);
    }
    ~Mutex()//析构自动销毁锁
    {
        pthread_mutex_destroy(&lock_);
    }
    void lock()//申请锁
    {
        pthread_mutex_lock(&lock_);
    }
    void unlock()//释放锁
    {
        pthread_mutex_unlock(&lock_);
    }
};

//定义了一个锁对象之后,为了防止加锁后忘记释放锁
//可以让临界区独处一个代码块,在代码块中定义一个Lock_Guard对象
//从而对临界区进行保护,在代码块结束,Lock_Guard对象声明周期结束会自动释放锁
class Lock_Guard
{
private:
    Mutex *mutex_;
public:
    Lock_Guard(Mutex *mutex)
        : mutex_(mutex)
    {
        mutex_->lock();
    }
    ~Lock_Guard()
    {
        mutex_->unlock();
    }
};

mythread.cc

int ticket = 1000;
Mutex mutex;//定义一个锁,自动完成初始化
bool getTicket();
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (!getTicket())//还有票则继续抢,没票了则直接退出线程
        {
            break;
        }
        cout << id << " get ticket success" << endl;
        // pthread_mutex_lock(&mutex); // 加锁
    }
    return nullptr;
}
bool getTicket()
{
    bool ret = false;//栈中的数据不是临界资源
    {//将Lock_Gard和临界区放于一个代码块中,这块区域则被保护起来了,代码块结束会自动释放锁
        Lock_Guard gard(&mutex);
        if (ticket > 0)
        {
            ticket--;
            ret = true;
        }
    }
    usleep(1000);//代表买票的实际过程消耗的时间
    return ret;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

死锁问题

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

互斥锁作为一种需要被抢占的资源,同样也会发生死锁问题

死锁案例

看如下代码

void *route1(void *args)
{
    cout << "我是线程1" << endl;
    return nullptr;
}
void *route2(void *args)
{
    cout << "我是线程2" << endl;
    return nullptr;
}
int main()
{
    cout << "我是主线程" << endl;
    pthread_t t1, t2;
    pthread_create(&t1, nullptr, route1, nullptr);
    pthread_create(&t2, nullptr, route2, nullptr);
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    cout << "主线程退出" << endl;
    return 0;
}

如果不加锁,可以正常执行:

Linux_线程控制_第16张图片

但如果用如下放方式进行加锁:

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *route1(void *args)
{
    pthread_mutex_lock(&mutex1);
    sleep(1); // 让中间有一定的时间延迟,从而让两个线程能够各申请一把锁
    pthread_mutex_lock(&mutex2);
    
    cout << "我是线程1" << endl;
    
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return nullptr;
}
void *route2(void *args)
{
    pthread_mutex_lock(&mutex2);
    sleep(1); // 让中间有一定的时间延迟,从而让两个线程能够各申请一把锁
    pthread_mutex_lock(&mutex1);
    
    cout << "我是线程2" << endl;
    
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return nullptr;
}
int main()
{
    cout << "我是主线程" << endl;
    pthread_t t1, t2;
    pthread_create(&t1, nullptr, route1, nullptr);
    pthread_create(&t2, nullptr, route2, nullptr);
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    cout << "主线程退出" << endl;
    return 0;
}

此时就发生了死锁

两个线程都要申请1、2两把锁,但是线程1先申请了1号锁,线程2先申请了2号锁,当线程1要申请2号锁的时候发生阻塞,线程2要申请1号锁的时候也发生阻,两个线程都发生阻塞,都无法unlock,便会一直这样阻塞下去

这便是互斥锁造成的死锁

注意:上面的代码虽然看起来极具违和感,但是一旦代码复杂起来,这样的情况不是没有可能发生

发生死锁的条件

  1. 互斥条件:一个资源每次只能被一个执行流使用
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁的方法

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

自旋锁

以上我们学习了互斥锁,当调用了phtread_mutex_lock()但没有抢到锁时,当前线程会从R状态变为S进入阻塞状态,直到其它线程解锁了,此线程才会被唤醒抢锁,然后继续执行后续代码

但是如果这个等待的过程极短,那么这个进入阻塞->阻塞中->被唤醒的过程就显得尤为笨重

这种情况下我们就可以使用自旋锁,这时如果lock申请不到锁不会进入阻塞状态,而是CPU进行疯狂的轮询检测,一旦锁可以申请,迅速就可以完成加锁操作

使用方法

有了互斥锁的使用基础,自旋锁的使用方法极为简单

只需要把先前所有的mutex换成spin,剩余用法仿照互斥锁即可

使用局限

在自旋锁等待的时候,CPU是在不停运行的

所以,如果无法保证一定会在极短的时间内等待成功,则还是建议使用互斥锁

线程同步

理解饥饿问题与线程同步

#include 
#include 
#include 
#include 
#include 
int ticket = 1000;
pthread_mutex_t mutex;
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        pthread_mutex_lock(&mutex); // 加锁
        if (ticket > 0)
        {
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex); // 解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}
int main()
{
    // 使用某个锁之前先进行初始化,下面这些线程会用到这个锁
    pthread_mutex_init(&mutex, nullptr);
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    // 使用锁结束,销毁锁
    pthread_mutex_destroy(&mutex);
}

运行如上代码,我们会发现,1000张票全被同一个线程抢走抢走

Linux_线程控制_第17张图片

我们分析一下:

最一开始,线程1抢到了mutex锁

此时,当运行到其它三个线程时,它们都开始阻塞申请Mutex锁

但是在线程1释放锁之前,任何线程都申请不到锁,无法向下执行

Linux_线程控制_第18张图片

那线程2如何能申请到mutex锁呢?

假设当线程1执行完unlock代码,此时恰好被切走,线程2开始执行

此时线程2即可成功申请锁

但是,我们可以看到,unlock执行结束后,只需一句jump指令,又开始执行loxk申请锁

而一句jump指令相对于整个代码块,所占的时间比例极小

也就是说,恰好在jump前后线程被切走的概率极低,其它线程能申请到锁的概率也就极小

于是,我们看到的现象就是,只有线程1在反复抢票,其它线程一直阻塞着,一张票也抢不到

像这样,一个执行流长时间得不到某种资源,我们成为饥饿问题

为了解决这样由互斥造成的饥饿问题,我们让线程访问某种资源时具有一定的顺序性(在保证数据安全的前提下),我们称为同步

条件变量

认识接口

与互斥锁类似,在使用条件变量前,我们要先定义一个条件变量用于申请

pthread_cond_t cond;

初始化

方法一:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

方法二:

函数原型:

#include

int pthread_cond_init(pthread_cond_t *restrict cond,
			const pthread_condattr_t *restrict attr);
  • cond:指定的环境变量
  • attr:环境变量属性,设置为空与方法一等价

使用:

pthread_cond_t cond;
int main()
{
    pthread_cond_init(&cond, nullptr);
}

销毁

#include 

int pthread_cond_destroy(pthread_cond_t *cond);

让当前线程排队等待被唤醒

函数原型:

#include 

int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);
  • cond:要在这个条件变量下等待
  • mutex:条件变量需要搭配互斥锁使用

唤醒等待中的线程

函数原型:

#include 

//唤醒当前条件变量下等待的的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//唤醒当前条件变量下等待的的第一个线程
int pthread_cond_signal(pthread_cond_t *cond);
  • cond:条件变量

简单案例

#include 
#include 
using namespace std;
pthread_cond_t cond;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *waitCommand(void *)
{
    pthread_detach(pthread_self()); // 主线程不再需要join
    while (true)
    {
        pthread_cond_wait(&cond, &mutex);
        cout << "thread " << pthread_self() << " running..." << endl;
    }
}
int main()
{
    pthread_cond_init(&cond, nullptr);
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, waitCommand, nullptr);
    pthread_create(&t2, nullptr, waitCommand, nullptr);
    pthread_create(&t3, nullptr, waitCommand, nullptr);
    while (true)
    {
        char cmd;
        cout << "Commond:";
        cin >> cmd;
        if (cmd == 'n')
        {
            pthread_cond_signal(&cond);
        }
        else
        {
            break;
        }
    }
}
Linux_线程控制_第19张图片

如上代码,三个子线程都在cond条件变量下进行排队等待,等待被唤醒

在主线程中,输入n就有一个队伍中的线程被唤醒,输入q即退出

可以看到,在输入n的过程中,三个线程有序运行,不存在饥饿问题

将main函数中的pthread_cond_signal换为pthread_cond_broadcast每次将同时唤醒三个线程

Linux_线程控制_第20张图片

第二次三个线程被调用的顺序与第一次也一致

生产者消费者模型

在一些业务场景中,为了提高效率,往往一些线程产生的任务会交由其它线程执行,尽量减少CPU阻塞等待外设的时间

因此就会有一些线程用于产生任务,我们称为生产者;一些线程用于执行任务,称为消费者

但是,如果生产者和消费者是直接交流的,不管是生产者产生问题的频率高,还是消费者解决问题的频率高,一定会有一方等另一方的情况出现,从而大大降低了效率,所以要想办法让生产者和消费者解耦。

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

123原则

  • 一个交易场所:
    内存中的额一段空间(某种数据结构)

  • 线程承担的两种角色:

    1. 生产者
    2. 消费者
  • 三种关系:

    1. 生产者和生产者:互斥

      (保证一个生产者放完一个完整的任务,其它生产者才能放入)

    2. 消费者和消费者:互斥

      (保证一个消费者取走整个任务,其它消费者才能开始取)

    3. 生产者和消费者:互斥&同步

      (生产者在交易产所中放完一个完整任务消费者才能取,消费者拿任务之前生产者一定已经放入了若干任务)

阻塞队列

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于:

  • 队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;
  • 队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出

(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

Linux_线程控制_第21张图片

单生产者单消费者

为方便理解,我们先以单生产者,单消费者进行实现,后续再作完善

BlockQueue.hpp

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

#define DEF_CAPACITY 3
template <class T>
class BlockQueue
{
private:
    uint32_t cap_;           //容量
    queue<T> bq_;            // blockqueue
    pthread_mutex_t mutex_;  //保护阻塞队列的互斥锁
    pthread_cond_t conCond_; //让消费者等待的环境变量
    pthread_cond_t proCond_; //让生产者等待的环境变量

public:
    BlockQueue(uint32_t cap = DEF_CAPACITY)
        : cap_(cap)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&conCond_, nullptr);
        pthread_cond_init(&proCond_, nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&conCond_);
        pthread_cond_destroy(&proCond_);
    }

public:
    //生产接口
    void push(const T &val)
    {
        //加锁
        //判断是否适合生产:bq满-->不生产-->休眠(同时释放了锁资源)
        //                bq不满-->生产(占据锁资源)
        //为防止轮询判断导致的生产者对锁资源的抢占,要设置环境变量,让其在不生产的时候阻塞休眠,释放锁资源给消费者
        //生产
        //解锁
        lockQueue();
        while (isFull())
        {
            //应该用while,用if不保险
            //等待成功并不一定等于队列不满
            //如果等待调用失败,不应该向下进行生产
            //可能我们自己或系统误发了唤醒信号,但此时依然是队满
            //所以唤醒后还要进行判满
            //概率非常小,但依然要判断,提高代码健壮性
            proBlockWait();
        }
        pushCore(val);
        unlockQueue();
        wakeupCon(); //唤醒消费者
        // 此时,如果消费者在等待,让消费者开始消费,消费为空成自然又开始阻塞
        // 如果消费者没有在等待,也不影响,消费者继续消费,生产者继续生产、
        // 消费者等待的原因是,刚刚队列为空
        // 此时生产者刚放完一个,已经不为空,即表示生产者线程和消费者线程可以开始同步运行
    }
    //消费接口
    T pop()
    {
        //加锁
        //判断是否适合消费:bq空-->不消费-->休眠(同时释放了锁资源)
        //                bq不空-->消费(占据锁资源)
        //为防止轮询判断导致的消费者对锁资源的抢占,要设置环境变量,让其在不消费的时候阻塞休眠,释放锁资源给生产者
        //生产
        //解锁
        lockQueue();
        while (isEmpty())
        {
            conBlockWait();
        }
        //条件满足,可以消费
        T tmp = popCore();
        unlockQueue();
        wakeupPro(); //唤醒生产者
        return tmp;
    }

private:
    void lockQueue()
    {
        pthread_mutex_lock(&mutex_);
    }
    void unlockQueue()
    {
        pthread_mutex_unlock(&mutex_);
    }
    bool isEmpty()
    {
        return bq_.size() == 0;
    }
    bool isFull()
    {
        return bq_.size() == cap_;
    }
    void proBlockWait() //生产者一定在临界区内
    {
        //在阻塞线程的时候, pthread_cond_wait会自动释放锁
        pthread_cond_wait(&proCond_, &mutex_);
        //在阻塞等待结束后,返回的时候, pthread_cond_wait会自动重新获得锁,然后才返回
    }
    void conBlockWait() //阻塞等待,等待被唤醒
    {
        pthread_cond_wait(&conCond_, &mutex_);
    }
    void wakeupCon() //唤醒消费者
    {
        pthread_cond_signal(&conCond_);
    }
    void wakeupPro() //唤醒生产者
    {
        pthread_cond_signal(&proCond_);
    }
    void pushCore(const T &in)
    {
        bq_.push(in);
    }
    T popCore()
    {
        T tmp = bq_.front();
        bq_.pop();
        return tmp;
    }
};

阻塞队列测试一

生产者生成随机数,消费者消费随机数

BlockQueueTest.cpp

#include "BlockQueue.hpp"
#include 
#include "Task.hpp"
// 队列传入数据
void *consumer(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        int data = bq->pop();
        sleep(1); // 消费得慢
        cout << "consumer 消费数据完成" << data << endl;
    }
}
void *producer(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        int data = rand() % 10; // 制作数据
        // sleep(1); // 生产得慢
        bq->push(data);
        cout << "productor 生产数完成" << data << endl;
    }
}
int main()
{
    srand((unsigned)time(nullptr));
    //定义一个阻塞队列
    //创建两个线程
    BlockQueue<int> bq;
    pthread_t con, pro;
    pthread_create(&con, nullptr, consumer, &bq);
    pthread_create(&pro, nullptr, producer, &bq);

    pthread_join(con, nullptr);
    pthread_join(pro, nullptr);

    return 0;
}

生产时长 > 消费时长:

Linux_线程控制_第22张图片

消费时长 > 生产时长:

Linux_线程控制_第23张图片

阻塞队列测试二

生产线程随机制造一些+ - * / %的算式

消费线程计算这些算式

Task.hpp

#pragma once
#include 

class Task
{
private:
    int elem1_;
    int elem2_;
    char op_;

public:
    Task(int elem1 = 0, int elem2 = 0, char op = 0)
        : elem1_(elem1),
          elem2_(elem2),
          op_(op) {}
    int operator()()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (op_)
        {
        case '+':
            result = elem1_ + elem2_;
            break;
        case '-':
            result = elem1_ - elem2_;
            break;
        case '*':
            result = elem1_ * elem2_;
            break;
        case '/':
            if (elem2_ == 0)
            {
                std::cout << "div zero error" << std::endl;
                return -1;
            }
            result = elem1_ / elem2_;
            break;
        case '%':
            if (elem2_ == 0)
            {
                std::cout << "div zero error" << std::endl;
                return -1;
            }
            result = elem1_ % elem2_;
            break;

        default:
            std::cout << "非法操作" << std::endl;
            break;
        }
        return result;
    }
    void getTask(int &elem1, int &elem2, char &op)
    {
        elem1 = elem1_;
        elem2 = elem2_;
        op = op_;
    }
};

BlockQueueTest.cpp

#include "Task.hpp"
string ops = "+-*/%";

//队列传入任务
void *consumer(void *args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        Task task = bq->pop(); //提取任务
        int res = task();      //处理任务
        //处理结果展示
        int elem1 = 0;
        int elem2 = 0;
        char op = 0;
        task.getTask(elem1, elem2, op);
        char taskBuff[50];
        sprintf(taskBuff, "%d %c %d = %d", elem1, op, elem2, res);
        cout << "consumer 消费任务完成" << taskBuff << endl;
    }
}
void *producer(void *args)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        sleep(1);
        int elem1 = rand() % 50;
        int elem2 = rand() % 10;
        char op = ops[rand() % 5];
        Task task(elem1, elem2, op); //制作一个任务
        bq->push(task);              //放入任务队列
        //打印完成信息
        char taskBuff[50];
        sprintf(taskBuff, "%d %c %d = ?", elem1, op, elem2);
        cout << "productor 生产任务完成" << taskBuff << endl;
    }
}
//并行如何体现:
//生产者和消费者放入队列、取出队列的时候虽然是串行的,
//但是生产者生产任务,消费者完成任务试并发进行的
//并发并不是出现在临界区的
int main()
{
    srand((unsigned)time(nullptr));
    //定义一个阻塞队列
    //创建两个线程
    BlockQueue<Task> bq;
    pthread_t con, pro;
    pthread_create(&con, nullptr, consumer, &bq);
    pthread_create(&pro, nullptr, producer, &bq);

    pthread_join(con, nullptr);
    pthread_join(pro, nullptr);

    return 0;
}
Linux_线程控制_第24张图片

POSIX信号量

所谓信号量就是一个计数器,只不过相对于一个普通的变量,这个计数器一般用来描述临界资源

其中++--操作都是原子的,分别表示归还资源申请资源

当信号量为0时,再进行--(申请信号量)就会发生阻塞

与互斥锁类似,一般信号量用于资源预定

  • 当一个线程申请锁成功,不管临界资源是否访问完成,它都已经拥有了这块资源,直到释放锁,别人才能访问这块资源;

  • 当一个线程申请了信号量,这个信号量对应的一块资源将被它唯一的使用,在归还之前,这块资源都不会被其它线程使用

当一个信号量为1,申请后变为0,归还了变回1,相当于与一个互斥锁

我们称这样的信号量为二元信号量

而互斥锁就是一个二元信号量

调用接口

初始化信号量

#include 

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:用于初始化的信号量
  • pshared:0表示线程间共享,非零表示进程间共享
  • value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

对信号量进行–操作

int sem_wait(sem_t *sem);

对信号量进行++操作

int sem_post(sem_t *sem);

基于环形队列的生产者消费者模型

我们前面用条件变量实现了基于阻塞队列的生产者消费者模型

当时,整个队列就是临界区,生产者添加任务和消费者消费任务的代码都需要进行加锁操作,也就是说生产者和消费者无法同时访问阻塞队列

这里,我们用信号量实现一个环形队列

Linux_线程控制_第25张图片

生产者在head放入数据,消费者消费tail处的数据

这里整个环形队列是临界资源,我们也可以把它划分成多个区域,当生产者线程访问区域1时,消费者线程可以同时访问7号区域

Linux_线程控制_第26张图片

但是,当消费者和生产者访问同一块区域时,就必须保证互斥

且这样的情况有两种:

  • 消费者快,追上了生产者,此时队列为空

    所以要先生产再消费

  • 生产者快,套了消费者一圈,此时队列为满

    所以要先消费完再生产

这便需要信号量来保证

  • 对于生产者,它最关心空间资源,

    空间不为0就可以生产,空间为0就阻塞

    所以我们为生产者定义一个信号量sem_t roomSem = N

  • 对于消费者,他最关心数据资源,

    数据不为0就可以消费

    所以我们为消费者定义一个信号量sem_t dataSem = 0

生产过程:

roomSem–;

//生产

dataSem++;

消费过程:

dataSem–;

//消费

roomSem++;

当环形队列为满时,roomSem为0,dataSem为N,

此时生产线程再执行roomSem--;就会阻塞,

消费线程dataSem--;可以正常执行;

当环形队列为空时,roomSem为N,dataSem为0

此时消费线程会在dataSem--;处进行阻塞

生产线程正常执行;

当阻塞队列不为空也不为满,消费者和生产者可以并发执行

以此,我们便使用信号量保证了环形队列的安全性

实现

ringQueue.hpp

#pragma once
#include 
#include 
#include 
#include 

using namespace std;
const int gCap = 10;
// 环形队列,支持多生产者多消费者
template <class T>
class RingQueue
{
private:
    vector<T> ringQueue_;   // 环形队列
    sem_t roomSem;          // 空间信号量
    sem_t dataSem;          // 数据信号量
    uint32_t pIndex = 0;    // 生产者当前写入位置
    uint32_t cIndex = 0;    // 消费者当前读取位置
    pthread_mutex_t pmutex; // 保证生产者之间互斥
    pthread_mutex_t cmutex; // 保证消费者之间互斥

public:
    RingQueue(int cap = gCap)
        : ringQueue_(cap)
    {
        sem_init(&roomSem, 0, cap);
        sem_init(&dataSem, 0, 0);
        pthread_mutex_init(&pmutex, nullptr);
        pthread_mutex_init(&cmutex, nullptr);
    }
    ~RingQueue()
    {
        sem_destroy(&roomSem);
        sem_destroy(&dataSem);
        pthread_mutex_destroy(&pmutex);
        pthread_mutex_destroy(&cmutex);
    }
    // 生产
    void push(const T &val)
    {
        sem_wait(&roomSem); // 申请信号量
        pthread_mutex_lock(&pmutex);
        ringQueue_[pIndex] = val;
        pIndex++; // 写入位置后移
        pIndex %= ringQueue_.size();
        pthread_mutex_unlock(&pmutex);
        sem_post(&dataSem);
    }
    // 消费
    T pop()
    {
        sem_wait(&dataSem);
        pthread_mutex_lock(&cmutex);
        T tmp = ringQueue_[cIndex];
        cIndex++; // 读取位置后移
        cIndex %= ringQueue_.size();
        pthread_mutex_unlock(&cmutex);
        sem_post(&roomSem);
        return tmp;
    }
};

如上环形队列的两个互斥锁用于保证多生产者多消费者时,消费者内部和生产者内部的互斥

测试

ringQueueTest.cc

// 队列传入数据
void *consumer(void *args)
{
    RingQueue<int> *bq = static_cast<RingQueue<int> *>(args);
    while (true)
    {
        int data = bq->pop();
        // sleep(1); // 消费得慢
        cout << "consumer 消费数据完成" << data << endl;
    }
}
void *producer(void *args)
{
    RingQueue<int> *bq = static_cast<RingQueue<int> *>(args);
    while (true)
    {
        int data = rand() % 10; // 制作数据
        sleep(1);               // 生产得慢
        bq->push(data);
        cout << "productor 生产数完成" << data << endl;
    }
}
int main()
{
    srand((unsigned)time(nullptr));
    // 定义一个阻塞队列
    // 创建两个线程
    RingQueue<int> bq;
    pthread_t con1, con2, pro1, pro2;
    pthread_create(&con1, nullptr, consumer, &bq);
    pthread_create(&con2, nullptr, consumer, &bq);
    pthread_create(&pro1, nullptr, producer, &bq);
    pthread_create(&pro2, nullptr, producer, &bq);

    pthread_join(con1, nullptr);
    pthread_join(con2, nullptr);
    pthread_join(pro1, nullptr);
    pthread_join(pro2, nullptr);

    return 0;
}
Linux_线程控制_第27张图片

线程池

我们知道新线程的创建和销毁都需要额外的时间

如果一些任务需要创建新线程执行,但是执行时间又相对较短,这时反复创建销毁线程的时间比就相对长

或者短时间内产生了大量的任务,需要新线程执行,但是创建这么多线程无疑会使内存达到极限

所以,我们不妨事先创造一些线程,和一个任务队列,将它们管理起来,形成一个线程池;当有任务需要执行,就放入任务队列中,线程池会自动挑选空闲的线程执行这些任务,即使当前所有线程都在使用当中,任务也会先存到队列中,等又空余线程再执行任务

下面我们对线程池进行实现

线程池的实现

首先一个线程池要有自己的任务队列,push进来的任务将暂存于此队列中

由于有多个线程访问同一个队列,所有访问队列的动作都要进行加锁

当线程池启动起来,要创建几个线程,分别等待任务队列中有任务传入

当有任务被push到线程池,就唤醒一个线程从队列中拿取任务,并开始执行

threadPool.hpp

#pragma once
#include "Lock.hpp"
#include 
#include 
#include 
#include 
#include 
#include  //更改线程名,便于调试查看
#include 
using namespace std;

const int gThreadNum = 5;
template <class T>
class ThreadPool
{
private:
    bool isStart;           // 判断防止当前线程池多次被启动
    int threadNum_;         // 线程的数量
    queue<T> taskQueue_;    // 任务队列
    pthread_mutex_t mutex_; // 保证访问任务队列是原子的
    pthread_cond_t cond_;   // 如果当前任务队列为空,让线程等待被唤醒
    bool quit_;
    static ThreadPool<T> *instance_; // 设计成单例模式

public:
    static ThreadPool<T> *getInstance()
    {
        static Mutex mutex;
        if (nullptr == instance_) // 仅仅过滤重复的判断
        {
            Lock_Guard lockGuard(&mutex); // 保护后面的内容
            if (nullptr == instance_)
            {
                instance_ = new ThreadPool<T>();
            }
        }

        return instance_;
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

public:
    void start() // 创建多个线程,让它们等待被唤醒,执行push的任务
    {
        assert(isStart == false);
        isStart = true;
        for (int i = 0; i < threadNum_; i++)
        {
            pthread_t tmp;
            pthread_create(&tmp, nullptr, threadRoutine, this);
        }
    }
    void quit() // 关闭线程池时确保所有任务都完成了
    {
        while (haveTask())
        {
            pthread_cond_broadcast(&cond_);
            // usleep(1000);
            //  cout << taskQueue_.size() << endl;
        }
        quit_ = true;
    }
    void push(const T &in) // 在线程池中添加任务
    {
        lockQueue();
        taskQueue_.push(in);
        choiceThreadForHandl();
        unlockQueue();
    }

private:
    ThreadPool(int threadNum = gThreadNum)
    {
        threadNum_ = threadNum;
        assert(threadNum > 0);
        isStart = false;
        quit_ = false;
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ThreadPool(const ThreadPool<T> &) = delete;           // 单例防拷贝
    ThreadPool operator=(const ThreadPool<T> &) = delete; // 同上
    static void *threadRoutine(void *args)
    {
        prctl(PR_SET_NAME, "follower");
        pthread_detach(pthread_self());
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        while (true) // 循环从任务队列中拿出任务并执行,队列为空则等待任务出现
        {
            tp->lockQueue();
            while (!tp->haveTask()) // 如果任务队列为空则等待
            {
                if (tp->quit_) // 当调用quit且队列已经为空的时候quit_才会被置为true
                {
                    cout << "quit" << endl;
                    return nullptr;
                }
                tp->waitForTask();
            }
            // 将任务从队列中拿到出来执行
            T t = tp->pop();
            tp->unlockQueue();

            t.run();
            // 规定所有任务内都有一个自己的run方法

            // if(quit&&)
        }
    }
    void lockQueue() // 加锁
    {
        pthread_mutex_lock(&mutex_);
    }
    void unlockQueue() // 解锁
    {
        pthread_mutex_unlock(&mutex_);
    }
    void waitForTask() // 让线程等待被唤醒
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool haveTask() // 队列不为空
    {
        return !taskQueue_.empty();
    }
    void choiceThreadForHandl() // 随便唤醒一个等待的线程
    {
        pthread_cond_signal(&cond_);
    }
    T pop() // 从队列中拿取一个任务
    {
        T tmp = taskQueue_.front();
        taskQueue_.pop();
        return tmp;
    }
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance_ = nullptr; // 单例

测试

Task.hpp

#pragma once
#include 

class Task
{
private:
    int elem1_;
    int elem2_;
    char op_;

public:
    Task(int elem1 = 0, int elem2 = 0, char op = 0)
        : elem1_(elem1),
          elem2_(elem2),
          op_(op) {}
    int operator()()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (op_)
        {
        case '+':
            result = elem1_ + elem2_;
            break;
        case '-':
            result = elem1_ - elem2_;
            break;
        case '*':
            result = elem1_ * elem2_;
            break;
        case '/':
            if (elem2_ == 0)
            {
                std::cout << "div zero error" << std::endl;
                return -1;
            }
            result = elem1_ / elem2_;
            break;
        case '%':
            if (elem2_ == 0)
            {
                std::cout << "div zero error" << std::endl;
                return -1;
            }
            result = elem1_ % elem2_;
            break;

        default:
            std::cout << "非法操作" << std::endl;
            break;
        }

        char taskBuff[50];
        sprintf(taskBuff, "%d %c %d = %d", elem1_, op_, elem2_, result);
        cout << pthread_self() << "完成任务" << taskBuff << endl;
        return result;
    }
    void getTask(int &elem1, int &elem2, char &op)
    {
        elem1 = elem1_;
        elem2 = elem2_;
        op = op_;
    }
};

Lock.hpp

#pragma once
#include 
#include 

class Mutex
{
private:
    pthread_mutex_t lock_;

public:
    Mutex()
    {
        pthread_mutex_init(&lock_, nullptr);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&lock_);
    }
    void lock()
    {
        pthread_mutex_lock(&lock_);
    }
    void unlock()
    {
        pthread_mutex_unlock(&lock_);
    }
};

class Lock_Guard
{
private:
    Mutex *mutex_;

public:
    Lock_Guard(Mutex *mutex)
        : mutex_(mutex)
    {
        mutex_->lock();
    }
    ~Lock_Guard()
    {
        mutex_->unlock();
    }
};

ThreadPoolTest.cc

#include "ThreadPool.hpp"
#include "Task.hpp"
#include 
#include 

int main()
{
    prctl(PR_SET_NAME, "master");
    const string operars = "+-*/%";
    unique_ptr<ThreadPool<Task>> tp(ThreadPool<Task>::getInstance()); // 获取单例
    tp->start();
    srand((unsigned int)time(nullptr));
    int cnt = 10;
    while (true)
    {
        // 模拟任务的制作
        int one = rand() % 50;
        int two = rand() % 50;
        char op = operars[rand() % operars.size()];
        Task t(one, two, op);
        tp->push(t);
        usleep(200000);
        char taskBuff[50];
        sprintf(taskBuff, "%d %c %d = ?", one, op, two);
        cout << "生产任务完成:" << taskBuff << endl;
        // 将任务交给线程池处理
    }
    tp->quit();
}
Linux_线程控制_第28张图片

如上线程池已经设计为单例模式,且线程安全

读者写者问题

前面我们学习了生产者消费者模型

当时,我们要保证生产者和生产者互斥、消费者和消费者互斥、生产者和消费者要保证互斥且同步

因为不管是生产者还是消费者,它们都需要在同一个交易场所中 拿 / 放 数据

但现实生活中还有一种情况:

  • 大量的线程会高频次读取临界区的内容,但并不会进行修改其中的内容(我们称其为读者)

  • 有小部分线程偶尔会对临界区内的数据进行修改(我们称其为写者)

像这样的情况,我们称之为读者写者问题

同样我们也分析一下读者写者问题中两种身份之间的关系:

  • 写者和写者:互斥
  • 读者和读者:没有关系(可以并行)
  • 读者和写者:互斥

此时如果像以前,读者在进行读取时都要加互斥锁,那么读者之间则无法同时对临界区进行访问,大大影响效率

POSIX库为了解决这样的读者写者问题,提供了专门的读写锁

使用方法

#include 

pthread_rwlock_t rwlock;//读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict attr);//初始化读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//为读者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//为写者加锁

  • pthread_rwlock_init初始化时,attr默认填写NULL

读写优先级问题;

POSIX提供的读写锁默认是读取优先的,也就是说如果有大量线程不停的调用读锁,前一个读者还没释放锁,后一个线程就又申请了读锁,得一直等到所有的读者都释放了锁,这无疑会造成写者的饥饿问题

如果我们想提高写者的优先级:当写者申请锁时,后续申请锁的读者先阻塞等待着,则可以对读写锁的权限进行更改

但目前是有些BUG的,实际测试时即使更改了权限,表现行为依然相同

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/

两种权限的初始化:

{
#if 1 // 写优先
    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);
    pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
    pthread_rwlock_init(&rwlock, &attr);
    pthread_rwlockattr_destroy(&attr);
#else // 读优先,会造成写饥饿
    pthread_rwlock_init(&rwlock, nullptr);
#endif
}

你可能感兴趣的:(Linux,linux,运维,服务器)