Linux多线程(详细全解)

目录

Linux线程概念

什么是线程

线程的优点

线程的缺点

线程异常

线程用途

Linux进程和线程的区别

进程vs线程

Linux线程控制

POSIX线程库

创建线程

线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以用来传递对象!!

线程ID以及进程地址空间布局

线程终止

pthrea_exit函数

pthread_cancel函数

线程等待

为什么需要线程等待?

分离线程

创建多线程例子:

线程互斥

进程线程间的互斥相关背景概念

互斥量mutex

互斥量的接口

方法1.静态分配

方法2.动态分配

销毁互斥量

互斥量加锁和解锁

锁的应用:使用RAII思想处理锁资源。

可重入VS线程安全

概念

常见的线程不安全的情况

常见的线程安全的情况

常见不可重入的情况

常见可重入的情况

可重入与线程安全联系

可重入与线程安全区别

常见锁概念

死锁

死锁的四个必要条件

避免死锁

Linux线程同步

条件变量

同步概念与竞态条件

条件变量函数初始化

销毁条件变量

等待条件满足

唤醒等待

使用条件变量:

为什么pthread_cond_wait需要互斥量?

条件变量的使用规范

生产者消费者模型

[举例理解Comsumer Productor问题](#举例理解Comsumer Productor问题)

为何要使用生产者消费者模型

生产者消费者模型优点

[基于Blocking Queue的生产者消费者模型](#基于Blocking Queue的生产者消费者模型)

BlockingQueue

POSIX信号量

初始化信号量

销毁信号量

等待信号量

发布信号量

下面来对比一下互斥量和信号量的区别

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

下面使用C++模拟实现一下环形队列的CP模型

线程池

线程池的应用场景

线程安全的单例模式

什么是单例模式

什么是设计模式

单例模式的特点

饿汉实现方式和懒汉实现方式

饿汉模式实现单例模式

懒汉方式实现单例模式

懒汉模式实现单例模式(线程安全版本)

为什么懒汉模式线程安全版本要使用Double-check:

STL,智能指针和线程安全

STL中的容器是否是线程安全的?

智能指针是否是线程安全的?

其他常见的各种锁

读者写者问题

读写锁

读写锁接口

设置读写优先

初始化

销毁

加锁和解锁


Linux线程概念

什么是线程

1.在一个程序的一个执行路线就叫做线程(thread)。更准确的定义是:线程是"一个进程内部的控制序列"。

2.一切进程至少都有一个执行线程。

3.线程在进程内部运行,本质是在进程地址空间内运行。

4.在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。

5.透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

Linux中没有真正意义上的线程,而是用"进程"模拟的线程。上图才是真正的进程!!!

Linux实现线程的方案:

1.在Linux中,线程在进程"内部"执行,线程在进程的地址空间内运行---->任何执行流,都要有资源!地址空间是进程的资源窗口。
2.在Linux中,线程的执行粒度要比进程更细,线程执行进程代码的一部分。

线程比进程要更加轻量化,为什么?

1.创建和释放更加的轻量化,因为线程只是进程地址空间内的资源,没有进程那么大。
2.切换更加轻量化。线程在进行切换的时候,不需要重新cache(缓存的热数据)数据,所以切换更加轻量化。

线程的优点

1.创建一个新线程的代价要比创建一个新进程的代价小得多。

2.与进程之间的切换相比,线程之间的切换需要操作系统的工作要小得多。

3.线程占用的资源要比进程小得多。

4.能充分利用多处理器的可并行数量。

5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。

6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。

7.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

1.性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

2.健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

3.缺乏访问控制:进程是可控制访问的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

4.编程难度提高:编写与调试一个多线程程序比单线程程序困难的多。

线程异常

1.单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

2.线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程。进程终止,该进程内的所有线程也就随即退出。

线程用途

1.合理的使用多线程,能提高CPU密集型程序的执行效率

2.合理的使用多线程,能提高IO密集型程序的用户体验(如:生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

Linux进程和线程的区别

进程vs线程

进程是资源分配的基本单位。
线程是调度的基本单位。
线程共享进程的数据,但是也拥有自己的一部分数据:

1.线程ID

2.一组寄存器(独立的上下文)

3.栈(独立的栈结构)

4.errno

5.信号屏蔽字

6.调度优先级

进程的多个线程共享 同一地址空间,因此,Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用。如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:

1.文件描述符表

2.各种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)

3.当前工作目录

4.用户id和组id

进程和线程的关系图如下:

所以以前的单进程---->就是一个线程执行流的进程。

Linux线程控制

POSIX线程库

1.与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"开头的

2.要使用这些函数库,要通过引入头文件<pthread.h>

3.链接这些线程函数时要使用编译器命令的"-lpthread"选项

创建线程

因为Linux中没有很明确的线程的概念,只有轻量化进程的概念----->所以不会直接提供线程的系统调用,只会提供轻量级进程的系统调用!

pthread线程库----->应用层----->对轻量级进程接口进行封装。为用户提供直接线程的接口!
几乎所有Linux平台,都默认自带这个库!
Linux中编写多线程代码 需要用到第三方pthread库!!!
功能:创建一个新的线程。

原型:

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

参数:

thread:返回线程ID。

attr:设置线程的属性,attr为NULL表示使用默认属性。

start_routine:是个函数地址,线程启动后要执行的函数。

arg:传给线程启动函数的参数。

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

使用pthread_create创建线程:

cpp 复制代码
mythread.cc


#include <iostream>
#include <unistd.h>
#include <pthread.h>

void *threadRoutine(void *args)
{
    while (true)
    {
        std::cout << "new thread,pid:" << getpid() << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);

    while (true)
    {
        std::cout << "main thread,pid:" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}
cpp 复制代码
makefile文件




mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f mythread

错误检查:

1.传统的一些函数是,成功返回0,错误返回-1,并且对全局变量errno赋值以指示错误

2.pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。

3.pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。

使用第四个参数,传递参数给函数指针:

cpp 复制代码
mythread.cc



#include <iostream>
#include <unistd.h>
#include <pthread.h>

int g_val = 100;

void *threadRoutine(void *args)
{
    const char *name = (char *)args;
    while (true)
    {
        printf("%s pid:%d,g_val:%d,&g_val:%p\n", name, getpid(), g_val, &g_val);
        // std::cout << "new thread,pid:" << getpid() << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"Thread 1");

    while (true)
    {
        printf("main thread pid:%d,g_val:%d,&g_val:%p\n", getpid(), g_val, &g_val);
        // std::cout << "main thread,pid:" << getpid() << std::endl;
        g_val++;
        sleep(1);
    }
    return 0;
}

线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以用来传递对象!!

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <pthread.h>

class Request
{
public:
    Request(int start, int end, const std::string &name)
        : _start(start), _end(end), _name(name)
    {
    }

public:
    int _start;
    int _end;
    std::string _name;
};

class Respond
{
public:
    Respond(int result, int exitcode)
        : _result(result), _exitcode(exitcode)
    {
    }

public:
    int _result;
    int _exitcode;
};

void *SumCount(void *args)
{
    Request *rq = static_cast<Request *>(args);
    Respond *rsp = new Respond(0, 0);
    for (size_t i = rq->_start; i <= rq->_end; i++)
    {
        std::cout << rq->_name << "is caling..." <<i<< std::endl;
        rsp->_result += i;
        usleep(10000);
    }
    delete rq;
    return rsp;
}

int main()
{
    pthread_t tid;
    Request *rq = new Request(1, 100, "thread 1");
    pthread_create(&tid, nullptr, SumCount, rq);

    // 线程等待join,并拿到线程的返回值
    void *retval;
    pthread_join(tid, &retval);
    Respond *rsp = static_cast<Respond *>(retval);
    std::cout << "rsp->result: " << rsp->_result << ", exitcode: " << rsp->_exitcode << std::endl;
    delete rsp;

    return 0;
}

线程ID以及进程地址空间布局

pthread_create函数会产生一个线程ID,存放第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
前面说的线程ID是属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_self函数,可以获得线程自身ID

pthread_t pthread_self(void);

那么pthread_t到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

1.从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。

2.线程可以调用pthread_exit终止自己。

3.一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

注意:exit是用来结束进程的,不能用来终止线程!!!

pthrea_exit函数

功能:线程终止

原型:

void pthread_exit(void * value_ptr);

参数:

value_ptr:value_ptr不要指向一个局部变量。

返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时,线程函数已经退出了。

pthread_cancel函数

功能:取消一个执行中的线程。

原型:

int pthread_cancel(pthread_t thread);

参数:

thread:线程ID

返回值:成功返回0,失败返回错误码。
使用样例:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>

int g_val = 100;

void *threadRoutine(void *args)
{
    const char *name = (char *)args;
    int cnt = 5;
    while (true)
    {
        printf("%s pid:%d,g_val:%d,&g_val:%p\n", name, getpid(), g_val, &g_val);
        // std::cout << "new thread,pid:" << getpid() << std::endl;
        sleep(1);
        cnt--;
        if (cnt == 0)
            break;
    }

    // return (void *)100; // 使用return返回
    pthread_exit((void *)100); 使用pthread_exit返回
    // exit(100);          千万不可使用exit来返回!!!
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"Thread 1");

    void *retval;
    pthread_join(tid, &retval); // main thread等待的时候,默认是阻塞等待的!

    std::cout << "main thread quit...,ret: " << (long long int)retval << std::endl;

    return 0;
}

使用pthread_cancel()函数终止线程。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>

int g_val = 100;

void *threadRoutine(void *args)
{
    const char *name = (char *)args;
    int cnt = 5;
    while (true)
    {
        printf("%s pid:%d,g_val:%d,&g_val:%p\n", name, getpid(), g_val, &g_val);
        // std::cout << "new thread,pid:" << getpid() << std::endl;
        sleep(1);
        cnt--;
        if (cnt == 0)
            break;
    }

    // return (void *)100; // 使用return返回
    pthread_exit((void *)100); // 使用pthread_exit返回
    // exit(100);          // 千万不可使用exit来返回!!!
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"Thread 1");
    pthread_cancel(tid);

    void *retval;
    pthread_join(tid, &retval); // main thread等待的时候,默认是阻塞等待的!

    std::cout << "main thread quit...,ret: " << (long long int)retval << std::endl;

    return 0;
}

结果为:

线程等待

为什么需要线程等待?

1.已经退出的线程,其空间没有被释放。仍然在进程的地址空间中。

2.创建新的线程不会复用刚才退出线程的地址空间。
功能:等待线程结束。

原型:

int pthread_join(pthread_t thread,void* * value_ptr );

参数:

thread:线程ID

value_ptr:它指向一个指针,后者指向线程的返回值。

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

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

1.如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。

2.如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED(-1)。

3.如果thread线程是自己调用thread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

4.如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。

使用案例:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>

int g_val = 100;

void *threadRoutine(void *args)
{
    const char *name = (char *)args;
    int cnt = 5;
    while (true)
    {
        printf("%s pid:%d,g_val:%d,&g_val:%p\n", name, getpid(), g_val, &g_val);
        // std::cout << "new thread,pid:" << getpid() << std::endl;
        sleep(1);
        cnt--;
        if (cnt == 0)
            break;
    }

    return (void *)100; // 使用return返回
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"Thread 1");

    void *retval;
    pthread_join(tid, &retval); // main thread等待的时候,默认是阻塞等待的!

    std::cout << "main thread quit...,ret: " << (long long int)retval << std::endl;

    return 0;
}

Linux下为64位,void*是8个字节。用long long int。
为什么我们join的时候不考虑异常呢?------>因为做不到!!!线程出异常,这个进程都会挂掉!

分离线程

1.默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

2.如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

std::string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void *threadRoutine(void *args)
{
    const char *name = static_cast<char *>(args);
    while (true)
    {
        std::cout << name << " is running... " << toHex(pthread_self()) << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
    sleep(1);

    std::cout << "main thread create new thread done,new thread id is " << toHex(pthread_self()) << std::endl;
    pthread_join(tid, nullptr);

    return 0;
}

创建多线程例子:

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <pthread.h>

#define NUM 10

struct ThreadData
{
    std::string _threadname;
};

std::string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void InitThreadData(ThreadData *&td, int i)
{
    td->_threadname = "thread-" + std::to_string(i);
}

void *threadRoutine(void *args)
{
    ThreadData *rd = static_cast<ThreadData *>(args);
    int i = 0;
    while (i < NUM)
    {
        std::cout << rd->_threadname << ",tid:" << toHex(pthread_self()) << std::endl;
        i++;
        sleep(1);
    }
    delete rd;
    return nullptr;
}

int main()
{
    // 创建多线程
    std::vector<pthread_t> tids;
    for (size_t i = 0; i < NUM; i++)
    {
        ThreadData *td = new ThreadData; // 要使用堆空间来创建
        InitThreadData(td, i);

        pthread_t tid;
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }

    // 线程等待
    for (size_t i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

每一个线程都有自己的独立的栈结构!但是线程的栈上的数据也是可以被其他线程看到并访问的!全局变量是被所有线程同时看到并访问的!

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <pthread.h>

#define NUM 10

int *p = nullptr;

struct ThreadData
{
    std::string _threadname;
};

std::string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void InitThreadData(ThreadData *&td, int i)
{
    td->_threadname = "thread-" + std::to_string(i);
}

void *threadRoutine(void *args)
{
    int test_i = 0;
    ThreadData *rd = static_cast<ThreadData *>(args);
    if (rd->_threadname == "thread-2")
        p = &test_i;
    int i = 0;
    while (i < NUM)
    {
        std::cout << rd->_threadname << ",tid:" << toHex(pthread_self())
                  << ",test_i=" << test_i << ",&test_i=" << &test_i << std::endl;
        i++;
        test_i++;
        sleep(1);
    }
    delete rd;
    return nullptr;
}

int main()
{
    // 创建多线程
    std::vector<pthread_t> tids;
    for (size_t i = 0; i < NUM; i++)
    {
        ThreadData *td = new ThreadData; // 要使用堆空间来创建
        InitThreadData(td, i);

        pthread_t tid;
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }

    // main thread 能获取thread-2子线程中的栈数据
    sleep(1);
    std::cout << "main thread get a thread local value,val: " << *p << ",&val: " << p << std::endl;

    // 线程等待
    for (size_t i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

结果如下:线程的栈内的数据也是被main线程获取,并访问了。

全局变量是被所有线程同时看到并访问的!如果我们想让线程一个"私有的全局变量"呢?

方法:在全局变量的前面加上 __thread 修饰全局变量。

表示会给每一个线程私有一份放在线程的局部存储。

注意:__thread只能够定义内置类型,不能用来修饰自定义类型!

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <pthread.h>

#define NUM 10

__thread int number = 0;

struct ThreadData
{
    std::string _threadname;
};

std::string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%p", tid);
    return buffer;
}

void InitThreadData(ThreadData *&td, int i)
{
    td->_threadname = "thread-" + std::to_string(i);
}

void *threadRoutine(void *args)
{
    ThreadData *rd = static_cast<ThreadData *>(args);
    number = pthread_self();
    int i = 0;
    while (i < NUM)
    {
        std::cout << "tid: " << number << std::endl;
        // std::cout << rd->_threadname << ",tid:" << toHex(pthread_self())
        //           << ",test_i=" << test_i << ",&test_i=" << &test_i << std::endl;
        i++;
        sleep(1);
    }
    delete rd;
    return nullptr;
}

int main()
{
    // 创建多线程
    std::vector<pthread_t> tids;
    for (size_t i = 0; i < NUM; i++)
    {
        ThreadData *td = new ThreadData; // 要使用堆空间来创建
        InitThreadData(td, i);

        pthread_t tid;
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }

    // main thread 能获取thread-2子线程中的栈数据
    // sleep(1);
    // std::cout << "main thread get a thread local value,val: " << *p << ",&val: " << p << std::endl;

    // 线程等待
    for (size_t i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

对于线程的等待:除了使用pthread_join外,还可以pthread_detach(pthread_self())进行线程分离。除了在main线程分离子线程外,还可以在子线程内部自己进行pthread_detach分离。

注:线程分离和线程等待不可同时存在,是冲突的!

线程互斥

进程线程间的互斥相关背景概念

1.临界资源:多线程执行流共享的资源就叫做临界资源。

2.临界区:每个线程内部,访问临界资源的代码,就叫做临界区。

3.互斥:任何时刻,互斥保证有且只有一个执行流进入到临界区,访问临界资源,通常对临界资源起保护作用。

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

互斥量mutex

1.大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

2.但是有时候,很多变量都需要再线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

3.多个线程并发的操作共享变量,会带来一些问题。

下面来模拟一下多线程并发抢票带来的问题:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>

#define NUM 4

int ticket = 100;

struct ThreadData
{
    ThreadData(int number)
    {
        _threadname = "thread-" + std::to_string(number);
    }
    std::string _threadname;
};

void *gitTicket(void *args)
{
    ThreadData *rd = static_cast<ThreadData *>(args);
    const char *name = rd->_threadname.c_str();
    while (true)
    {
        if (ticket > 0)
        {
            usleep(10000);
            printf("%s get a ticket,ticket:%d\n", name, ticket);
            ticket--;
        }
        else
            break;
    }
    printf("%s ... quit\n", name);
    return nullptr;
}

int main()
{
    std::vector<pthread_t> tids;
    std::vector<ThreadData *> tds;

    for (size_t i = 0; i < NUM; i++)
    {
        pthread_t tid;
        ThreadData *rd = new ThreadData(i);

        tds.push_back(rd);
        pthread_create(&tid, nullptr, gitTicket, tds[i]);
        tids.push_back(tid);
    }

    // 线程分离
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }

    for (auto thread : tds)
    {
        delete thread;
    }

    return 0;
}

结果为:并发出错了,为什么?

为什么可能无法获取争取结果?

1.if语句判断为真以后,代码可以并发的切换到其他线程。

2.usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多线程会进入该代码段。

3.--ticket 操作本身就不是一个原子操作。

--操作并不是原子操作,而是对应三条汇编指令:

1.load:将共享变量ticket从内存加载到寄存器中。

2.update:更新寄存器里面的值,执行-1操作。

3.store:将新值,从寄存器写回共享变量ticket的内存地址。

要解决以上问题,需要做到三点:

1.代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

2.如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就需要一把锁。Linux上提供的这把锁叫互斥量。

互斥量的接口

初始化互斥量。初始化互斥量有两种方法:

方法1.静态分配

pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;

方法2.动态分配

int pthread_mutex_init(pthread_mutex_t * restrict mutex,const pthread_mutexattr_t * restrict attr);

参数:

mutex:要初始化的互斥量。

attr:nullptr

销毁互斥量

销毁互斥量需要注意:

1.使用PTHREAD_MUTEX_INITIALIZER(宏)初始化的互斥量不需要销毁!

2.不要销毁一个已经加锁的互斥量。

3.已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t * mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t * mutex);

int pthread_mutex_unlock(pthread_mutex_t * mutex);

返回值:成功返回0,失败返回错误号。

调用pthread_mutex_lock时,可能会遇到以下情况:

1.互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。

2.发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

下面利用互斥量来解决上面的买票出错:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>

#define NUM 4


使用这个宏来定义这个全局锁,就不需要再init和destroy了
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

int ticket = 1000;

struct ThreadData
{
    ThreadData(int number, pthread_mutex_t *mutex)
    {
        _threadname = "thread-" + std::to_string(number);
        lock = mutex;
    }
    std::string _threadname;
    pthread_mutex_t *lock;
};

void *gitTicket(void *args)
{
    ThreadData *rd = static_cast<ThreadData *>(args);
    const char *name = rd->_threadname.c_str();
    while (true)
    {
        pthread_mutex_lock(rd->lock);//申请锁成功,才能往后执行,不成功,阻塞等待。
        if (ticket > 0)
        {
            usleep(10000);
            printf("%s get a ticket,ticket:%d\n", name, ticket);
            ticket--;
            pthread_mutex_unlock(rd->lock);
        }
        else
        {
            pthread_mutex_unlock(rd->lock);
            break;
        }
        usleep(100);
    }
    printf("%s ... quit\n", name);
    return nullptr;
}

int main()
{
    std::vector<pthread_t> tids;
    std::vector<ThreadData *> tds;
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);//初始化

    for (size_t i = 0; i < NUM; i++)
    {
        pthread_t tid;
        ThreadData *rd = new ThreadData(i, &lock);

        tds.push_back(rd);
        pthread_create(&tid, nullptr, gitTicket, tds[i]);
        tids.push_back(tid);
    }

    // 线程分离
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }

    for (auto thread : tds)
    {
        delete thread;
    }

    pthread_mutex_destroy(&lock);//销毁

    return 0;
}

结果如下:加了锁之后正常了。

加锁的本质:是用时间来换取安全。
加锁的表现:线程对于临界区代码串行执行。
加锁原则:尽量的要保证临界区代码,越少越好!
注:纯互斥环境,如果锁的分配不够合理,容易导致其他线程的饥饿问题。

同步---->让所有的线程,获取锁,按照一定的顺序。按照一定的顺序性获取资源!!

锁本身就是共享资源!!!所以,申请和释放锁本身就被设计成了原子性操作!!

互斥量实现原理

1.经过上面的例子,大家已经意识到单纯的i++或者++i都不是原子的,有可能会有数据不一致的问题。
2.为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。

锁的应用:使用RAII思想处理锁资源。

cpp 复制代码
LockGuard.hpp





#pragma once

#include <iostream>
#include <unistd.h>
#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t *lock) : _lock(lock)
    {
    }
    void Lock()
    {
        pthread_mutex_lock(_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(_lock);
    }
    ~Mutex() {}

private:
    pthread_mutex_t *_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock) : _mutex(lock)
    {
        _mutex.Lock();
    }
    ~LockGuard()
    {
        _mutex.Unlock();
    }

private:
    Mutex _mutex;
};
cpp 复制代码
mythread.cc



#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
#include "LockGuard.hpp"

#define NUM 4

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定义全局锁
int ticket = 1000;

struct ThreadData
{
    ThreadData(int number)
    {
        _threadname = "thread-" + std::to_string(number);
    }
    std::string _threadname;
};

void *buyTicket(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    const char *name = td->_threadname.c_str();
    while (true)
    {
        {
            // 局部变量,出了作用域自动销毁,调用析构函数释放锁资源
            LockGuard mutex(&lock);
            if (ticket > 0)
            {
                printf("%s get a ticket,ticket:%d\n", name, ticket);
                ticket--;
            }
            else
                break;
        }
        usleep(100); // 模拟抢票的过程,防止某个锁的竞争太强
    }
    printf("%s quit!\n", name);
    return nullptr;
}

int main()
{
    std::vector<pthread_t> tids;
    std::vector<ThreadData *> tds;
    for (size_t i = 0; i < NUM; i++)
    {
        pthread_t tid;
        ThreadData *td = new ThreadData(i);
        tds.push_back(td);
        pthread_create(&tid, nullptr, buyTicket, tds[i]);
        tids.push_back(tid);
    }

    // 线程等待
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }

    for (auto thread : tds)
    {
        delete thread;
    }
    return 0;
}

可重入VS线程安全

概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入一个函数在重入的情况下,运行结果总是正确且不会出现任何问题,则该函数被称为可重入函数,否则,是不可重入函数

常见的线程不安全的情况

1.不保护共享变量的函数。

2.函数状态随着被调用,状态发生变化的函数。

3.返回指向静态变量指针的函数。

4.调用线程不安全函数的函数。

常见的线程安全的情况

1.每个线程对全局变量或静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。

2.类或者接口对于线程来说都是原子操作。

3.多个线程之间的切换不会导致该接口的执行结果存在二义性。

常见不可重入的情况

1.调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。

2.调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

3.可重入函数体内使用了静态的数据结构。

常见可重入的情况

1.不使用全局变量或静态变量。

2.不使用malloc或者new开辟出的空间。

3.不调用不可重入函数。

4.不返回静态或全局数据,所有数据都有函数的调用者来提供。

5.使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

可重入与线程安全联系

1.函数是可重入的,那就是线程安全的。

2.函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。

3.如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

1.可重入函数是线程安全函数的一种。

2.线程安全不一定是可重入的,而可重入函数一定是线程安全的。

3.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还没有释放则会产生死锁,因此是不可重入的。

常见锁概念

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于一种永久等待状态。
一个锁也可以引发死锁,比如:你申请了两次锁资源没有释放,就死锁了。
注:pthread_mutex_trylock()是一个非阻塞式申请锁资源,申请失败就用返回值,不会阻塞。

死锁的四个必要条件

注:想产生死锁,这四个条件必须同时满足!

1.互斥条件:一个资源每次只能被一个执行流使用。(前提)

2.请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(原则)

3.不剥夺条件:一个执行流已获得的资源,在没使用完之前,不能强行剥夺。(原则)

4.循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。(重要条件)

避免死锁

1.破坏死锁的四个必要条件(只需要破坏其中一个即可破坏死锁)。

2.加锁顺序一致。

3.避免所未释放的场景。

4.资源一次性分配。

避免死锁的算法:死锁检测算法(了解),银行家算法(了解)。

Linux线程同步

条件变量

1.当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

2.例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

条件变量函数初始化

int pthread_cond_init(pthread_cond_t * restrict cond,const pthread_condattr_t * restrict attr);

参数:

cond:要初始化的条件变量。

attr:nullptr。

销毁条件变量

int pthread_cond_destroy(pthread_cond_t * cond);

等待条件满足

int pthread_cond_wait(pthread_cond_t * restrict cond,pthread_mutex_t * restrict mutex);

参数:

cond:要在这个条件变量上等待。

mutex:互斥量。

注:pthread_cond_wait让线程等待的时候,会自动释放锁资源!!!

唤醒等待

int pthread_cond_broadcast(pthread_cond_t * cond);//唤醒所有的线程

int pthread_cond_signal(pthread_cond_t * cond);//唤醒一个线程

使用条件变量:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>

#define NUM 4

int cnt = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 使用全局变量定义mutex和cond可以不用pthread_mutex_destroy和pthread_cond_destroy

void *Count(void *args)
{
    pthread_detach(pthread_self()); // 线程自我分离
    uint64_t number = (uint64_t)args;
    std::cout << "thread-" << number << " create success\n";
    while (true)
    {
        pthread_mutex_lock(&lock);
        pthread_cond_wait(&cond, &lock); // 放入condd的等待队列里
        std::cout << "thread-" << number << " ,cnt: " << cnt++ << std::endl;
        pthread_mutex_unlock(&lock);
    }
    return nullptr;
}

int main()
{
    for (uint64_t i = 0; i < NUM; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, Count, (void *)i); // 这里是传值到线程函数中
        usleep(100);
    }
    sleep(3);
    std::cout << "main ctrl begin..." << std::endl;

    while (true)
    {
        sleep(1);
        pthread_cond_signal(&cond); // 唤醒cond的等待队列中等待的第一个线程
        // pthread_cond_broadcast(&cond);//唤醒cond的等待队列中的全部线程
        std::cout << "signal one thread..." << std::endl;
    }

    return 0;
}

如上面代码所写,为什么pthread_cond_wait要在pthread_mutex_lock后面呢?必须要这样写吗?

因为判断临界资源是否就绪,也是在访问临界资源!!!所以pthread_cond_wait必须要在pthread_mutex_lock之后写!!!

为什么pthread_cond_wait需要互斥量?

1.条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且有好的通知等待在条件变量上的线程。
2.条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

那么,按照上面的说法,我们设计出如下代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了。代码如下:

// 错误的设计

pthread_mutex_lock(&mutex);

while (condition_is_false) {

pthread_mutex_unlock(&mutex);

//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过 pthread_cond_wait(&cond);

pthread_mutex_lock(&mutex);

}

pthread_mutex_unlock(&mutex);
由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,那么pthread_cond_wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t * mutex);进入该函数后,会去看条件变量等于0不?等于,就把互斥量变成1,直到cond_wait返回,把互斥量恢复成原样。

条件变量的使用规范

1.等待条件代码

pthread_mutex_lock(&mutex);

while (条件为假)

pthread_cond_wait(cond, mutex);

修改条件

pthread_mutex_unlock(&mutex);

2.给条件发送信号代码

pthread_mutex_lock(&mutex);

设置条件为真。

pthread_cond_signal(cond);

pthread_mutex_unlock(&mutex);

生产者消费者模型

321原则(便于自己记忆)

举例理解Comsumer Productor问题

所以consumer productor问题可以简化成321原则来便于记忆:

3种关系(生产vs生产,消费vs消费,生产vs消费),2个角色(生产和消费),1个交易场所(特定结构的内存空间)。

consumer productor模式的优点是:1.对生产者和消费者的关系进行解耦。2.支持忙闲不均。3.支持并发。

为何要使用生产者消费者模型

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

生产者消费者模型优点

1.解耦。

2.支持并发。

3.支持忙闲不均。

基于Blocking Queue的生产者消费者模型

BlockingQueue

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素。当队列满时,往队列里存放元素的操作也会被阻塞,直到元素被从队列中取出(以上的操作都是基于不同的线程来说,线程在对阻塞队列进程操作时会被阻塞)。

下面使用C++queue模拟阻塞队列的生产消费模型:

cpp 复制代码
BlockQueue.hpp



#pragma once
#include <iostream>
#include <unistd.h>
#include <queue>
#include <pthread.h>

template <class T>
class BlockQueue
{
    static const int defaultnum = 10;

public:
    BlockQueue(int maxcap = defaultnum) : _maxcap(maxcap)
    {
        // 对互斥量和条件变量进行初始化
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        _low_water = _maxcap / 3;
        _high_water = (_maxcap * 2) / 3;
    }
    T pop()
    {
        pthread_mutex_lock(&_mutex);
        while(_q.size() == 0) // 判断也是访问临界资源,所以要在lock后
        {
            //要防止线程wait的时候,被伪唤醒
            pthread_cond_wait(&_c_cond, &_mutex); // consumer进入等待
        }
        T data = _q.front();
        _q.pop();
        if (_q.size() < _low_water)
            pthread_cond_signal(&_p_cond); // 消费太快,赶紧唤醒一个生产者
        pthread_mutex_unlock(&_mutex);
        return data;
    }

    void push(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        while(_q.size() == _maxcap) // 判断也是访问临界资源,所以要在lock后
        {
            pthread_cond_wait(&_p_cond, &_mutex);
        }
        _q.push(in);
        if (_q.size() > _high_water)
            pthread_cond_signal(&_c_cond); // 生产太快了,赶紧唤醒一个消费者
        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_c_cond);
        pthread_cond_destroy(&_p_cond);
    }

private:
    std::queue<T> _q; // 共享资源
    int _maxcap;      // 支持放入的最大数据
    pthread_mutex_t _mutex;
    pthread_cond_t _c_cond; // consumer的条件变量
    pthread_cond_t _p_cond; // productor的条件变量
    int _low_water;         // 低水位线
    int _high_water;        // 高水位线
};
cpp 复制代码
main.cc



#include <iostream>
#include <unistd.h>
#include "BlockQueue.hpp"

void *Consumer(void *args)
{
    BlockQueue<int> *rq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        // consumer
        int data = rq->pop();
        std::cout << "消费了一个数据:" << data << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *Productor(void *args)
{
    BlockQueue<int> *rq = static_cast<BlockQueue<int> *>(args);
    int data = 0;
    while (true)
    {
        // productor
        data++;
        rq->push(data);
        std::cout << "生产了一个数据:" << data << std::endl;
    }
    return nullptr;
}

int main()
{
    BlockQueue<int> *rq = new BlockQueue<int>();
    pthread_t c, p;
    pthread_create(&c, nullptr, Consumer, rq);
    pthread_create(&p, nullptr, Productor, rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    delete rq;

    return 0;
}

BlockingQueue不仅可以传内置类型,还可以传对象,任务...

cpp 复制代码
Task.hpp





#pragma once
#include <iostream>
#include <unistd.h>
#include <string>

enum
{
    DIVZERO = 1,
    MODZERO,
    UNKNOWN
};

const char *opers = "+-*/%";

class Task
{
public:
    Task(int data1, int data2, char op)
        : _data1(data1), _data2(data2), _op(op), _result(0), _exitcode(0)
    {
    }
    std::string GetTask()
    {
        std::string r = std::to_string(_data1);
        r += _op;
        r += std::to_string(_data2);
        r += "=?";
        return r;
    }

    std::string GetResult()
    {
        std::string r = std::to_string(_data1);
        r += _op;
        r += std::to_string(_data2);
        r += "=";
        r += std::to_string(_result);
        r += "[exitcode: ";
        r += std::to_string(_exitcode);
        r += " ]";
        return r;
    }

    void run()
    {
        switch (_op)
        {
        case '+':
            _result = _data1 + _data2;
            break;
        case '-':
            _result = _data1 - _data2;
            break;
        case '*':
            _result = _data1 * _data2;
            break;
        case '/':
            if (_data2 == 0)
                _exitcode = DIVZERO;
            else
                _data1 / _data2;
            break;
        case '%':
            if (_data2 == 0)
                _exitcode = MODZERO;
            else
                _data1 % _data2;
            break;
        default:
            _exitcode = UNKNOWN;
            break;
        }
    }

private:
    int _data1;
    int _data2;
    char _op;
    int _result;
    int _exitcode;
};
cpp 复制代码
main.cc




#include <iostream>
#include <unistd.h>
#include <cstring>
#include <ctime>
#include "BlockQueue.hpp"
#include "Task.hpp"

void *Consumer(void *args)
{
    BlockQueue<Task> *rq = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        // consumer
        Task t = rq->pop();
        t.run();
        std::cout << "消费了一个任务:" << t.GetTask() << ",结果为:" << t.GetResult() << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *Productor(void *args)
{
    int len = strlen(opers);
    BlockQueue<Task> *rq = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        // productor
        // 处理数据
        int data1 = rand() % 10;
        int data2 = rand() % 10;
        char op = opers[rand() % len];
        Task t(data1, data2, op);

        rq->push(t);
        std::cout << "生产了一个任务:" << t.GetTask() << std::endl;
    }
    return nullptr;
}

int main()
{
    srand(time(nullptr));
    BlockQueue<Task> *rq = new BlockQueue<Task>();
    pthread_t c, p;
    pthread_create(&c, nullptr, Consumer, rq);
    pthread_create(&p, nullptr, Productor, rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    delete rq;

    return 0;
}

结果如下:

POSIX信号量

POSIX信号量和System V信号量作用相同,都是用于同步操作,,达到无冲突的访问共享资源目的。但是POSIX可以用于线程间同步。

信号量的本质是一把计数器。执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源!

信号量值只能为0,1两态的,就称为二元信号量。

申请信号量,本质就是对计数器减减,P操作。

释放信号量,本质就是对计数器加加,V操作。

申请和释放PV操作----->都是原子的!

初始化信号量

#include <semaphore.h>

int sem_init(sem_t * sem,int pshared,unsigned int value);

参数:

pshared:0表示线程间共享,非零表示进程间共享。

value:信号量初始值。

销毁信号量

int sem_destroy(sem_t * sem);

等待信号量

功能**:等待信号量,会将信号量的值减一**。

int sem_wait(sem_t * sem);//P()P操作

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值增加一。

int sem_post(sem_t * sem);//V()V操作

上面介绍的生产者消费者例子是基于queue的,其空间是可以动态分配的,现在基于固定大小的环形队列(POSIX信号量)来重写这个CP模型。

下面来对比一下互斥量和信号量的区别

|----------|-----------------------------------|--------------------------------------------------------------------------------------------------------|
| 特性 | 互斥量(mutex) | 信号量(semaphore) |
| 核心目的 | 互斥,保护临界区,防止数据竞争 | 同步与协作,协调多线程的执行顺序或控制对资源池的并发访问量 |
| 抽象模型 | 一把钥匙(二元状态:锁定/未锁定) | 一个非负整数计数器 +等待队列 |
| 所有者 | 有所有者概念。只有锁定的线程能解锁。 | 没有所有者概念。任何线程都可以增加(V)或者减少(P)信号量 |
| 计数 | 二元的,只有0(被占)和1(可用)两种状态。 | 计数的,其值可以是非负整数(如0,1,2,3,4...,N)。 |
| 主要操作 | lock():获取锁。 unlock():释放锁。 | wait()/P():信号量减一,如果值<0则阻塞。 signal()/V():信号量加一,唤醒一个等待者。 |
| 典型用途 | 保护一个共享变量、一个数据结构、一段代码。 | 1.限流:控制同时访问某个资源(如如数据库连接池)的最大线程数。 2.同步:线程A完成某任务后通知线程B(初始值为0的信号量)。 3.生产者-消费者问题(通常需要两个信号量配合)。 |
| 释放责任 | 严格配对,必须由加锁者解锁。 | 可由任意线程操作,V操作不一定由执行了P操作的线程执行。 |

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

环形队列采用数组模拟,用模运算来模拟环状特性。

环形结构起始状态和结束状态都是一样的,不好判断为空或者为满。所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。

但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程。

下面使用C++模拟实现一下环形队列的CP模型

cpp 复制代码
RingQueue.hpp


#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

static const int defaultnum = 5;

template <class T>
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        // 申请信号量,--
        sem_wait(&sem);
    }
    void V(sem_t &sem)
    {
        // 释放信号量,++
        sem_post(&sem);
    }
    void Lock(pthread_mutex_t &mutex)
    {
        pthread_mutex_lock(&mutex);
    }
    void Unlock(pthread_mutex_t &mutex)
    {
        pthread_mutex_unlock(&mutex);
    }

public:
    RingQueue(int cap = defaultnum)
        : _ringqueue(cap), _cap(cap), _c_step(0), _p_step(0)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, _cap);
        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }

    void Push(const T &in)
    {
        // 信号量不需要加锁,本身就是原子操作
        //  先申请空间信号量--
        P(_space_sem);

        Lock(_p_mutex);
        _ringqueue[_p_step] = in;
        _p_step++;
        _p_step %= _cap; // 循环
        Unlock(_p_mutex);

        // 释放信号量++
        V(_data_sem);
    }

    void Pop(T *out)
    {
        // 先申请数据信号量--
        P(_data_sem);

        Lock(_c_mutex);
        *out = _ringqueue[_c_step];
        _c_step++;
        _c_step %= _cap;
        Unlock(_c_mutex);

        // 释放空间信号量++
        V(_space_sem);
    }

    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }

private:
    std::vector<T> _ringqueue; // 数组模拟环形队列
    int _cap;                  // 数组容量
    int _c_step;               // 消费者下标
    int _p_step;               // 生产者下标
    sem_t _data_sem;           // 数据信号量
    sem_t _space_sem;          // 空间信号量

    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
};
cpp 复制代码
main.cc




#include <iostream>
#include <ctime>
#include <pthread.h>
#include "RingQueue.hpp"

void *Consumer(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (true)
    {
        // 消费
        int data = 0;
        rq->Pop(&data);
        std::cout << "消费了一个数据:" << data << std::endl;
    }
    return nullptr;
}

void *Productor(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (true)
    {
        // 生产
        int data = rand() % 10;
        rq->Push(data);
        std::cout << "生产了一个数据:" << data << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand(time(nullptr));
    RingQueue<int> *rq = new RingQueue<int>(10);
    pthread_t c[5], p[3];

    for (size_t i = 0; i < 3; i++)
    {
        pthread_create(&p[i], nullptr, Productor, rq);
    }
    for (size_t i = 0; i < 5; i++)
    {
        pthread_create(&c[i], nullptr, Consumer, rq);
    }

    for (size_t i = 0; i < 3; i++)
    {
        pthread_join(p[i], nullptr);
    }

    for (size_t i = 0; i < 5; i++)
    {
        pthread_join(c[i], nullptr);
    }

    delete rq;
    return 0;
}

RingQueue内不仅可以放入内置类型,还可以放对象、任务等。
下面演示一下生产和消费任务:

cpp 复制代码
Task.hpp




#pragma once
#include <iostream>
#include <unistd.h>
#include <string>

enum
{
    DIVZERO = 1,
    MODZERO,
    UNKNOWN
};

const char *opers = "+-*/%#@";

class Task
{
public:
    Task(int data1, int data2, char op)
        : _data1(data1), _data2(data2), _op(op), _exitcode(0), _result(0)
    {
    }
    Task()
    {
    }

    void run()
    {
        switch (_op)
        {
        case '+':
            _result = _data1 + _data2;
            break;
        case '-':
            _result = _data1 - _data2;
            break;
        case '*':
            _result = _data1 * _data2;
            break;
        case '/':
            if (0 == _data2)
                _exitcode = DIVZERO;
            else
                _result = _data1 / _data2;
            break;
        case '%':
            if (0 == _data2)
                _exitcode = MODZERO;
            else
                _result = _data1 % _data2;
            break;
        default:
            _exitcode = UNKNOWN;
            break;
        }
    }

    std::string GetTask()
    {
        std::string r = std::to_string(_data1);
        r += _op;
        r += std::to_string(_data2);
        r += "=?";
        return r;
    }

    std::string GetResult()
    {
        std::string r = std::to_string(_data1);
        r += _op;
        r += std::to_string(_data2);
        r += "=";
        r += std::to_string(_result);
        r += "[exitcode:";
        r += std::to_string(_exitcode);
        r += "]";
        return r;
    }

    void operator()()
    {
        this->run();
    }

private:
    int _data1;
    int _data2;
    char _op;
    int _exitcode;
    int _result;
};
cpp 复制代码
main.cc




#include <iostream>
#include <ctime>
#include <string.h>
#include <pthread.h>
#include "RingQueue.hpp"
#include "Task.hpp"

struct ThreadData
{
    RingQueue<Task> *_rq;
    std::string _threadname;
};

void *Consumer(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        // 消费
        Task t;
        td->_rq->Pop(&t);

        t();
        std::cout << "Consumer get a task: " << t.GetTask() << " who:" << td->_threadname
                  << " result: " << t.GetResult() << std::endl;
        usleep(100);
    }
    delete td;
    return nullptr;
}

void *Productor(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    int len = strlen(opers);
    while (true)
    {
        // 生产
        int data1 = rand() % 10;
        int data2 = rand() % 10;
        char op = opers[rand() % len];
        Task t(data1, data2, op);
        t();

        td->_rq->Push(t);
        std::cout << "Prodoct a task done,task: " << t.GetTask() << ",who: "
                  << td->_threadname << std::endl;
        sleep(1);
    }
    delete td;
    return nullptr;
}

int main()
{
    srand(time(nullptr));
    RingQueue<Task> *rq = new RingQueue<Task>(10);
    pthread_t c[5], p[3];

    for (size_t i = 0; i < 3; i++)
    {
        ThreadData *td = new ThreadData();
        td->_rq = rq;
        td->_threadname = "thread-" + std::to_string(i);
        pthread_create(&p[i], nullptr, Productor, td);
    }
    for (size_t i = 0; i < 5; i++)
    {
        ThreadData *td = new ThreadData();
        td->_rq = rq;
        td->_threadname = "thread-" + std::to_string(i);
        pthread_create(&c[i], nullptr, Consumer, td);
    }

    for (size_t i = 0; i < 3; i++)
    {
        pthread_join(p[i], nullptr);
    }

    for (size_t i = 0; i < 5; i++)
    {
        pthread_join(c[i], nullptr);
    }

    delete rq;
    return 0;
}

线程池

**线程池:**一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络socket等的数量。

线程池的应用场景

1.需要大量的线程来完成任务,且完成任务的时间比较短web服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大太多了。
2.对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3.接受突发性的大量需求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误

线程池的示例:

1.创建固定数量线程池,循环从任务队列中获取任务对象。

2.获取到任务对象后,执行任务对象中的任务接口。

下面来演示一下线程池的创建与使用

cpp 复制代码
ThreadPool.hpp




#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <queue>
#include <pthread.h>
#include "Task.hpp"

static const int defaultnum = 5;

struct ThreadData
{
    pthread_t _tid;
    std::string _name;
};

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }

    void WakeUpThread()
    {
        pthread_cond_signal(&_cond);
    }

    void SleepThread()
    {
        pthread_cond_wait(&_cond, &_lock);
    }

    bool IsQueueEmpty()
    {
        return _tasks.empty();
    }

    std::string GetThreadName(pthread_t tid)
    {
        for (const auto &th : _threads)
        {
            if (th._tid == tid)
                return th._name;
        }
        return "None";
    }

public:
    ThreadPool(int threadnum = defaultnum)
        : _threads(threadnum)
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    static void *ThreadRoute(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();
            while (tp->IsQueueEmpty())
            {
                tp->SleepThread(); // 线程进入等待
            }
            T t = tp->Pop();
            tp->Unlock();
            t();
            std::cout << name << " run," << "result:" << t.GetResult() << std::endl;
        }
        return nullptr;
    }

    void
    Start()
    {
        size_t num = _threads.size();
        for (size_t i = 0; i < num; i++)
        {
            _threads[i]._name = "thread-" + std::to_string(i + 1);
            pthread_create(&(_threads[i]._tid), nullptr, ThreadRoute, this);
        }
    }

    void Push(const T &in)
    {
        Lock();
        _tasks.push(in);
        WakeUpThread(); // push进去之后,唤醒一个线程来处理
        Unlock();
    }

    T Pop()
    {
        // 这里为啥不加锁呢?
        // 因为使用Pop是在我们的线程函数里面用的,那里面加了锁
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }

private:
    std::vector<ThreadData> _threads; // 线程池
    std::queue<T> _tasks;             // 线程处理的任务队列

    pthread_mutex_t _lock;
    pthread_cond_t _cond;
};
cpp 复制代码
main.cc



#include <iostream>
#include <ctime>
#include "Task.hpp"
#include "ThreadPool.hpp"

int main()
{
    ThreadPool<Task> *tp = new ThreadPool<Task>(5);
    tp->Start(); // 创建线程池
    srand(time(nullptr));
    int len = strlen(opers);
    while (true)
    {
        // 构建任务
        int data1 = rand() % 10;
        int data2 = rand() % 10;
        char op = opers[rand() % len];
        Task t(data1, data2, op);
        tp->Push(t);

        std::cout << "main thread make task: " << t.GetTask() << std::endl;
        sleep(1);
    }
    return 0;
}

线程安全的单例模式

什么是单例模式

单例模式是一种"经典的,常用的,常考的"设计模式

什么是设计模式

IT行业这么火,涌入的人很多。所以造成了大佬和菜鸟们两极分化的越来越严重。为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定一些对应的解决方案,这个就是设计模式

单例模式的特点

某些类,只应该具有一个对象(实例),就称之为单例。
在很多服务器开发场景中,经常需要让服务器加载很多数据(上百G)到内存中。此时往往要用一个单例的类来管理这些数据

饿汉实现方式和懒汉实现方式

洗碗的例子:

吃完饭,立刻洗碗,这种就是饿汉模式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。

吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,这就是懒汉模式。

懒汉方式最核心的思想就是"延迟加载"。从而能够优化服务器的启动速度。

饿汉模式实现单例模式

template<typename T>

class SingLeton

{

private:

static T data;

SingLeton(){}

public:

static T* GetInstance()

{

return &data;

}

};

//在main函数之间就完成初始化

复制代码
SingLeton* SingLeton::GetInstance = new SingLeton(); 

只要通过SingLeton这个包装类来使用T对象,则一个进程中只有一个T对象的实例。
饿汉模式的实例在程序启动时(进入main函数之前)就已经完成初始化,此时程序还是单线程状态,不存在多线程竞争的问题。

懒汉方式实现单例模式

template <typename T>

class SingLeton

{

static T* inst;

public:

static T* GetInstance()

{

if(inst == NULL)

{

inst=new T();

}

return inst;

}

};

T* SingLeton<T>::inst=nullptr;
但是上面的懒汉模式存在一个严重的问题,线程不安全。

线程不安全例子:

复制代码
线程A:进入if判断,instance为nullptr
线程B:也进入if判断,instance仍为nullptr(A还未创建完成)
线程A:执行new操作
线程B:也执行new操作 → 内存泄漏!返回不同实例!

第一次调用GetInstance的时候,如果两个线程同时调用,可能会创建出两份T对象的实例。但是后续再次调用,就没有用了

懒汉模式实现单例模式(线程安全版本)

//懒汉模式,线程安全

template <typename T>

class SingLeton

{

private:

SingLeton(){};

volatile static T* inst;//需要设置volatile关键字,否则可能被编译器优化。

static std::mutex lock;

public:

static T* GetInstance()

{

//懒汉模式要使用Double-Check,效率高

if(inst == NULL) { //双重判定空指针,降低锁冲突的概率,提高性能

lock.lock(); //使用互斥锁,保证多线程的情况下也只调用一次new。

if(inst == NULL)

{

inst = new T();

}

}

return inst;

}

SingLeton(const SingLeton<T>& )=delete;

SingLeton& (const SingLeton<T>& )=delete;

};

T* SingLeton<T>::inst=nullptr;

为什么懒汉模式线程安全版本要使用Double-check:

第一次检查(无锁):快速路径,避免已初始化实例的重复加锁产生的开销。

第二次检查(有锁):确保唯一性。
注:C++11标准之后能够保证局部静态变量初始化是线程安全的。

所以可以使用以下的最优方案:

复制代码
// C++11起线程安全的最简实现
class Singleton {
private:
    Singleton() {}
    
public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++11保证线程安全
        return instance;
    }
    
    // 删除拷贝构造函数和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

注意事项:

1.加锁解锁的位置。

2.双重if判定,避免不必要的锁竞争。

3.volatile关键字防止过度优化。

STL,智能指针和线程安全

STL中的容器是否是线程安全的?

不是线程安全的,原因是,STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁的方式不同,性能也可能不同(例如hash表的锁表和锁桶)。

因此STL默认不是线程安全。如果需要再多线程环境下使用,往往需要调用者自行保证线程安全。

智能指针是否是线程安全的?

对于unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。
对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题 。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证shared_ptr能够高效,原子的操作引用计数。

其他常见的各种锁

1.悲观锁:在每次取数据的时候,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
2.乐观锁:每次取数据的时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
3.CAS操作:当需要更新数据的时候,判断当前内存值和之前取得值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
4.自旋锁(看你在临界区待的时间长不长,如果不长就可以使用自旋锁),公平锁,非公平锁...

读者写者问题

读写锁

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高得多通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率 。那么有没有一种方法,可以专门处理这种多读少写的情况呢?--->有,那就是读写锁。

注意:写独占,读共享,读锁优先级高。

|-----|-------|-----|
| 写-写 | 写-读 | 读-读 |
| 互斥 | 互斥+同步 | 共享 |

读写锁接口

设置读写优先

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 写者优先,但写者不能递归加锁

*/

初始化

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);

int pthread_rwlock_unlock(pthread_rwlock_t * rwlock);

相关推荐
闲人编程8 小时前
消息通知系统实现:构建高可用、可扩展的企业级通知服务
java·服务器·网络·python·消息队列·异步处理·分发器
Xの哲學8 小时前
Linux Platform驱动深度剖析: 从设计思想到实战解析
linux·服务器·网络·算法·边缘计算
苏宸啊8 小时前
C++(二)类和对象上篇
开发语言·c++
Y淑滢潇潇8 小时前
RHCE Day 10 流程控制之条件语句和循环结构
linux·运维·rhce
gaize12138 小时前
服务器怎么选择与配置才能满足企业需求?
运维·服务器·架构
fqbqrr8 小时前
2601C++,编译时连接两个串指针
c++
superman超哥8 小时前
双端迭代器(DoubleEndedIterator):Rust双向遍历的优雅实现
开发语言·后端·rust·双端迭代器·rust双向遍历
嵌入式进阶行者8 小时前
【算法】TLV格式解析实例:华为OD机考双机位A卷 - TLV解析 Ⅱ
数据结构·c++·算法
OC溥哥9998 小时前
Paper MinecraftV3.0重大更新(下界更新)我的世界C++2D版本隆重推出,拷贝即玩!
java·c++·算法