Linux多线程

1.Linux线程概念

什么是线程

教材观点:线程是一个执行分值,执行粒度比进程更细(因为执行的是进程内部资源的一部分),调度成本更低。线程是进程内部的一个执行流(所谓一个线程在进程内部执行,说直白点,其实就是一个线程在进程的地址空间内运行,所以你这个线程就隶属于该进程)。

内核观点:线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体

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

一切进程至少都有一个执行线程(指有一个PCB)。

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

在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

注:CPU不需要区分谁是进程和谁是线程,但是切换工作是谁在执行的?切换工作是OS执行的,是CPU执行OS的代码完成的切换工作,所以OS在调度的时候发现你这个目标要切换的进程和之前的进程访问的是同一块地址空间,你俩的地址一样,所以说明你是在内部调用的,所以地址空间和页表就不需要切换了,否则反之,这就是为什么说线程的调度成本更低。

每一个PCB都可以看成一个执行流(线程),那么什么叫做进程?如下图中的大黑色框框选中的部分就被称为是一个进程,一个进程里面包含诺干个PCB,因为进程要从创建PCB开始,然后向CPU申请各种各样的空间对数据进行管理和分配,并映射到物理内存中,这一整套就叫做进程,而线程只不过是执行了进程分配给它的一部分资源,所以为什么说进程是承担分配系统资源的基本实体

OS要不要管理线程呢?必须的!先描述,在组织!!

TCB:线程控制块,属于进程PCB调度进程,线程调度

windows就是这么干的!--内核有真线程

Linux内核的设计者:复用你的PCB的结构体,用PCB模拟线程的TCB不就行了?很好的复用了进程的设计方案。

Linux没有真正意义上的线程,而是用进程方案模拟的线程!

复用代码和结构来实现更简单,好维护,效率更高,也更安全--Linux可以不间断的运行!实际上,一款0S系统,使用最频繁的功能,除了0S本身,下来就是进程了!

线程的优点

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

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

线程占用的资源要比进程少很多

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

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

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

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

线程的缺点

1.性能损失

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

2.健壮性降低

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

3.缺乏访问控制

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

4.编程难度提高

编写与调试一个多线程程序比单线程程序困难得多。

线程异常

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

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

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

.PHONY:clean
clean:
	rm -rf mythread

注:g++ -o $@ $^ -std=c++11 -lpthread ,在编译可执行程序时需要携带-lpthread,因为我们在Linux需要使用多线程,而多线程不是系统级多线程,他是一个原生用户库级别的多线程解决方案,也就是说你用的是一个库,所以你就要把这个库告诉编译器。

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

using namespace std;

int g_val = 0;//全局变量,在多线程场景中,我们多个线程看到的是同一个变量!

void* threadRun1(void* args)
{
    while(true)
    {
        sleep(1);
        cout << "t1 thread..." << getpid() << " &g_val: " << &g_val << " ,g_val: " << g_val << endl;
    }
}

void* threadRun2(void* args)
{
    //char *s = "hello bit";
    while(true)
    {
        sleep(1);
        cout << "t2 thread..." << getpid() << " &g_val: " << &g_val << " ,g_val: " << g_val++ << endl;
        //*s = 'S';
    }
}

int main()
{
    pthread_t t1, t2, t3;

    pthread_create(&t1, nullptr, threadRun1, nullptr);
    pthread_create(&t2, nullptr, threadRun2, nullptr);

    while(true)
    {
        sleep(1);
        cout << "main thread..." << getpid() << " &g_val: " << &g_val << " ,g_val: " << g_val << endl;
    }

    return 0;
}

pthread_t类型

pthread_t 是 POSIX 线程库中表示线程的数据类型,它是一个用于存储线程ID的数据类型。在 C/C++ 编程中,pthread_t 被用来创建、管理和控制线程。

在 POSIX 标准中,pthread_t 的原型通常定义为:

typedef unsigned long int pthread_t;

这意味着 pthread_t 是一个无符号长整型数据类型,用于存储线程的标识符(ID)。在实际编程中,pthread_t 变量被用来表示一个线程的标识符,并用于执行与该线程相关的操作,比如创建线程、等待线程结束、发送信号等。

需要注意的是,虽然 pthread_t 在 POSIX 标准中通常是无符号长整型,但其具体的实现可能因操作系统和编译器而异。在不同的系统上,pthread_t 的实际类型可能会有所不同,但通常都是用于唯一标识线程的类型。

pthread_create 函数

pthread_create 函数是 POSIX 线程库中用于创建新线程的函数。它的原型如下:

#include <pthread.h>

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

这个函数的参数解释如下:

  • thread: 一个指向 pthread_t 类型的指针,用于存储新创建线程的 ID。
  • attr: 一个指向 pthread_attr_t 类型的指针,用于指定新线程的属性。如果传入 NULL,则使用默认属性。
  • start_routine: 一个函数指针,指向新线程要运行的函数。这个函数应该具有 void* 类型的单一参数,并返回 void* 类型的结果。这个函数将在新线程中执行。
  • arg: 传递给 start_routine 函数的参数,即新线程启动时传递给它的参数。

pthread_create 函数用于创建一个新的线程,新线程开始执行 start_routine 函数,并将 arg 作为参数传递给该函数。新线程的标识符将存储在 thread 指向的位置。

成功创建线程时,pthread_create 返回 0;否则,它返回一个非零的错误代码,表示创建线程失败。

示例用法:

#include <pthread.h>
#include <stdio.h>

void* myThreadFunction(void* arg) {
    // 该函数将在新线程中执行
    printf("Hello from the new thread!\n");
    return NULL;
}

int main() {
    pthread_t myThread;

    // 创建新线程,指定线程函数为 myThreadFunction
    int result = pthread_create(&myThread, NULL, myThreadFunction, NULL);

    if (result != 0) {
        perror("Thread creation failed");
        return 1;
    }

    // 主线程继续执行其他任务

    // 等待新线程结束
    pthread_join(myThread, NULL);

    return 0;
}

在这个示例中,pthread_create 函数用于创建一个新线程,新线程将执行 myThreadFunction 函数。主线程继续执行其他任务,然后通过 pthread_join 等待新线程结束。

注:clone: 在 Linux 中用于创建新进程或线程。Pthreads 库使用 clone 系统调用来实现线程创

POSIX 线程库

POSIX(Portable Operating System Interface,可移植操作系统接口)线程库是一组用于多线程编程的标准接口,定义了在不同操作系统上编写可移植多线程应用程序的规范。POSIX 线程库的正式名称是 Pthreads(POSIX threads),它提供了一套标准的线程操作和同步原语,允许开发者编写在支持 POSIX 标准的操作系统上可移植的多线程程序。

以下是一些 POSIX 线程库的关键概念和特点:

  1. 线程创建: 提供了创建和管理线程的函数,例如 pthread_create。
  2. 线程同步: 包含了同步原语,如互斥锁(Mutex)、条件变量(Condition Variable)、读写锁(Read-Write Lock)等,以确保线程之间的协同工作。
  3. 线程控制: 提供了一系列用于线程控制的函数,如线程的取消和退出。
  4. 线程安全: 确保多线程程序能够在共享资源时避免数据竞争和其他并发问题。

POSIX 线程库的设计目标是提供一个标准的、可移植的多线程编程接口,使得开发者能够在不同的操作系统上编写相同的多线程代码,而不需要担心底层操作系统的差异。因此,支持 POSIX 标准的操作系统,如 Linux、Unix、macOS 等,通常都提供了对 Pthreads 的支持。

在使用 POSIX 线程库时,开发者可以通过链接 -pthread 或 -lpthread 这样的标志来告知编译器和链接器使用 Pthreads 库。

2.Linux进程VS线程

进程是资源分配的基本单位

线程是调度的基本单位

线程共享进程数据,但也拥有自己的一部分数据:

进程(Process)和线程(Thread)是操作系统中用于执行程序的两个基本概念,它们之间有一些关键的区别:

  1. 定义:
    • 进程是程序的一次执行过程,是一个独立的、独立地址空间的执行环境。在操作系统中,一个进程可以包含多个线程。
    • 线程是进程的一个执行流,是进程内的一个独立执行单元。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间和文件句柄。
  1. 资源分配:
    • 进程拥有独立的内存空间,一个进程的数据不能直接被另一个进程访问。
    • 线程共享相同的地址空间,可以直接访问进程的数据。
  1. 通信与同步:
    • 进程间通信相对复杂,需要采用特殊的通信机制(例如管道、消息队列、共享内存等)。
    • 线程间通信相对容易,因为它们共享相同的地址空间,可以通过共享内存等简单的方式进行通信。
  1. 独立性:
    • 进程是独立的,一个进程的崩溃通常不会影响其他进程。
    • 线程是进程的一部分,一个线程的崩溃可能会导致整个进程的崩溃,因为它们共享相同的资源。
  1. 创建和销毁开销:
    • 创建和销毁进程的开销相对较大,因为它需要分配和释放独立的内存空间、建立和维护进程控制块等。
    • 创建和销毁线程的开销相对较小,因为它们共享进程的资源,只需要创建和销毁线程控制块即可。
  1. 并发性:
    • 进程可以并发执行,不同进程之间相互独立。
    • 线程在同一进程内并发执行,共享进程的资源,需要通过同步机制来保护共享数据。

总的来说,进程和线程是操作系统中用于执行程序的两种基本方式,它们在资源分配、通信、独立性等方面有明显的区别,而线程则是进程内的执行流,提供了一种更轻量级的并发模型。选择使用进程还是线程取决于具体的应用场景和需求。

那么那些是线程私有的呢?

线程ID

一组寄存器

errno

信号屏蔽字

调度优先级

其中最重要的两个:

  1. 线程私有栈(Thread-Specific Stack)

每个线程有自己的栈空间。栈用于存储局部变量、函数参数、函数返回地址以及执行上下文等信息。这使得每个线程可以有自己的函数调用栈,而不会与其他线程相互干扰。

  1. 寄存器上下文(Register Context)

每个线程在执行时会保存自己的寄存器状态。这包括程序计数器(Program Counter)和其他寄存器值。当线程切换时,操作系统会保存当前线程的寄存器状态,并恢复另一个线程的寄存器状态。

尽管线程共享进程的地址空间和大部分资源,但上述资源对于每个线程都是独立的,允许每个线程拥有自己的执行环境。

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

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

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

3.关于进程线程的问题

pthread_join 函数

pthread_join 函数是用于等待一个指定的线程结束执行的 POSIX 线程库函数。当一个线程通过 pthread_create 创建后,可以使用 pthread_join 来等待该线程的结束,并获取其返回值。

函数原型如下:

int pthread_join(pthread_t thread, void **retval);
//retval:也可以用来接收 return 的返回值

参数说明:

  • thread:要等待的目标线程的标识符(ID)。
  • retval:用于存储目标线程的返回值的指针。如果不关心返回值,可以将其设置为 NULL。

函数返回值:

  • 成功时返回 0。
  • 失败时返回一个非零的错误码。

使用 pthread_join 的基本流程如下:

  1. 创建一个线程,并获取其线程标识符(pthread_t 类型)。
  2. 在主线程中使用 pthread_join 函数等待目标线程的结束。
  3. 如果需要获取等待的目标子线程的返回值,可以通过 retval 参数获得。

以下是一个简单的例子,演示了 pthread_join 的用法:

#include <pthread.h>
#include <stdio.h>

void *thread_function(void *arg) {
    // Thread code
    int *result = malloc(sizeof(int));
    *result = 42;
    pthread_exit(result);
}

int main() {
    pthread_t thread_id;
    int *result;

    // 创建线程
    pthread_create(&thread_id, NULL, thread_function, NULL);

    // 等待线程结束,并获取返回值
    pthread_join(thread_id, (void **)&result);

    // 输出返回值
    printf("Thread returned: %d\n", *result);

    // 释放返回值的内存
    free(result);

    return 0;
}

在上述例子中,pthread_join 用于等待 thread_id 标识的线程结束,并通过 result 获取了线程的返回值。这里假设线程的返回值是一个动态分配的整数。

pthread_exit 函数

pthread_exit 函数是用于终止调用它的线程的 POSIX 线程库函数。它可以用来返回一个值,该值将成为线程的返回值,并且可以被其他线程通过 pthread_join 函数获取。

函数原型如下:

void pthread_exit(void *retval);

参数说明:

  • retval:线程的返回值,可以是一个指针。这个值将传递给等待该线程结束的其他线程。

pthread_exit 通常用于在线程的执行过程中提前终止线程,并指定一个返回值。在线程执行的最后,当线程返回时,也会隐式地调用 pthread_exit。

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


using namespace std;

#define NUM 10

enum{
    OK = 0,
    ERROR
};
class ThreadDate
{
public:
    ThreadDate(const string &name, int id, time_t createTime, int top)
    :_name(name),_id(id),_createTime((uint64_t)createTime),_status(OK),_top(top),_result(0)
    {}
    ~ThreadDate()
    {}
public:
    //输入的
    string _name;
    int _id;
    uint64_t _createTime;
    //返回的
    int _status;
    int _top;
    int _result;
};

class ResultDate
{

};

//线程终止
//1.线程函数执行完毕
//2.
void* thread_run(void* args)
{
    //char* name = (char*)args;
    ThreadDate* td = static_cast<ThreadDate*>(args);

    for(int i = 1; i <= td->_top; i++)
    {
        td->_result += i;
    }
    cout << td->_name << " cal done!" << endl;
    
    pthread_exit(td);
    //return td;//这两种写法都可以传给pthread_join的第二个参数

    // while (true)
    // {
    //     sleep(1);
    //     cout << "thread is running, name: " << td->_name << " create time: "<< td->_createTime << " index: "<< td->_id << endl;
    //     //cout << "new thread running, my thread name is: " << name << endl;
    //     //exit(10);//exit:进程退出,不是线程退出!只要有任何一个线程调用exit,整个进程(所以线程)全部退出
    //     break;
    // }

    // delete td;
    // pthread_exit((void*)1);//把1强转为地址类型,避免报错
    // //return nullptr;
}

int main()
{
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; i++)
    {
        //char* tname = new char[64];
        char tname[64];
        snprintf(tname, 64, "thread-%d", i+ 1);
        ThreadDate *td = new ThreadDate(tname, i + 1, time(nullptr), 100 + 5 * i);
        pthread_create(tids + i, nullptr, thread_run, td);
        sleep(1);
    }

    void* ret = nullptr;

    for(int i = 0; i < NUM; i++)
    {
        int n = pthread_join(tids[i], &ret);//它的等待方式默认是阻塞等待
        if(n != 0)
            cerr << "pthread_join error" << endl;

        ThreadDate *td = static_cast<ThreadDate*>(ret);
        if(td->_status == OK)
        {
            cout << td->_name << " 计算的结果是:" << td->_result << " (它要计算的是[1, " << td->_top << "])"<< endl;
        }

        delete td;
    }

    cout << "qll thread quit..." << endl;

    //sleep(2);

    // while(true)
    // {
    //     cout << "main thread running, new thread id: " << endl;
    //     sleep(1);
    // }

    return 0;
}

pthread_cancel 函数

pthread_cancel 函数是 POSIX 线程库中的函数,用于请求取消指定线程的执行。这个函数允许一个线程请求另一个线程的取消。请注意,这只是一个请求,被请求的线程可以选择在适当的时机取消自己的执行。

函数原型如下:

#include <pthread.h>

int pthread_cancel(pthread_t thread);

参数说明:

  • thread:要取消的线程的标识符(ID)。

函数返回值:

  • 成功时返回 0。
  • 失败时返回一个非零的错误码。

使用 pthread_cancel 函数时需要注意以下几点:

  1. 取消类型:线程的取消可以是异步的或者推迟的。异步取消意味着请求线程立即取消目标线程,而推迟取消则表示请求线程只是向目标线程发出取消请求,由目标线程在适当的时候自行取消。取消类型通过设置线程属性来指定,使用 pthread_setcanceltype 函数。

  2. 取消点:在目标线程的执行中,需要存在取消点(cancellation points),这些是线程可以响应取消请求的点。通常,系统调用是取消点,因为在这些调用中线程会阻塞,可以响应取消请求。

  3. 取消状态:线程可以设置取消状态,包括启用或禁用取消。使用 pthread_setcancelstate 函数来设置。

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

    using namespace std;

    void* thread_run(void* args)
    {
    const char* name = static_cast<const char*>(args);

     int cnt = 5;
     while(cnt)
     {
         cout << name << "is running: " << cnt-- << endl;
         sleep(1);
     }
    
     pthread_exit((void*)11);
    

    }

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

     pthread_cancel(tid);//如果调用了线程取消,线程返回值(ret)会被设置为 -1
    
     void* ret = nullptr;
     pthread_join(tid, &ret);
     cout << "new thread quit: " << (int64_t)ret << endl; // 输出线程退出码 --- -1
    
     return 0;
    

    }

pthread_self 函数

pthread_self 函数是 POSIX 线程库中的函数,用于获取调用线程的线程标识符(ID)。线程标识符是一个 pthread_t 类型的变量,用于唯一标识一个线程。

函数原型如下:

#include <pthread.h>

pthread_t pthread_self(void);

函数返回值:

  • 返回调用线程的线程标识符(ID)。

    using namespace std;

    void* thread_run(void* args)
    {
    const char* name = static_cast<const char*>(args);

      int cnt = 5;
      while(cnt)
      {
          cout << name << "is running: " << cnt-- << " obtain self id: " << pthread_self() << endl;
          sleep(1);
      }
    
      pthread_exit((void*)11);
    

    }

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

      void* ret = nullptr;
      pthread_join(tid, &ret);//它的等待方式默认是阻塞等待
      cout << "new thread quit: " << (int64_t)ret << " quit thread: " << tid <<endl;
    
      return 0;
    

    }

pthread_detach函数

在 POSIX 线程库中,pthread_detach 函数用于将线程的退出状态分离,使得线程在结束时可以立即释放其资源,而无需其他线程调用 pthread_join 来获取其退出状态。

函数原型如下:

#include <pthread.h>

int pthread_detach(pthread_t thread);

参数说明:

  • thread:要分离的线程的标识符(ID)。

函数返回值:

  • 成功时返回 0。
  • 失败时返回一个非零的错误码。

用 pthread_detach 将线程分离。分离后,子线程在结束时会自动释放其资源,而无需主线程调用 pthread_join 去等待该子线程。注意:一旦该子线程被分离,就无法对其使用 pthread_join ,如果继续使用 join 函数就会报错。

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

using namespace std;

void* threadRoutine(void* args)
{
    //pthread_detach(pthread_self());//不推荐线程自己执行分离
    string name =static_cast<const char*>(args);
    int cnt = 5;
    while(cnt)
    {
        cout << name << " : " << cnt-- << endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");//线程被创建的时候,谁先执行并不确定
    //sleep(1);

    pthread_detach(tid);

    int n = pthread_join(tid, nullptr);
    if(0 != n)
    {
        cerr << "error: " << n << " : " << strerror(n) << endl;
    }

    //sleep(5);
    return 0;
}

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

1.pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程(LWP)ID不是一回事。

2.前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。

3.pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

4.线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

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

using namespace std;

string hexAddr(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);

    return buffer;
}

void* threadRoutine(void* args)
{
    string name =static_cast<const char*>(args);
    int cnt = 5;
    while(cnt)
    {
        cout << name << " : " << cnt-- << " : " << hexAddr(pthread_self()) << endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");//线程被创建的时候,谁先执行并不确定

    while(true)
    {
        cout << " main thread: " << hexAddr(pthread_self()) << " new thread id: "<< hexAddr(tid) << endl;
        sleep(1);
    }

    int n = pthread_join(tid, nullptr);
    if(0 != n)
    {
        cerr << "error: " << n << " : " << strerror(n) << endl;
    }

    return 0;
}

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

__thread int g_val = 100;//全局变量

__thread 关键字,它是一种线程局部存储(TLS)的方式。在这个例子中,g_val 被声明为一个线程局部变量(也就是从一个全局变量变为了局部变量),每个线程都有一份独立的拷贝,互不影响。

具体解释如下:

__thread 是一个存储类说明符,用于声明线程局部存储变量。它告诉编译器,每个线程都有自己独立的变量实例。

pstack指令查看 进程/线程 的调用栈信息指令

pstack 是一个用于显示进程的调用栈信息的命令。它通常用于分析进程中的线程调用栈,以帮助诊断问题和性能优化。

在大多数Unix/Linux系统上,pstack 命令通常与操作系统的调试工具一起提供,例如在GDB(GNU Debugger)或其他调试器不可用的情况下,pstack 可以用于获取进程的堆栈信息。

使用方法如下:

pstack <pid>

其中 <pid> 是目标进程的进程ID。

例如,如果你有一个运行中的进程,其进程ID为12345,你可以通过以下方式使用 pstack:

pstack 12345

这将显示与进程相关的每个线程的调用栈信息。 pstack 通常用于诊断应用程序中的死锁、内存泄漏或性能问题。请注意,对于某些系统和平台,pstack 可能需要安装或存在额外的调试信息。

多线程和多进程创建的失败的区别

在多线程和多进程编程中,创建失败的情况可能会有不同的表现和影响,主要取决于失败的原因和所处的环境。以下是两者之间可能的区别:

  1. 资源分配失败
    • 在多线程编程中,创建线程失败通常是由于内存不足或者系统资源限制导致的。这种情况下,通常会收到类似于内存分配失败或者资源耗尽的错误。
    • 在多进程编程中,创建进程失败可能是由于系统资源限制(如可用的进程数达到了系统限制)或者权限不足(如试图在没有足够权限的情况下创建进程)导致的。这种情况下,通常会收到类似于权限被拒绝或者资源不足的错误。

因为线程的资源是进程分配的给每个线程的,如果线程创建失败可以通过进程捕捉信号行处理,而进程创建失败则会直接导致程序崩溃, 因为创建失败可能涉及到系统资源的限制或者权限问题。

  1. 错误处理和反馈
    • 在多线程环境中,通常可以通过捕获异常或者检查返回值来处理线程创建失败的情况。这样可以允许程序在创建失败时进行适当的错误处理,比如回退或者重新尝试。
    • 在多进程环境中,创建进程失败可能会导致程序立即退出,因为进程创建失败可能会被视为一个严重的系统错误。在某些情况下,系统可能会向进程发送信号或者返回错误码,但这并不总是保证。
  1. 影响范围
    • 在多线程环境中,单个线程的创建失败通常不会影响整个应用程序的稳定性,因为其他线程仍然可以继续执行。但是,如果多个线程创建失败,可能会影响到程序的功能性和性能。
    • 在多进程环境中,进程创建失败可能会对整个应用程序的功能性产生严重影响,因为一个进程的失败可能会导致整个应用程序的某些模块不可用。

4.Linux程序互斥

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

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

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

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

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

互斥量mutex:

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

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

多个线程并发的操作共享变量,会带来一些问题,比如数据互相覆盖等,进而导致数据不一致!

**互斥量(Mutex)**是一种同步机制,用于保护共享资源,防止多个线程同时访问和修改这些资源,从而避免数据竞争和一致性问题。互斥量提供了一种独占锁的机制,一个线程成功获取互斥锁后,其他线程就不能再获取,直到该线程释放锁。

在多线程编程中,互斥量通常包括两种操作:锁定(Lock)和解锁(Unlock)。

pthread_mutex_t类型

pthread_mutex_t 是 POSIX 线程库中表示互斥锁(Mutex)的数据类型。它是一种结构体,用于存储互斥锁的信息和状态。在使用互斥锁时,通常需要先声明一个 pthread_mutex_t 类型的变量,然后对其进行初始化,最后在程序中使用这个变量进行互斥锁的操作。

以下是一般的 pthread_mutex_t 的定义和初始化方式:

初始化互斥量(互斥锁类型):

初始化互斥锁时,可以使用函数进行初始化,也可以使用宏进行初始化。选择使用哪种方式通常取决于初始化的灵活性和具体的需求。

  1. 使用宏初始化:
vb 复制代码
`pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//注:如果定义的是全局或者静态的互斥锁可以使用宏进行初始化,并且可以不调用
//pthread_mutex_destroy()函数进行销毁全局和静态的互斥锁在程序的整个生命周
//期内都存在,因此在程序结束时,它们会被自动销毁。`

使用宏进行初始化是一种简单且方便的方式。它在声明的同时完成了初始化,适用于那些在整个生命周期中不需要修改互斥锁属性的情况。这样的初始化适用于大多数基本的互斥锁使用场景。

  1. 使用函数初始化:

    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
    //注:使用函数初始化,不管互斥锁定义在哪里都可以使用

使用函数进行初始化提供了更多的灵活性,允许你在初始化时设置互斥锁的属性。第二个参数 attr 是一个指向互斥锁属性的指针,可以通过配置这个属性来实现更高级的控制,例如设置互斥锁的类型、进程共享等。

区别:

  • 宏初始化的优势:
    • 简洁: 使用宏初始化更为简洁,适用于大多数情况。
    • 适用于默认情况: 如果不需要设置额外的属性,宏初始化足够满足需求。
  • 函数初始化的优势:
    • 灵活性: 使用函数初始化可以在初始化时设置更多的属性,提供更灵活的控制。
    • 适用于动态属性变化: 如果互斥锁的属性需要在运行时动态变化,使用函数初始化更为合适。
    • 属性配置: 使用函数初始化可以通过 pthread_mutexattr_t 结构体来配置更多的属性信息。

在实际应用中,一般而言,如果使用默认属性且不需要在运行时更改互斥锁的属性,宏初始化是足够的。而如果需要更多的控制和灵活性,或者需要在运行时更改属性,就可以选择使用函数进行初始化。

pthread_mutex_lock() 函数

pthread_mutex_lock 函数是 POSIX 线程库中用于锁定(获取)互斥量的函数。它的原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex:指向要锁定的互斥量的指针。

该函数用于在进入临界区之前锁定互斥量。如果互斥量已经被其他线程锁定,那么调用线程将会被阻塞,直到互斥量可用。一旦线程成功锁定互斥量,它就可以进入临界区,执行相应的操作。

锁定互斥量:

pthread_mutex_lock(&mutex);
// 临界区代码,对共享资源的访问和修改在这里进行

pthread_mutex_unlock() 函数

pthread_mutex_unlock 函数是 POSIX 线程库中用于解锁互斥量(mutex)的函数。它的原型如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • mutex:指向要解锁的互斥量的指针。

pthread_mutex_unlock 函数用于释放由当前线程持有的互斥量,允许其他线程访问由该互斥量保护的共享资源。如果当前线程没有持有该互斥量,或者该互斥量没有被锁定,pthread_mutex_unlock 的行为将是未定义的。

解锁互斥量:

pthread_mutex_unlock(&mutex);

在上述代码中,pthread_mutex_lock 用于锁定互斥量,如果互斥量已经被其他线程锁定,当前线程会被阻塞,直到互斥量可用。pthread_mutex_unlock 用于解锁互斥量,释放锁,使其他线程可以锁定该互斥量。

互斥量的使用有助于确保对共享资源的安全访问,防止多个线程之间发生竞争条件。然而,要小心避免死锁(Deadlock)和饥饿(Starvation)等问题,正确地设计和使用互斥量是多线程编程中的关键之一。

pthread_mutex_init() 函数

pthread_mutex_init 函数是 POSIX 线程库中用于初始化互斥量的函数。它的原型如下:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex:指向要初始化的互斥量的指针。
  • attr:指向互斥量属性的指针,通常设置为 NULL 表示使用默认属性。

该函数用于动态地初始化一个互斥量,为其分配必要的资源并设置初始状态。如果不再需要互斥量,通常需要在程序退出前调用 pthread_mutex_destroy 函数来释放相关资源。

如果函数成功初始化了互斥量,返回值为0;否则,返回值是一个表示错误的正整数。

pthread_mutex_destroy()函数

pthread_mutex_destroy 函数是 POSIX 线程库中用于销毁互斥量的函数。它的原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • mutex:指向要销毁的互斥量的指针。

该函数用于释放互斥量占用的资源,并将互斥量恢复到初始状态。在调用这个函数之前,确保互斥量已经不再使用。如果在销毁之前还有线程在等待这个互斥量,行为是未定义的。

以下是一个简单的例子:

#include <pthread.h>
#include <stdio.h>

int main() {
    // 定义并初始化互斥量
    pthread_mutex_t myMutex;
    if (pthread_mutex_init(&myMutex, NULL) != 0) {
        perror("Mutex initialization failed");
        return 1;
    }

    // 使用互斥量进行一些操作...

    // 销毁互斥量
    if (pthread_mutex_destroy(&myMutex) != 0) {
        perror("Mutex destruction failed");
        return 1;
    }

    return 0;
}

在这个例子中,pthread_mutex_init 初始化了一个互斥量,然后程序在使用完互斥量后,确保在不再需要互斥量时,调用 pthread_mutex_destroy 函数来销毁它,以防止资源泄漏。

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

using namespace std;

//细节
//1.凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,这个是一个游戏规则,不能有例外
//2.每一个线程访问临界区之前,得加锁,加锁本质是给 临界区 加锁,加锁的粒度尽量要 细 一些(不要给多余的代码进行加锁,尽量给临界区里的代码进行加锁)
//3.线程访问临界区的时候,需要先加锁->所有线程都必须要先看到同一把锁 -> 锁本身就是公共资源->锁如何保证自己的安全? -> 加锁和解锁本身就是原子的!
//4.临界区可以是一行代码,也可以是一批代码
//a.加锁后,线程可能被切换吗?当然可能,不要特殊化加锁和解锁,还有临界区代码。
//b.切换会有影响吗?不会,因为在我不在期间,任何人都没有办法进入临界区,因为谁也无法成功的申请到锁!因为锁被我拿走了
//5.这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态就是:锁被我申请(持有锁),锁被我释放了(不持有锁),原子性就体现在这里
//6.解锁的过程也被设计成为原子的!


//临界资源
int tickets = 10000;
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//全局锁,互斥量类型(互斥锁),用来对共享资源加锁,保证共享资源的安全

//局部锁的使用, 通过封装保证所有线程访问的是同一把锁
class TData
{
public:
    TData(const string &name, pthread_mutex_t *mutex)
    :_name(name), _pmutex(mutex)
    {}
    ~TData()
    {}

public:
    string _name;
    pthread_mutex_t *_pmutex;
};


void* threadRoutine(void* args)
{
    TData* td = static_cast<TData*>(args);
    while(true)
    {
        pthread_mutex_lock(td->_pmutex);//加锁,只有持有锁的线程才能执行后续代码,没有锁的线程会被阻塞在这里
        if(tickets > 0)
        {
            //usleep(2000);//模拟抢票花费的时间
            cout << td->_name << " get a ticket: " << tickets-- << endl;//临界区
            pthread_mutex_unlock(td->_pmutex);
        }
        else
        {
            pthread_mutex_unlock(td->_pmutex);//if只执行一个分支,所以else也进行解锁
            break;
        }
        usleep(1000);//充当抢完一张票的后续动作,例如,把数据写入内存等
        //不仅仅是为了充当后续动作,也是为了防止一个线程刚解锁,又拿到锁,导致其他执行无法获取到锁
    }

    return nullptr;
}

int main()
{
    pthread_mutex_t mutex;//局部锁
    pthread_mutex_init(&mutex, nullptr);

    pthread_t tids[4];
    int n = sizeof(tids) / sizeof(tids[0]);
    for(int i = 0; i < n; i++)
    {
        char name[64];
        snprintf(name, 64, "thread-%d", i + 1);
        TData* td = new TData(name, &mutex);//保证所有线程看见的是同一把锁
        pthread_create(tids+i, nullptr, threadRoutine, td);
    }

    for(int i = 0; i < n; i++)
    {
        pthread_join(tids[i], nullptr);
    }

    pthread_mutex_destroy(&mutex);

    return 0;
}

互斥锁的实现原理:

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

C++线程的封装

//Thread.hpp
#pragma once

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

using namespace std;


class Thread
{
public:
    typedef enum
    {
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
    typedef void (*func_t)(void*);

public:
    Thread(int num, func_t func, void *args) : _tid(0), _status(NEW), _func(func), _args(args)
    {
        char name[128];
        snprintf(name, sizeof(name), "thread-%d", num);
        _name = name;
    }

    int status() { return _status; }//获取线程状态

    std::string threadname() { return _name; }//获取线程名字

    pthread_t threadid()//获取线程ID
    {
        if (_status == RUNNING)
            return _tid;
        else
        {
            return 0;
        }
    }

    // runHelper是不是类的成员函数,而类的成员函数,具有默认参数this,需要static
    // 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数
    static void *runHelper(void *args)
    {
        Thread *ts = (Thread*)args; // 就拿到了当前对象
        // _func(_args);
        (*ts)();
        return nullptr;
    }

     void operator ()() //仿函数
    {
        if(_func != nullptr) 
            _func(_args);
    }

    void run()
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);
        if(n != 0) exit(1);
        _status = RUNNING;
    }

    void join()
    {
        int n = pthread_join(_tid, nullptr);
        if( n != 0)
        {
            std::cerr << "main thread join thread " << _name << " error" << std::endl;
            return;
        }
        _status = EXITED;
    }

    ~Thread()
    {}

private:
    pthread_t _tid;
    std::string _name;
    func_t _func; // 线程未来要执行的回调
    void* _args;
    ThreadStatus _status;
};

//mythread.cc
#include "Thread.hpp"

void threadRun(void* args)
{
    string message = static_cast<const char*>(args);
    int cnt = 10;
    while(cnt)
    {
        cout << "我是一个线程," << message << ", cnt: " << cnt-- << endl;
        sleep(1);
    }
}

int main()
{
    Thread t1(1, threadRun, (void*)"hellobit");
    cout << "thread name: " << t1.threadname() << " thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;
    t1.run();
    cout << "thread name: " << t1.threadname() << " thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;
    t1.join();
    cout << "thread name: " << t1.threadname() << " thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;

    return 0;
}

锁的封装

//lockGuard.hpp
#pragma once

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

class Mutex // 自己不维护锁,有外部传入
{
public:
    Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmutex);//加锁
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmutex);//解锁
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *_pmutex;
};

class LockGuard // 自己不维护锁,有外部传入
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        _mutex.lock();//通过构造函数自动加锁,防止忘记加锁
    }
    ~LockGuard()
    {
        _mutex.unlock();//通过析构函数自动解锁,防止忘记解锁
    }
private:
    Mutex _mutex;
};

//mythread.cc
#include "lockGuard.hpp"
#include "Thread.hpp"

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//全局锁,互斥量类型(互斥锁),用来对共享资源加锁,保证共享资源的安全

void threadRoutine(void* args)
{
    string message = static_cast<const char*>(args);
    while(true)
    {
        LockGuard lockguard(&mutex);//通过封装的这把锁,进入该代码块作用域自动加锁,出该代码块作用域自动解锁
        if(tickets > 0)
        {
            usleep(2000);//模拟抢票花费的时间
            cout << message << " get a ticket: " << tickets-- << endl;//临界区
        }
        else
        {
            break;
        }
        
        //usleep(1000);//充当抢完一张票的后续动作,例如,把数据写入内存等
        //不仅仅是为了充当后续动作,也是为了防止一个线程刚解锁,又拿到锁,导致其他执行无法获取到锁
    }
}

int main()
{
    Thread t1(1, threadRoutine, (void*)"hello world1");
    Thread t2(2, threadRoutine, (void*)"hello world1");
    Thread t3(3, threadRoutine, (void*)"hello world1");
    Thread t4(4, threadRoutine, (void*)"hello world1");

    t1.run();
    t2.run();
    t3.run();
    t4.run();

    t1.join();
    t2.join();
    t3.join();
    t4.join();
    
    return 0;
}

可重入VS线程安全

概念:

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

常见的线程不安全的情况:

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

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

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

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

常见的线程安全的情况

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

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

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

常见不可重入的情况

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

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

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

常见可重入的情况

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

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

不调用不可重入函数。

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

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

可重入与线程安全联系

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

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

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

可重入与线程安全区别

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

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

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

5.常见死锁概念

死锁

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

死锁四个必要条件

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

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

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

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

避免死锁的四个必要条件

1.不加锁

2.主动释放锁

3.按照顺序申请锁

4.控制线程统一释放锁(如:通过其他线程进行解锁)

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

using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* threadRoutine(void* args)
{
    cout << "I am a new thread " << endl;

    pthread_mutex_lock(&mutex);
    cout << "I got a mutex!" << endl;

    pthread_mutex_lock(&mutex);//申请锁的问题,它会停下来,因为锁在之前已经被申请了
    cout << "I alive again" << endl;

    return nullptr;
}

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

    sleep(3);
    cout << "main thread run begin" << endl;
    pthread_mutex_unlock(&mutex);
    cout << "main thread unlock..." << endl;

    sleep(3);
    return 0;
}

6.Linux线程同步

线程同步是指多个线程按照一定的顺序执行,以确保它们在访问共享资源时不会发生冲突或产生不一致的结果。在多线程环境中,多个线程同时运行,可能会访问和修改共享的数据,如果没有适当的同步机制,就可能导致数据竞争和程序错误。

线程同步的目标是协调线程之间的执行,以确保它们按照预期的顺序访问共享资源,从而避免潜在的问题,比如:

  1. 数据竞争(Race Condition): 多个线程同时访问共享数据,其中一个线程的操作可能会影响到其他线程,导致数据的不一致性。
  2. 死锁(Deadlock): 多个线程因为互相等待对方释放资源而无法继续执行,导致程序停滞。
  3. 饥饿(Starvation): 一个或多个线程由于竞争资源不公平而无法获得执行的机会。

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了(也就是说一个线程在访问一个资源时,资源用完了,判断失败并解锁,然后又重新加锁并访问资源,也就是一直处于加锁和解锁的状态中,直到这个资源被补充,最后条件满足拿到资源为止)。

while(true)
{
	pthread_mutex_lock(td->_pmutex);
    if(tickets > 0)//tickets假设后台会定期补充资源
	{
 		//usleep(2000);//模拟抢票花费的时间
		cout << td->_name << " get a ticket: " << tickets-- << endl;//临界区
		pthread_mutex_unlock(td->_pmutex);
		break;
	}
	else
	{
		pthread_mutex_unlock(td->_pmutex);//if只执行一个分支,所以else也进行解锁
	}
	usleep(1000);//充当抢完一张票的后续动作,例如,把数据写入内存等
	//不仅仅是为了充当后续动作,也是为了防止一个线程刚解锁,又拿到锁,导致其他执行无法获取到锁
}

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

同步概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问

题,叫做同步。

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

pthread_cond_t类型

pthread_cond_t 是 POSIX 线程库中用于条件变量操作的数据类型。条件变量允许线程在某个特定条件下进行等待(或者被唤醒),它通常与互斥锁一起使用,用于实现线程间的同步。

特性和功能:

  1. 等待和通知: 条件变量允许线程等待某个特定条件的发生,同时允许其他线程通知条件的发生或变化。
  2. 与互斥锁配合使用: 通常和互斥锁一起使用,用于在等待某个条件时解锁互斥锁(以允许其他线程修改共享数据),并在条件满足时重新获取互斥锁。
  3. 线程间通信: 通过条件变量,线程可以在共享资源满足特定条件时进行等待,从而有效地进行线程间的通信和同步。

基本操作函数:

  • pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr): 初始化条件变量。
  • pthread_cond_destroy(pthread_cond_t *cond): 销毁条件变量。
  • pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex): 线程在等待条件变量时释放互斥锁,并等待条件发生。
  • pthread_cond_signal(pthread_cond_t *cond): 通知等待该条件变量的某一个线程。
  • pthread_cond_broadcast(pthread_cond_t *cond): 通知等待该条件变量的所有线程。

注:以上的几个函数的返回值都是成功返回0,失败返回错误码。可以通过检查返回值来判断函数是否成功执行。如果返回0,表示操作成功;否则,可以根据返回的错误码进行错误处理。请注意,这里提到的返回值是针对 POSIX 线程库的标准,具体实现可能会有一些细微的差异。

使用方法:

  1. 初始化条件变量:使用 pthread_cond_init 函数进行初始化。
  2. 等待条件:使用 pthread_cond_wait 等待条件变量的发生。
  3. 发出通知:使用 pthread_cond_signal 或 pthread_cond_broadcast 发出通知。
  4. 销毁条件变量:在不需要时使用 pthread_cond_destroy 进行销毁。

注意事项:

  • 条件变量必须与互斥锁一起使用,通常在等待条件变量之前需要先获取互斥锁,以确保对共享资源的安全访问。
  • 等待条件时,线程会自动释放互斥锁,并在条件满足或者等待取消时重新获取互斥锁,这有助于避免死锁。

条件变量是实现线程间同步和通信的重要机制,使用它们可以有效地管理线程的等待和唤醒,确保多线程程序的正确性和性能。

pthread_cond_init()函数

pthread_cond_init 函数是 POSIX 线程库中用于初始化条件变量的函数。条件变量通常与互斥锁一起使用,用于在线程之间同步共享资源的访问。以下是关于 pthread_cond_init 函数的详细信息:

函数原型:

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

参数:

  • cond:指向条件变量的指针,用于存储初始化后的条件变量。
  • attr:指向条件变量属性的指针。通常可以设置为 NULL,表示使用默认的条件变量属性。

函数功能:

  • pthread_cond_init 函数用于初始化条件变量 cond。一旦条件变量被初始化,它可以与其他线程共享,并用于线程间的同步。

注:全局的条件变量类型的初始化和全局的互斥锁一样都可以使用宏进行初始化,并且可以不调用 pthread_cond_destroy ()函数进行销毁,全局和静态的条件变量在程序的整个生命周期内都存在,因此在程序结束时,它们会被自动销毁。

注意事项:

  • 在使用条件变量之前,必须先调用 pthread_cond_init 进行初始化。
  • 通常,条件变量的初始化与互斥锁结合使用。在初始化之后,可以通过 pthread_cond_wait、pthread_cond_signal 和 pthread_cond_broadcast 等函数来实现线程之间的等待和通知机制。
  • 如果不再需要条件变量,通常应使用 pthread_cond_destroy 函数进行清理。

示例:

#include <pthread.h>

//pthread_cond_t my_condition = PTHREAD_COND_INITIALIZER;//使用宏初始化

int main() {
	pthread_cond_t my_condition;//通过pthread_cond_init函数进行初始化
    
    // 使用条件变量前的初始化
    pthread_cond_init(&my_condition, NULL);

    // 在这里可以使用条件变量进行同步操作

    // 不再需要条件变量时的清理
    pthread_cond_destroy(&my_condition);

    return 0;
}

上述示例中,my_condition 是一个条件变量,通过 pthread_cond_init 进行初始化。在使用完毕后,可以使用 pthread_cond_destroy 进行清理。条件变量的具体用法通常涉及到和互斥锁一起使用,以实现线程之间的同步。

pthread_cond_wait()函数

pthread_cond_wait 函数是 POSIX 线程库中用于等待条件变量的函数。它通常与互斥锁一起使用,用于在线程等待某个特定条件时释放互斥锁,允许其他线程访问共享资源。

函数原型:

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

参数:

  • cond:指向条件变量的指针。
  • mutex:指向互斥锁的指针。在调用 pthread_cond_wait 之前,必须先获得这个互斥锁。

函数功能:

  1. 线程调用 pthread_cond_wait 时,它会将自己放入**条件变量(cond)**的等待队列进行排队,并释放与传入的互斥锁相关联的互斥锁。
  2. 然后,它会阻塞等待,直到其他线程调用 pthread_cond_signal 或 pthread_cond_broadcast 发出通知,并且等待的线程被唤醒。
  3. 一旦被唤醒,线程将重新申请获取之前释放的互斥锁,申请成功才会彻底返回,接着会在之前休眠的位置继续向后执行。在重新获取互斥锁之前,线程处于阻塞状态,不会消耗 CPU 资源。

注意事项:

  • 在调用 pthread_cond_wait 之前,线程必须已经获得与条件变量关联的互斥锁。通常的做法是在调用前使用 pthread_mutex_lock 获取锁,在调用后使用 pthread_mutex_unlock 释放锁(意思就是处于它们中间调用该函数)。

  • pthread_cond_wait 函数在等待之前会自动释放互斥锁,并在等待返回时重新获取互斥锁。这样的机制有助于防止死锁。

  • 为了避免虚假唤醒,即在没有明确通知的情况下线程被唤醒,通常会将 pthread_cond_wait 放在一个循环中,检查条件是否满足。

    pthread_mutex_lock(&mutex);
    while (!condition) {
    pthread_cond_wait(&cond, &mutex);
    }
    // 执行线程的操作
    pthread_mutex_unlock(&mutex);

pthread_cond_wait 在多线程编程中是常用的同步机制之一,它提供了一种有效的方式来等待特定条件的发生,并在条件满足时唤醒等待的线程。

pthread_cond_signal()函数

pthread_cond_signal 函数是 POSIX 线程库中用于向等待某个条件变量的线程发送信号的函数。它用于通知等待在条件变量上的某一个线程,使其从等待状态醒来,继续执行。以下是关于 pthread_cond_signal 函数的详细信息:

函数原型:

int pthread_cond_signal(pthread_cond_t *cond);

参数:

  • cond:指向条件变量的指针,该条件变量上至少有一个线程在等待。

函数功能:

  • pthread_cond_signal 函数用于向等待在条件变量上的某一个线程发送信号,通知其可以继续执行。
  • 该函数通常与互斥锁一起使用,以确保在发送信号之前和之后对共享资源的安全访问。

注意事项:

  • 调用 pthread_cond_signal 之前,通常需要先获取与条件变量关联的互斥锁,以避免竞争条件。
  • pthread_cond_signal 会唤醒等待在条件变量上的至少一个线程,但不保证唤醒哪一个线程。如果需要唤醒所有等待线程,可以使用 pthread_cond_broadcast 函数。
  • 通常的用法是在满足某个条件时调用 pthread_cond_signal,以通知等待在条件变量上的线程。

示例:

#include <pthread.h>

pthread_cond_t my_condition = PTHREAD_COND_INITIALIZER;
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;

// 等待条件的线程
void* wait_thread(void* arg) {
    pthread_mutex_lock(&my_mutex);
    printf("Thread waiting...\n");
    pthread_cond_wait(&my_condition, &my_mutex);
    printf("Thread woke up!\n");
    pthread_mutex_unlock(&my_mutex);
    return NULL;
}

// 发送信号的线程
void* signal_thread(void* arg) {
    // 在满足某个条件时发送信号
    pthread_mutex_lock(&my_mutex);
    printf("Sending signal...\n");
    pthread_cond_signal(&my_condition);
    pthread_mutex_unlock(&my_mutex);
    return NULL;
}

int main() {
    pthread_t wait_t, signal_t;

    // 创建等待条件的线程
    pthread_create(&wait_t, NULL, wait_thread, NULL);

    // 创建发送信号的线程
    pthread_create(&signal_t, NULL, signal_thread, NULL);

    // 等待线程结束
    pthread_join(wait_t, NULL);
    pthread_join(signal_t, NULL);

    return 0;
}

上述示例中,pthread_cond_signal 在 signal_thread 函数中被调用,通知等待在条件变量上的线程(在 wait_thread 中)可以继续执行。需要注意的是,调用 pthread_cond_signal 之前,需要先获取互斥锁,以确保对条件变量和共享资源的安全访问。

pthread_cond_broadcast()函数

pthread_cond_broadcast 函数是 POSIX 线程库中用于向等待某个条件变量的所有线程发送信号的函数。与 pthread_cond_signal 不同的是,pthread_cond_broadcast 会唤醒所有等待在条件变量上的线程,而不仅仅是一个。以下是关于 pthread_cond_broadcast 函数的详细信息:

函数原型:

int pthread_cond_broadcast(pthread_cond_t *cond);

参数:

  • cond:指向条件变量的指针,该条件变量上至少有一个线程在等待。

函数功能:

  • pthread_cond_broadcast 函数用于向等待在条件变量上的所有线程发送信号,通知它们可以继续执行。
  • 与 pthread_cond_signal 不同,pthread_cond_broadcast 会唤醒所有等待的线程,而不仅仅是一个。这通常用于广播一些全局状态的变化,使得多个线程都能够检测到。

注意事项:

  • 调用 pthread_cond_broadcast 之前,通常需要先获取与条件变量关联的互斥锁,以确保在发送信号之前和之后对共享资源的安全访问。
  • 与 pthread_cond_signal 一样,pthread_cond_broadcast 的调用也可能在没有等待线程的情况下执行,而这样的调用不会产生任何效果。

示例:

#include <pthread.h>

pthread_cond_t my_condition = PTHREAD_COND_INITIALIZER;
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;

// 等待条件的线程
void* wait_thread(void* arg) {
    pthread_mutex_lock(&my_mutex);
    printf("Thread waiting...\n");
    pthread_cond_wait(&my_condition, &my_mutex);
    printf("Thread woke up!\n");
    pthread_mutex_unlock(&my_mutex);
    return NULL;
}

// 发送信号的线程
void* signal_thread(void* arg) {
    // 在满足某个条件时发送广播信号
    pthread_mutex_lock(&my_mutex);
    printf("Sending broadcast signal...\n");
    pthread_cond_broadcast(&my_condition);
    pthread_mutex_unlock(&my_mutex);
    return NULL;
}

int main() {
    pthread_t wait_t1, wait_t2, signal_t;

    // 创建等待条件的两个线程
    pthread_create(&wait_t1, NULL, wait_thread, NULL);
    pthread_create(&wait_t2, NULL, wait_thread, NULL);

    // 创建发送广播信号的线程
    pthread_create(&signal_t, NULL, signal_thread, NULL);

    // 等待线程结束
    pthread_join(wait_t1, NULL);
    pthread_join(wait_t2, NULL);
    pthread_join(signal_t, NULL);

    return 0;
}

上述示例中,pthread_cond_broadcast 在 signal_thread 函数中被调用,通知等待在条件变量上的所有线程(在 wait_thread 中)可以继续执行。需要注意的是,调用 pthread_cond_broadcast 之前,需要先获取互斥锁,以确保对条件变量和共享资源的安全访问。

使用上述几个接口进行测试:

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

using namespace std;

const int num = 5;

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* active(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);//pthread_cond_wait, 在调用的时候,会自动释放锁,并且线程会在这里等待进行休眠,直到被唤醒
        cout << name << " 活动" << endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t tids[num];
    for(int i = 0; i < num; i++)
    {
        char* name = new char[32];
        snprintf(name, 32, "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, active, name);
    }

    sleep(3);
    while(true)
    {
        cout << "main thread wakeup thread..." << endl;
        //pthread_cond_signal(&cond);
        pthread_cond_broadcast(&cond);
        sleep(1);
    }

    for(int i = 0; i < num; i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

总结:条件变量(cond),就是允许多线程在cond中进行队列式等待,就是一种顺序排队。

7.生产者 消费者 模型

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

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

生产者消费者模型优点

解耦

支持并发

支持忙闲不均

基于BlockingQueue的生产者消费者模型

BlockingQueue:

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

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

//main.cc
#include "blockQueue.hpp"
#include "task.hpp"
#include <unistd.h>
#include <ctime>

void* consumer(void *args)//消费者
{
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);
    while (true)
    {
        sleep(1);
        Task t;
        // 1. 将数据从blockqueue中获取 -- 获取到了数据
        bq->pop(&t);
        t();
        // 2. 结合某种业务逻辑,处理数据! -- TODO
        cout << pthread_self() << " | consumer Task: " << t.formatArg() << t.formatRes() << endl;
    }
}

void *productor(void *args)//生产者
{
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(args);
    string opers = "+-*/%";
    while (true)
    {
        // 1. 先通过某种渠道获取数据
        int x = rand() % 20 + 1;
        int y = rand() % 10 + 1;
        char op = opers[rand() % opers.size()];
        // 2. 将数据推送到blockqueue -- 完成生产过程
        Task t(x, y, op);
        bq->push(t);
        cout << pthread_self() << " | productor Task: " << t.formatArg() << "?" << endl;
    }
}

int main()
{
    srand((uint64_t)time(nullptr) ^ getpid());
    //BlockQueue<int>* bq = new BlockQueue<int>();
    BlockQueue<Task>* bq = new BlockQueue<Task>();

    // //单生产和单消费
    // pthread_t c, p;
    // pthread_create(&c, nullptr, consumer, bq);
    // pthread_create(&p, nullptr, productor, bq);

    // pthread_join(c, nullptr);
    // pthread_join(p, nullptr);

    //多生产和多消费:生产者和生产者是互斥关系,消费者和消费者也是互斥关系,他们都是共用的一把锁,所以这里是支持多生产和多消费的。
    pthread_t c[2], p[3];
    pthread_create(&c[0], nullptr, consumer, bq);
    pthread_create(&c[1], nullptr, consumer, bq);
    pthread_create(&p[0], nullptr, productor, bq);
    pthread_create(&p[1], nullptr, productor, bq);
    pthread_create(&p[2], nullptr, productor, bq);

    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);

    return 0;
}

//blcokQueue.hpp
#pragma once

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

using namespace std;

const int gcap = 5;

//不要认为,阻塞队列只能放整数字符串之类的,也可以放对象
template <class T>
class BlockQueue
{
public:
    BlockQueue(const int cap = gcap):_cap(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumerCond, nullptr);
        pthread_cond_init(&_productorCond, nullptr);
    }

    bool isFull(){ return _q.size() == _cap; }

    bool isEmpty() { return _q.empty(); }

    void push(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        //细节1:在多线程的情况下尽量使用while进行循环判断,不要使用if单词判断,一定要保证,在任何时候,才
        //进行生产或者消费,因为在多线程的情况下有可能存在 误唤醒 的情况,比如在唤醒的时候使用的是pthread_cond_broadcast 
        //而不是pthread_cond_signal,导致全部线程被唤醒,而生产(空)位置或者消费(空)位置只剩下一个时,就会导致生产者
        //在同一个位置连续生成数据而形成数据覆盖 或者 除了第一个消费者,后续的消费者拿不到数据或者拿到错误的数据
        //细节2:高效不是体现在你生产一个我拿一个,而是体现在生产的数据可以进行提前缓存,当你拿想要数据的时候
        //说不定我已经完成了数据的生成,所以程序从串行变成并行!
        while(isFull())//1.我们只能在临界区内部,判断临界资源是否就绪!注定了我们在当前一定是持有锁的!
        {
            // 2.要让线程进行休眠等待,不能持有锁等待!
            // 3.注定了,pthread_cond_wait要有锁的释放的能力!
            pthread_cond_wait(&_productorCond, &_mutex);//我休眠(切换)了,我醒来的时候,在哪里往后执行呢?
            // 4.当线程醒来的时候,注定了继续从临界区内部继续运行!因为我是在临界区被切走的!
            //5.注定了当线程被唤醒的时候,继续在pthread_cond_wait函数出向后运行,又要重新申请锁,申请成功才会彻底返回,
        }
        //没有满,就让他进行生产
        _q.push(in);
        pthread_cond_signal(&_consumerCond);//有数据时,通知消费者进行消费
        pthread_mutex_unlock(&_mutex);
    }

    void pop(T* out)
    {
        pthread_mutex_lock(&_mutex);//这里和上面说的几点是一样的
        while(isEmpty())//细节1问题
        {
            pthread_cond_wait(&_consumerCond, &_mutex);
        }
        *out = _q.front();
        _q.pop();
        pthread_cond_signal(&_productorCond);//有空位时,通知生产者进行生产
        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_productorCond);
    }
private:
    queue<T> _q;
    int _cap;//队列容量上限
    pthread_mutex_t _mutex;//生产者和消费者访问的是同一个资源只需要一把锁即可,我们生产和消费访问的是同一个queue并且queue被当做整体使用!
    pthread_cond_t _consumerCond;// 消费者对应的条件变量,空,wait,进行等待
    pthread_cond_t _productorCond;// 生产者对应的条件变量,满,wait,进行等待
};

//task.hpp
#pragma once

#include <iostream>
#include <string>

using namespace std;

class Task//Task表示:任务
{
public:
    Task()
    {}
    Task(int x, int y, char op):_x(x), _y(y), _op(op), _result(0), _exitCode(0)
    {}
    int operator()()//仿函数
    {
        switch(_op)
        {
            case '+':
                _result = _x + _y;
                break;
            case '-':
                _result = _x - _y;
                break;
            case '*':
                _result = _x * _y;
                break;
            case '/':
            {
                if(_y == 0)
                    _exitCode = -1;
                else
                    _result = _x / _y;
            }
                break;
            case '%':
            {
                if(_y == 0)
                    _exitCode = -2;
                else
                    _result = _x % _y;
            }
                break;
            default:
                break;
        }
    }

    string formatArg()
    {
        return to_string(_x) + _op + to_string(_y) + "=";
    }

    string formatRes()
    {
        return to_string(_result) + "(" + to_string(_exitCode) + ")";
    }

    ~Task()
    {}
private:
    int _x;
    int _y;
    char _op;

    int _result;
    int _exitCode;
};

POSIX信号量

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

以前:

信号量(信号灯)︰本质就是一个计数器,信号量需要进行PV操作,P 等于 --, V 等于 ++,原子性的!

今天:

信号量是用来描述临界资源中资源数目的!sem : 1 -> 0 -> 1,这种信号量我们称为二元信号量 == 互斥锁。

二元信号量是一种具有两个状态的信号量,通常用于同步和互斥操作。这两个状态通常是 0 和 1。二元信号量的目的是协调多个进程或线程,确保它们不会同时访问共享资源或执行特定的临界区代码。

在多进程或多线程的环境中,有时候需要控制对共享资源的访问,以避免数据竞争和不一致性。二元信号量提供了一种简单而有效的方法来实现这种同步。

常见的操作包括:

  1. 等待(Wait): 如果信号量的值为 1,将其减少为 0,表示资源正在被占用。如果信号量的值为 0,线程或进程将等待,直到信号量的值变为 1。
  2. 释放(Signal): 将信号量的值增加为 1,表示资源现在可用。这通常是在临界区的代码执行完毕后进行的操作。

如果只有一个资源的话我们可以定义一个二元信号量,但是如果有多份资源的情况下我们就可以定义一个多元信号量(本质就是一个计数器),每一个线程,在访问对应的资源的时候,先申请信号量,申请成功,表示该线程允许使用该资源,申请不成功。目前无法使用该资源!信号量的工作机制:信号量机制类似于我们看电影买票,是一种资源的预订机制!

信号量已经是资源的计数器了,申请信号量成功,本身就表明资源可用!申请信号量失败本身表明资源不可用,所以本质就是把判断转化成为信号量的申请成功与否的行为!比如之前的代码:

void push(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        while(isFull())//1.我们只能在临界区内部,判断临界资源是否就绪!注定了我们在当前一定是持有锁的!
        {
            pthread_cond_wait(&_productorCond, &_mutex);//我休眠(切换)了,我醒来的时候,在哪里往后执行呢?
        }
        //没有满,就让他进行生产
        _q.push(in);
        pthread_cond_signal(&_consumerCond);//有数据时,通知消费者进行消费
        pthread_mutex_unlock(&_mutex);
    }

我们根本就不用去理会资源是否可用或者满足条件,直接进行加锁,然后进行判断资源是否可用,不可用就解锁,接着又去重新判断资源是否可用,而之前说了想要使用资源,就要先申请信号量,申请成功,表示该线程允许使用该资源。申请不成功,就无法使用该资源!所以还用不用去判断资源是否可用了?答案肯定是不用了,只要你申请信号量成功,就一定能用,申请信号量失败就一定不能用。所以申请信号量是不是在访问临界之前?是不是就注定了,申请信号量在访问临界资源之前,就决定了我们其实是把判断资源就绪的行为放在了加锁或者未来访问资源之前我们就做了申请,所以信号量它是用来解决问题的

POSIX信号量类型和函数接口

sem_t 是在 POSIX 标准中定义的信号量类型,用于进程间或线程间的同步。这个类型通常与一组函数一起使用,这些函数允许对信号量执行不同的操作,如初始化、等待、以及释放。

以下是 sem_t 的一些主要特征和操作:

  1. sem_t 类型定义 **:**sem_t 是一个不透明的类型,具体的实现方式在不同的系统上可能会有所不同。在头文件 <semaphore.h> 中定义。

    #include <semaphore.h>

    sem_t my_semaphore; // 声明一个 sem_t 类型的信号量变量

  2. sem_init() 函数 (初始化): 是用于初始化信号量的 POSIX 线程库函数。

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

  • sem:指向要初始化的信号量的指针。
  • pshared:如果 pshared 参数的值为 0,表示信号量是线程共享的;如果非零,表示信号量是进程共享的。
  • value:指定信号量的初始值。

作用: 用于初始化信号量。创建并初始化一个信号量,设置其初始值为 value。如果 pshared 为 0,表示信号量是线程共享的,否则为进程共享。

返回值: 成功时返回 0,失败时返回 -1,并设置 errno 表示错误类型。

  1. sem_wait() 函数 (**等待):**是 POSIX 线程和进程间同步的一部分,在使用信号量时用于等待(阻塞)信号量的值降低(减少信号量)。

    int sem_wait(sem_t *sem);

  • sem:指向要等待的信号量的指针。

sem_wait() 函数用来等待信号量的值降低。如果信号量的值大于 0,则将其减一并立即返回。如果信号量的值为 0,则该调用将被阻塞(阻塞当前线程或进程),直到信号量的值不再为 0。一旦信号量的值变为非零,sem_wait() 函数将其减一并让阻塞线程继续执行。

当多个线程或进程同时调用 sem_wait() 等待同一个信号量时,只有一个线程或进程能够成功减小信号量的值,其他线程或进程将被阻塞,直到信号量的值再次变为非零。

需要注意的是,sem_wait() 可能由于各种原因返回错误,例如被信号中断(如 SIGINT)或者被其他线程取消等。因此,在使用 sem_wait() 函数时,应该检查函数的返回值来检测是否发生了错误。

返回值: 成功时返回 0,失败时返回 -1,并设置 errno 表示错误类型。

  1. sem_post() 函数 (释放): 是 POSIX 线程库提供的用于增加(释放)信号量值的函数。以下是该函数的原型和参数说明:

    int sem_post(sem_t *sem);

  • sem:指向要释放的信号量的指针。

作用: 用于释放(增加)信号量的值。将信号量的值增加一。如果有其他线程或进程等待该信号量,其中之一将被唤醒。

返回值: 成功时返回 0,失败时返回 -1,并设置 errno 表示错误类型。

  1. sem_destroy() 函数 (**销毁):**是 POSIX 线程库提供的用于销毁信号量的函数。以下是该函数的原型和参数说明:

    int sem_destroy(sem_t *sem);

  • sem:指向要销毁的信号量的指针。

作用: 用于销毁信号量。销毁信号量后,相关的资源将被释放,并且信号量不能再被使用。

注意事项:

  • 在销毁信号量之前,必须确保没有任何线程或进程正在使用该信号量或等待该信号量。
  • 确保在销毁信号量之前,必须调用相应的释放操作(如 sem_post)来确保信号量被正确释放。

返回值: 成功时返回 0,失败时返回 -1,并设置 errno 表示错误类型。

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

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

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

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

注:阻塞生产者消费者队列在加锁的时候,我们本质是把阻塞队列作为一个整体使用,加锁的时候要么只有生产者线程在进行生产,要么只有消费者在消费,总之就是只有一端的线程可以运行。但是!信号量(环形队列生产者消费者),它本质是将我们的一个大的临界资源进行肢解,变成多种(多份)资源,换句话说只要我们对于临界资源的访问,只要我们不访问同一个位置的资源,我们的读写、生产消费是可以并 **执行的,由串行变成并行,**也就是说可以一边消费,一边进行生产!

//Makefile
ringqueue:Main.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONT:clean
clean:
	rm -rf ringqueue

//Main.cc
#include "RingQueue.hpp"
#include "task.hpp"
#include <ctime>
#include <pthread.h>
#include <memory>
#include <sys/types.h>
#include <unistd.h>
#include <cstring>

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

void* consumerRoutine(void* args)
{
    RingQueue<Task>* rq = static_cast<RingQueue<Task> *>(args);
    while (true)
    {
        Task t;
        rq->pop(&t);
        t();
        cout << "consumer done, 处理完成的任务是: " << t.formatRes() << endl;
    }
}

void* productorRoutine(void* args)
{
    RingQueue<Task>* rq = static_cast<RingQueue<Task> *>(args);
    while(true)
    {
        int x = rand() % 100;
        int y = rand() % 100;
        char op = ops[(x + y) % strlen(ops)];
        Task t(x, y, op);
        rq->push(t);
        cout << "productor done, 生产的任务是: " << t.formatArg() << endl;
    }
}

int main()
{
    srand(time(nullptr) ^ getpid());
    RingQueue<Task>* rq = new RingQueue<Task>();
    //单生产 单消费
    // pthread_t c, p;
    // pthread_create(&c, nullptr, consumerRoutine, rq);
    // pthread_create(&p, nullptr, productorRoutine, rq);

    // pthread_join(c, nullptr);
    // pthread_join(p, nullptr);

    //多生产 多消费
    //他们的意义在哪里呢?意义绝对不在从缓冲区放入和拿去,意义在于,放前并发构建Task,获取后多线程
    //可以并发处理Task,因为这些操作没有加锁!
    pthread_t c[3], p[2];
    for(int i = 0; i < 3; i++)
        pthread_create(&c[i], nullptr, consumerRoutine, rq);
    for(int i = 0; i < 2; i++)
        pthread_create(&p[i], nullptr, productorRoutine, rq);

    for(int i = 0; i < 3; i++)
        pthread_join(c[i], nullptr);
    for(int i = 0; i < 2; i++)
        pthread_join(p[i], nullptr);

    delete rq;

    return 0;
}

//RingQueue.hpp
#pragma once

#include <iostream>
#include <vector>
#include <semaphore.h>
#include <pthread.h>

using namespace std;

static const int N = 5;

template<class T>
class RingQueue
{
private:
    void P(sem_t &s)
    {
        sem_wait(&s);//P操作 -- 
    }

    void V(sem_t &s)
    {
        sem_post(&s);//V操作 ++
    }

    void Lock(pthread_mutex_t &m)
    {
        pthread_mutex_lock(&m);
    }

    void UnlocK(pthread_mutex_t &m)
    {
        pthread_mutex_unlock(&m);
    }

public:
    RingQueue(int num = N) :_ring(num), _cap(num)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, num);
        _c_step = _p_step = 0;

        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }

    void push(const T &in)//生产
    {
        //先申请锁,还是先申请信号量?答案是推荐先申请信号量
        //如果我们先申请锁,那么也就意味着生产线程只要持有锁了,其他线程就没有机会进入后续的代码逻辑了
        //更加意味着其他线程在当前线程持有锁期间,其他线程也就只能在外部等待,但是如果先申请信号量,我们
        //就可以先把资源分配好,就好比一间教室你是让一个一个的人进来找好坐位,还是直接让一批人进来找好坐位,那个
        //速度更快?当然是让一批人进来找坐位速度更快,效率更好,所以下面的消费者也是如此。
        //所以先申请信号量,在进行加锁。
        P(_space_sem);//P操作 -- 
        Lock(_p_mutex);
        _ring[_p_step++] = in;
        _p_step %= _cap;
        UnlocK(_p_mutex);
        V(_data_sem);//V操作 ++

    }

    void pop(T* out)//消费
    {
        //信号量存在的意义
        //1.可以不用在临界区内部对资源做判断,就可以知道临界资源的使用情况
        //2.什么时候用锁?什么时候用sem(信号量)?取决于你对应的临界资源,是否被整体使用!
        P(_data_sem);
        Lock(_c_mutex);
        *out = _ring[_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:
    vector<T> _ring;
    int _cap;//环形队列容器大小
    sem_t _data_sem;//数据 -- 只有消费者关心
    sem_t _space_sem;//空间 -- 只有生产者关心
    int _c_step;//消费位置
    int _p_step;//生产位置

    //因为消费者和生产者是并行的所以需要两把锁,防止消费者内部的竞争和生产者内部的竞争
    //因为信号量的原因并不用担心死锁问题,因为消费者和生产者指向同一个位置时,只有其中一个可以运行
    pthread_mutex_t _c_mutex;//消费者之间的锁
    pthread_mutex_t _p_mutex;//生产者之间的锁
};

//Task.hpp
#pragma once

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

using namespace std;

class Task//Task表示:任务
{
public:
    Task()
    {}
    Task(int x, int y, char op):_x(x), _y(y), _op(op), _result(0), _exitCode(0)
    {}
    int operator()()//仿函数
    {
        switch(_op)
        {
            case '+':
                _result = _x + _y;
                break;
            case '-':
                _result = _x - _y;
                break;
            case '*':
                _result = _x * _y;
                break;
            case '/':
            {
                if(_y == 0)
                    _exitCode = -1;
                else
                    _result = _x / _y;
            }
                break;
            case '%':
            {
                if(_y == 0)
                    _exitCode = -2;
                else
                    _result = _x % _y;
            }
                break;
            default:
                break;
        }

        usleep(100000);
    }

    string formatArg()
    {
        return to_string(_x) + _op + to_string(_y) + "= ?";
    }

    string formatRes()
    {
        return to_string(_result) + "(" + to_string(_exitCode) + ")";
    }

    ~Task()
    {}
private:
    int _x;
    int _y;
    char _op;

    int _result;
    int _exitCode;
};

互斥锁和信号量的优缺点和差别

互斥锁(Mutex)和信号量(Semaphore)都是并发编程中用于协调多个线程或进程之间共享资源的同步机制。它们有各自的优缺点和适用场景。

互斥锁(Mutex):

优点:

  1. 简单易用: 互斥锁提供了简单的锁定和解锁机制,容易理解和使用。
  2. 只允许一个访问: 一次只允许一个线程或进程访问被保护的共享资源,避免了竞态条件(Race Condition)。

缺点:

  1. 可能引起死锁: 不正确使用互斥锁可能导致死锁,即多个线程互相等待对方释放锁,从而导致程序无法继续执行。
  2. 性能开销: 由于只允许一个线程访问共享资源,当有多个线程竞争同一资源时,其他线程可能被阻塞,导致性能开销。

信号量(Semaphore):

优点:

  1. 更灵活: 信号量是一种更为灵活的同步机制,可以用于控制对资源的访问数量,而不仅仅是单一的互斥。
  2. 避免死锁: 使用信号量时,可以更容易地避免死锁情况的发生,因为信号量的设计允许更复杂的同步协议。

缺点:

  1. 复杂性: 相对于互斥锁,信号量的使用可能更为复杂,尤其是在处理多个资源的情况下。
  2. 可能引起竞争条件: 错误地使用信号量也可能导致竞争条件,特别是在对信号量的计数不正确进行管理时。

差别:

  1. 用途: 互斥锁主要用于保护对临界区的访问,确保一次只有一个线程可以进入。信号量则可以用于控制对多个相同或不同资源的并发访问。
  2. 计数: 互斥锁是二进制的,要么被一个线程持有,要么没有。信号量是一个计数器,可以被多个线程持有,计数可以增减。
  3. 复杂性: 信号量相对于互斥锁来说更为复杂,因为它可以用于更复杂的同步方案,如生产者-消费者问题。
  4. 阻塞: 互斥锁在资源被释放之前,等待的线程会被阻塞。信号量则允许多个线程在同一时刻持有资源,只要信号量计数允许。

在选择使用互斥锁或信号量时,开发者需要根据具体的场景和需求来权衡它们的优缺点,以及对应的适用性。

8.线程池

线程池:

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

线程池的应用场景:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

线程池的种类:

线程池示例:

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

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

    //Main.cc
    //#include "ThreadPool_V1.hpp"
    //#include "ThreadPool_V2.hpp"
    //#include "ThreadPool_V3.hpp"
    #include "ThreadPool_V4.hpp"
    #include <memory>

    //V4,单例版的线程池
    int main()
    {
    printf("0X%x\n", ThreadPool<Task>::getinstance());
    printf("0X%x\n", ThreadPool<Task>::getinstance());
    printf("0X%x\n", ThreadPool<Task>::getinstance());
    printf("0X%x\n", ThreadPool<Task>::getinstance());
    printf("0X%x\n", ThreadPool<Task>::getinstance());
    printf("0X%x\n", ThreadPool<Task>::getinstance());

     while (true)
     {
         int x, y;
         char op;
         std::cout << "please Enter x> ";
         std::cin >> x;
         std::cout << "please Enter y> ";
         std::cin >> y;
         std::cout << "please Enter op(+-*/%)> ";
         std::cin >> op;
    
         Task t(x, y, op);
         ThreadPool<Task>::getinstance()->pushTask(t); //单例对象也有可能在多线程场景中使用!
     }
    
     return 0;
    

    }

    // //V2,V3
    // int main()
    // {
    // unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>(20));
    // tp->init();
    // tp->start();
    // tp->check();

    // while (true)
    // {
    // int x, y;
    // char op;
    // std::cout << "please Enter x> ";
    // std::cin >> x;
    // std::cout << "please Enter y> ";
    // std::cin >> y;
    // std::cout << "please Enter op(+-*/%)> ";
    // std::cin >> op;

    // Task t(x, y, op);
    // tp->pushTask(t);
    // }

    // return 0;
    // }

    // //V1
    // int main()
    // {
    // unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());
    // tp->init();
    // tp->start();

    // while (true)
    // {
    // int x, y;
    // char op;
    // std::cout << "please Enter x> ";
    // std::cin >> x;
    // std::cout << "please Enter y> ";
    // std::cin >> y;
    // std::cout << "please Enter op(+-*/%)> ";
    // std::cin >> op;

    // Task t(x, y, op);
    // tp->pushTask(t);

    // // 充当生产者, 从网络中读取数据,构建成为任务,推送给线程池
    // // sleep(1);
    // // tp->pushTask();
    // }

    // return 0;
    // }

    //ThreadPool_V1.hpp
    #pragma once

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

    const static int N = 5;

    template <class T>
    class ThreadPool
    {
    public:
    ThreadPool(int num = N) : _num(num), _threads(num)
    {
    pthread_mutex_init(&_lock, nullptr);
    pthread_cond_init(&_cond, nullptr);
    }
    void lockQueue()
    {
    pthread_mutex_lock(&_lock);
    }
    void unlockQueue()
    {
    pthread_mutex_unlock(&_lock);
    }
    void threadWait()
    {
    pthread_cond_wait(&_cond, &_lock);
    }
    void threadWakeup()
    {
    pthread_cond_signal(&_cond);
    }
    bool isEmpty()
    {
    return _tasks.empty();
    }

     T popTask()
     {
         T t = _tasks.front();
         _tasks.pop();
         return t;
     }
    
     static void *threadRoutine(void *args)
     {
         pthread_detach(pthread_self());
         ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
         while (true)
         {
             // 1. 检测有没有任务
             // 2. 有:处理
             // 3. 无:等待
             // 细节:必定加锁
             tp->lockQueue();
             while (tp->isEmpty())
             {
                 tp->threadWait();
             }
             T t = tp->popTask(); // 从公共区域拿到私有区域
             tp->unlockQueue();
    
             // for test
             // t.run(); // 处理任务,应不应该在临界区中处理?不应该
             t();
             std::cout << "thread handler done, result: " << t.formatRes() << std::endl;
         }
     }
    
     void init()
     {
         // TODO
     }
     void start()
     {
         for (int i = 0; i < _num; i++)
         {
             pthread_create(&_threads[i], nullptr, threadRoutine, this); // ?
         }
     }
     void pushTask(const T &t)
     {
         lockQueue();
         _tasks.push(t);
         threadWakeup();
         unlockQueue();
     }
     ~ThreadPool()
     {
         pthread_mutex_destroy(&_lock);
         pthread_cond_destroy(&_cond);
     }
    

    private:
    std::vector<pthread_t> _threads;
    int _num;

     std::queue<T> _tasks; // 使用stl的自动扩容的特性
    
     pthread_mutex_t _lock;
     pthread_cond_t _cond;
    

    };

    //ThreadPool_V2.hpp
    #pragma once

    #include <iostream>
    #include <string>
    #include <vector>
    #include <queue>
    #include <unistd.h>
    #include "Thread.hpp"
    #include "Task.hpp"

    const static int N = 5;

    template <class T>
    class ThreadPool
    {
    public:
    ThreadPool(int num = N) : _num(num)
    {
    pthread_mutex_init(&_lock, nullptr);
    pthread_cond_init(&_cond, nullptr);
    }
    void lockQueue()
    {
    pthread_mutex_lock(&_lock);
    }
    void unlockQueue()
    {
    pthread_mutex_unlock(&_lock);
    }
    void threadWait()
    {
    pthread_cond_wait(&_cond, &_lock);
    }
    void threadWakeup()
    {
    pthread_cond_signal(&_cond);
    }
    bool isEmpty()
    {
    return _tasks.empty();
    }
    T popTask()
    {
    T t = _tasks.front();
    _tasks.pop();
    return t;
    }
    static void threadRoutine(void *args)
    {
    // pthread_detach(pthread_self());
    ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
    while (true)
    {
    // 1. 检测有没有任务
    // 2. 有:处理
    // 3. 无:等待
    // 细节:必定加锁
    tp->lockQueue();
    while (tp->isEmpty())
    {
    tp->threadWait();
    }
    T t = tp->popTask(); // 从公共区域拿到私有区域
    tp->unlockQueue();

             // for test
             t();
             std::cout << "thread handler done, result: " << t.formatRes() << std::endl;
         }
     }
     void init()
     {
         for (int i = 0; i < _num; i++)
         {
             _threads.push_back(Thread(i, threadRoutine, this));
         }
     }
     void start()
     {
         for (auto &t : _threads)
         {
             t.run();
         }
     }
     void check()
     {
         for (auto &t : _threads)
         {
             std::cout << t.threadname() << " running..." << std::endl;
         }
     }
     void pushTask(const T &t)
     {
         lockQueue();
         _tasks.push(t);
         threadWakeup();
         unlockQueue();
     }
     ~ThreadPool()
     {
         for (auto &t : _threads)
         {
             t.join();
         }
         pthread_mutex_destroy(&_lock);
         pthread_cond_destroy(&_cond);
     }
    

    private:
    std::vector<Thread> _threads;
    int _num;

     std::queue<T> _tasks; // 使用stl的自动扩容的特性
    
     pthread_mutex_t _lock;
     pthread_cond_t _cond;
    

    };

    //ThreadPool_V3.hpp
    #pragma once

    #include <iostream>
    #include <string>
    #include <vector>
    #include <queue>
    #include <unistd.h>
    #include "Thread.hpp"
    #include "Task.hpp"
    #include "lockGuard.hpp"

    const static int N = 5;

    template <class T>
    class ThreadPool
    {
    public:
    ThreadPool(int num = N) : _num(num)
    {
    pthread_mutex_init(&_lock, nullptr);
    pthread_cond_init(&_cond, nullptr);
    }
    pthread_mutex_t* getlock()
    {
    return &_lock;
    }
    void threadWait()
    {
    pthread_cond_wait(&_cond, &_lock);
    }
    void threadWakeup()
    {
    pthread_cond_signal(&_cond);
    }
    bool isEmpty()
    {
    return _tasks.empty();
    }
    T popTask()
    {
    T t = _tasks.front();
    _tasks.pop();
    return t;
    }
    static void threadRoutine(void *args)
    {
    // pthread_detach(pthread_self());
    ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
    while (true)
    {
    // 1. 检测有没有任务
    // 2. 有:处理
    // 3. 无:等待
    // 细节:必定加锁
    T t;
    {
    LockGuard lockguard(tp->getlock());
    while (tp->isEmpty())
    {
    tp->threadWait();
    }
    t = tp->popTask(); // 从公共区域拿到私有区域
    }
    // for test
    t();
    std::cout << "thread handler done, result: " << t.formatRes() << std::endl;
    }
    }
    void init()
    {
    for (int i = 0; i < _num; i++)
    {
    _threads.push_back(Thread(i, threadRoutine, this));
    }
    }
    void start()
    {
    for (auto &t : _threads)
    {
    t.run();
    }
    }
    void check()
    {
    for (auto &t : _threads)
    {
    std::cout << t.threadname() << " running..." << std::endl;
    }
    }
    void pushTask(const T &t)
    {
    LockGuard lockgrard(&_lock);
    _tasks.push(t);
    threadWakeup();
    }
    ~ThreadPool()
    {
    for (auto &t : _threads)
    {
    t.join();
    }
    pthread_mutex_destroy(&_lock);
    pthread_cond_destroy(&_cond);
    }

    private:
    std::vector<Thread> _threads;
    int _num;

     std::queue<T> _tasks; // 使用stl的自动扩容的特性
    
     pthread_mutex_t _lock;
     pthread_cond_t _cond;
    

    };

    //Task.hpp
    #pragma once

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

    using namespace std;

    class Task//Task表示:任务
    {
    public:
    Task()
    {}
    Task(int x, int y, char op):_x(x), _y(y), _op(op), _result(0), _exitCode(0)
    {}
    int operator()()//仿函数
    {
    switch(_op)
    {
    case '+':
    _result = _x + _y;
    break;
    case '-':
    _result = _x - _y;
    break;
    case '*':
    _result = _x * _y;
    break;
    case '/':
    {
    if(_y == 0)
    _exitCode = -1;
    else
    _result = _x / _y;
    }
    break;
    case '%':
    {
    if(_y == 0)
    _exitCode = -2;
    else
    _result = _x % _y;
    }
    break;
    default:
    break;
    }

         usleep(100000);
     }
    
     string formatArg()
     {
         return to_string(_x) + _op + to_string(_y) + "= ?";
     }
    
     string formatRes()
     {
         return to_string(_result) + "(" + to_string(_exitCode) + ")";
     }
    
     ~Task()
     {}
    

    private:
    int _x;
    int _y;
    char _op;

     int _result;
     int _exitCode;
    

    };

    //Thread.hpp
    #pragma once

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

    using namespace std;

    class Thread
    {
    public:
    typedef enum
    {
    NEW = 0,
    RUNNING,
    EXITED
    } ThreadStatus;
    typedef void (func_t)(void);

    public:
    Thread(int num, func_t func, void *args) : _tid(0), _status(NEW), _func(func), _args(args)
    {
    char name[128];
    snprintf(name, sizeof(name), "thread-%d", num);
    _name = name;
    }

     int status() { return _status; }//获取线程状态
    
     std::string threadname() { return _name; }//获取线程名字
    
     pthread_t threadid()//获取线程ID
     {
         if (_status == RUNNING)
             return _tid;
         else
         {
             return 0;
         }
     }
    
     // runHelper是不是类的成员函数,而类的成员函数,具有默认参数this,需要static
     // 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数
     static void *runHelper(void *args)
     {
         Thread *ts = (Thread*)args; // 就拿到了当前对象
         // _func(_args);
         (*ts)();
         return nullptr;
     }
    
      void operator ()() //仿函数
     {
         if(_func != nullptr) 
             _func(_args);
     }
    
     void run()
     {
         int n = pthread_create(&_tid, nullptr, runHelper, this);
         if(n != 0) exit(1);
         _status = RUNNING;
     }
    
     void join()
     {
         int n = pthread_join(_tid, nullptr);
         if( n != 0)
         {
             std::cerr << "main thread join thread " << _name << " error" << std::endl;
             return;
         }
         _status = EXITED;
     }
    
     ~Thread()
     {}
    

    private:
    pthread_t _tid;
    std::string _name;
    func_t _func; // 线程未来要执行的回调
    void* _args;
    ThreadStatus _status;
    };

    //lockGuard.hpp
    #pragma once

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

    class Mutex // 自己不维护锁,有外部传入
    {
    public:
    Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
    {}
    void lock()
    {
    pthread_mutex_lock(_pmutex);//加锁
    }
    void unlock()
    {
    pthread_mutex_unlock(_pmutex);//解锁
    }
    ~Mutex()
    {}
    private:
    pthread_mutex_t *_pmutex;
    };

    class LockGuard // 自己不维护锁,有外部传入
    {
    public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
    _mutex.lock();//通过构造函数自动加锁,防止忘记加锁
    }
    ~LockGuard()
    {
    _mutex.unlock();//通过析构函数自动解锁,防止忘记解锁
    }
    private:
    Mutex _mutex;
    };

9.线程安全的单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点以确保唯一性。单例模式是一种 "经典的, 常用的, 常考的" 设计模式。

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

[洗碗的例子]:

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

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

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

懒汉方式实现单例模式

//ThreadPool_V4.hpp
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <unistd.h>
#include "Thread.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"

const static int N = 5;

//单例版的线程池
template <class T>
class ThreadPool
{
private:
    ThreadPool(int num = N) : _num(num)
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool<T> &tp) = delete;
    void operator=(const ThreadPool<T> &tp) = delete;

public:
    static ThreadPool<T> *getinstance()
    {
        if(nullptr == instance) // 为什么要这样?提高效率,减少加锁的次数!
        {
            LockGuard lockguard(&instance_lock);
            if (nullptr == instance)
            {
                instance = new ThreadPool<T>();
                instance->init();
                instance->start();
            }
        }

        return instance;
    }

    pthread_mutex_t *getlock()
    {
        return &_lock;
    }
    void threadWait()
    {
        pthread_cond_wait(&_cond, &_lock);
    }
    void threadWakeup()
    {
        pthread_cond_signal(&_cond);
    }
    bool isEmpty()
    {
        return _tasks.empty();
    }
    T popTask()
    {
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }
    static void threadRoutine(void *args)
    {
        // pthread_detach(pthread_self());
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        while (true)
        {
            // 1. 检测有没有任务
            // 2. 有:处理
            // 3. 无:等待
            // 细节:必定加锁
            T t;
            {
                LockGuard lockguard(tp->getlock());
                while (tp->isEmpty())
                {
                    tp->threadWait();
                }
                t = tp->popTask(); // 从公共区域拿到私有区域
            }
            // for test
            t();
            std::cout << "thread handler done, result: " << t.formatRes() << std::endl;
        }
    }
    void init()
    {
        for (int i = 0; i < _num; i++)
        {
            _threads.push_back(Thread(i, threadRoutine, this));
        }
    }
    void start()
    {
        for (auto &t : _threads)
        {
            t.run();
        }
    }
    void check()
    {
        for (auto &t : _threads)
        {
            std::cout << t.threadname() << " running..." << std::endl;
        }
    }
    void pushTask(const T &t)
    {
        LockGuard lockgrard(&_lock);
        _tasks.push(t);
        threadWakeup();
    }
    ~ThreadPool()
    {
        for (auto &t : _threads)
        {
            t.join();
        }
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }

private:
    std::vector<Thread> _threads;
    int _num;

    std::queue<T> _tasks; // 使用stl的自动扩容的特性

    pthread_mutex_t _lock;
    pthread_cond_t _cond;

    static ThreadPool<T> *instance;
    static pthread_mutex_t instance_lock;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::instance_lock = PTHREAD_MUTEX_INITIALIZER;

10.STL,智能指针和线程安全

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

原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.

而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).

因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。

对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。

11.其他常见的各种锁

1.悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。

2.乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。

3.CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

4.自旋锁,公平锁,非公平锁?

自旋锁(Spinlock)是一种简单的同步原语,用于在多线程或多处理器系统中实现对共享资源的互斥访问。与传统的互斥锁不同,自旋锁不会让线程进入睡眠状态等待资源可用,而是会一直尝试获取锁(自旋,本质就是非阻塞轮询)直到成功。

自旋锁的基本工作原理如下:

当一个线程希望访问共享资源时,它会尝试获取自旋锁。

如果自旋锁当前没有被其他线程持有,则当前线程成功获取锁,可以进入临界区。

如果自旋锁已经被其他线程持有,当前线程会一直在一个忙循环中自旋等待,不释放 CPU 时间片。

当持有锁的线程释放锁时,其他线程中的一个将能够获取锁,继续执行。

自旋锁适用于临界区很小且短时间内能够完成的情况。如果临界区很大或者线程需要等待的时间较长,使用自旋锁可能会导致性能下降,因为线程一直在自旋等待,占用了 CPU 时间。

在实现上,自旋锁通常是一个整数变量,当被设置为某个特定值时表示锁已被某个线程持有,当为另一个特定值时表示锁处于空闲状态。自旋锁的获取和释放通常使用原子操作来确保线程安全。

需要注意的是,自旋锁不适用于单核处理器系统,因为在这种情况下,自旋等待会导致浪费 CPU 时间而无法实现真正的并发。在多核系统中,自旋锁可以更有效地防止线程在竞争条件下进入睡眠状态。

**自旋锁(Spinlock)是一种用于多线程编程的锁,**它通过循环检测锁的状态(自旋等待)来实现对共享资源的互斥访问。自旋锁在多核处理器或对称多处理系统中较为常见,它允许线程在等待共享资源时保持活跃状态,而不是让线程进入睡眠状态。当自旋锁处于锁定状态时,线程会不断地轮询直到获取到锁。

自旋锁的函数接口可以因编程语言和操作系统而异,通常包含以下操作:

初始化: 初始化自旋锁的函数,用于分配所需的资源并设置锁的初始状态。

获取锁(加锁): 获取自旋锁以访问共享资源。如果锁已被其他线程持有,则当前线程可能会进入自旋等待状态,直到锁被释放。

释放锁(解锁): 释放自旋锁,允许其他线程获取并访问共享资源。

在C语言中,自旋锁的函数接口通常包括以下操作:

#include <pthread.h>

// 自旋锁的定义和初始化
pthread_spinlock_t lock;
pthread_spin_init(&lock, PTHREAD_PROCESS_SHARED); // 初始化自旋锁

// 获取锁(加锁)
pthread_spin_lock(&lock); // 获取自旋锁

// 释放锁(解锁)
pthread_spin_unlock(&lock); // 释放自旋锁

// 销毁自旋锁
pthread_spin_destroy(&lock);

请注意,自旋锁不适用于所有情况。在单核处理器系统或高竞争情况下,自旋锁可能会导致性能问题。因此,开发人员在选择锁类型时应考虑到应用程序的特定需求和系统环境。

12.读者写者问题

读写锁

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

读写锁(Read-Write Lock)是一种同步机制,允许多个线程同时读取共享资源,但在写入时需要互斥。这种锁的设计是为了提高多线程环境下对读操作的并发性能。读写锁有两种状态:读模式和写模式。

读写锁的基本操作包括:

读取操作(共享模式): 允许多个线程同时获得读取锁,这些线程可以并发地读取共享资源。

写入操作(独占模式): 当一个线程获得写入锁时,其他线程无法同时获得读取或写入锁。写入锁是独占的,确保在写入时不会有其他线程进行读取或写入操作。

读写锁的优势在于对读操作的并发性能进行了优化,允许多个线程同时读取,而不会互斥地阻塞。这对于读操作远远多于写操作的情况非常有利。

在C语言中,使用pthread库可以实现读写锁。以下是一个简单的读写锁的示例:

#include <pthread.h>

// 读写锁的定义和初始化
pthread_rwlock_t rwlock;//类型
pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁

// 读取操作(共享模式)
pthread_rwlock_rdlock(&rwlock); // 获取读取锁

// 写入操作(独占模式)
pthread_rwlock_wrlock(&rwlock); // 获取写入锁

// 释放锁
pthread_rwlock_unlock(&rwlock); // 释放读取或写入锁

// 销毁读写锁
pthread_rwlock_destroy(&rwlock);

在上述示例中,pthread_rwlock_rdlock() 用于获取读取锁,pthread_rwlock_wrlock() 用于获取写入锁,pthread_rwlock_unlock() 用于释放读取或写入锁,而 pthread_rwlock_destroy() 用于销毁读写锁。

使用读写锁时,需要小心避免写入锁的过度使用,以免降低并发性能。选择读写锁还取决于应用程序的特定需求和并发访问模式。

设置读写优先

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
  • attr 参数是一个指向读写锁属性对象(pthread_rwlockattr_t)的指针。此属性对象通过 pthread_rwlockattr_init 进行初始化。
  • pref 参数代表着锁的类型。它可以是 PTHREAD_RWLOCK_PREFER_READER_NP 或 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP。

这些参数的含义:

  • PTHREAD_RWLOCK_PREFER_READER_NP:表示系统更偏向于读取锁。在竞争锁时,系统会更倾向于授予读取锁。
  • PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:表示系统更偏向于写入锁。在竞争锁时,系统会更倾向于授予写入锁,并且这些写入锁是非递归的。

需要注意的是,_NP 后缀表示这是一个非标准的扩展,不属于 POSIX 标准的一部分。使用此函数时,需要小心考虑兼容性和移植性,并确保目标系统支持该扩展。

在标准的 POSIX 中,通常不提供直接设置读写锁类型的函数。默认情况下,系统会根据实现和策略来处理读写锁的竞争情况。

实际pref 共有 3 种选择

PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况,因为读者一直进入就导致写者无法拿到资源,直到没有写者为止。

PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和

PTHREAD_RWLOCK_PREFER_READER_NP 一致

PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁

使用方式:

这里是一个使用 pthread_rwlockattr_setkind_np 的简单示例,用于设置读写锁的优先类型:

#include <pthread.h>
#include <stdio.h>

// 定义一个读写锁
pthread_rwlock_t rwlock;

int main() {
    // 初始化读写锁属性
    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);

    // 设置锁的类型为偏向于读取锁
    pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_READER_NP);

    // 使用属性对象初始化读写锁
    pthread_rwlock_init(&rwlock, &attr);

    // 在这里可以使用读写锁进行操作

    // 销毁读写锁和属性对象
    pthread_rwlock_destroy(&rwlock);
    pthread_rwlockattr_destroy(&attr);

    return 0;
}

这个示例演示了如何使用 pthread_rwlockattr_setkind_np 函数来设置读写锁的优先类型。首先,你需要初始化一个读写锁属性对象 pthread_rwlockattr_t,然后使用 pthread_rwlockattr_setkind_np 设置锁的类型。最后,使用这个属性对象初始化一个读写锁。

需要注意的是,pthread_rwlockattr_setkind_np 是一个非标准的函数,其行为和可用性可能因系统而异。在实际使用中,建议查看你所使用系统的文档或相关的系统头文件以获取准确的信息和确保兼容性。

pthread_rwlock_t 和 pthread_rwlockattr_t 是用于操作 POSIX 线程读写锁的两个相关的类型。

  1. pthread_rwlock_t
    • pthread_rwlock_t 是用于表示 POSIX 线程读写锁的类型。
    • 读写锁是一种多线程同步机制,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种锁的设计适用于对于读操作频繁但写操作较少的情况,以提高并发性能。
    • 你需要使用 pthread_rwlock_init 函数进行初始化,使用 pthread_rwlock_rdlock 和 pthread_rwlock_wrlock 函数进行上锁,以及使用 pthread_rwlock_unlock 函数进行解锁。
  1. pthread_rwlockattr_t
    • pthread_rwlockattr_t 是用于表示 POSIX 线程读写锁属性的类型。
    • 你需要使用 pthread_rwlockattr_init 函数初始化属性对象,然后通过 pthread_rwlockattr_set* 等函数设置属性的各种参数,如锁的类型、进程间共享等(比如 pthread_rwlockattr_setsetkind_np 函数用来设置锁的类型)。
    • 最后,使用属性对象初始化读写锁。
相关推荐
007php0073 小时前
linux服务器上CentOS的yum和Ubuntu包管理工具apt区别与使用实战
linux·运维·服务器·ubuntu·centos·php·ai编程
djykkkkkk3 小时前
ubuntu编译遇到的问题
linux·运维·ubuntu
qq_429856573 小时前
linux 查看服务是否开机自启动
linux·运维·服务器
7yewh4 小时前
Linux驱动开发 IIC I2C驱动 编写APP访问EEPROM AT24C02
linux·arm开发·驱动开发·嵌入式硬件·嵌入式
dessler5 小时前
Docker-Dockerfile讲解(三)
linux·运维·docker
KevinRay_5 小时前
命令行之巅:Linux Shell编程的至高艺术(中)
linux·运维·服务器·重定向·shell编程
程序员JerrySUN5 小时前
Yocto 项目 - 共享状态缓存 (Shared State Cache) 机制
linux·嵌入式硬件·物联网·缓存·系统架构
林农7 小时前
C05S16-MySQL高可用
linux·mysql·云计算
码中小白鼠7 小时前
Ubuntu系统部署Mysql8.0后设置不区分大小写
linux·mysql·ubuntu·adb
gz94567 小时前
Virtualbox安装ubuntu20虚拟机无法打开终端
java·linux·开发语言