第 11 章 线程

线程

线程的概念

一个正在运行的函数
Posix 线程是一套标准,而不是实现。另外还有 openmp 线程等

线程标识

Posix 线程标识:pthread_t

gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note$ ps axm
  PID TTY      STAT   TIME COMMAND
    1 ?        -      0:00 /init
    - -        Ssl    0:00 -
    - -        Ssl    0:00 -
    6 ?        -      0:00 plan9 --control-socket 6 --log-level 4 --server-fd 7 --pipe-fd 9 --socket-path /mnt/c/Users/gaowanlu/AppData/Local/Package
    - -        Sl     0:00 -//线程
    - -        Sl     0:00 -//线程
    9 tty1     -      0:00 /init
    - -        Ss     0:00 -
   10 tty1     -      0:00 -bash
    - -        S      0:00 -
  145 tty1     -      0:00 ps axm
    - -        R      0:00 -
//一个进程至少一个线程

其实线程是 LWP,轻量级进程

//-L 为以Linux形式查看
gaowanlu@DESKTOP-QDLGRDB:/$ ps axm -L
  PID   LWP TTY      STAT   TIME COMMAND
    1     - ?        -      0:00 /init
    -     1 -        Ssl    0:00 -
    -     8 -        Ssl    0:00 -
    6     - ?        -      0:00 plan9 --control-socket 6 --log-level 4 --server-fd 7 --pipe-fd 9 --socket-path /mnt/c/Users/gaowanlu/AppData/Local/Packages/CanonicalGroupLimited.Ubuntu20.04onWindows_
    -     6 -        Sl     0:00 -
    -     7 -        Sl     0:00 -
    9     - tty1     -      0:00 /init
    -     9 -        Ss     0:00 -
   10     - tty1     -      0:00 -bash
    -    10 -        S      0:00 -
  160     - tty1     -      0:00 ps axm -L
    -   160 -        R      0:00 -

进程其实就是容器,用来存储线程

pthread_equal 函数

pthread_equal 用来比较两个线程标识是否相同

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

pthread_self 函数

pthread_self 是一个 POSIX 线程库函数,它返回调用线程的线程 ID(Thread ID)。线程 ID 是一个唯一的无符号长整型(pthread_t 类型),用于标识线程。该函数的原型如下:

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

线程 ID 在其生命周期中始终保持不变。因此,在调用 pthread_create 函数创建线程后,您可以使用 pthread_self 函数在新线程中获取其线程 ID。

pthread_create 线程的创建

pthread_create 函数

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                  void *(*start_routine) (void *), void *arg);
//创建成功返回0 否则返回1
// thread是指向线程标识符的指针
// attr是一个指向线程属性的指针
// start_routine是一个指向线程函数的指针
// arg是传递给线程函数的参数。

样例

#include <iostream>
#include <pthread.h>
#include <stdio.h>
using namespace std;

void *message(void *message)
{
    char *str = (char *)message;
    cout << str << endl;
    return nullptr;
}

int main(int argc, char **argv)
{
    pthread_t thread1, thread2;
    const char *message1 = "thread1";
    const char *message2 = "thread2";
    int ret1, ret2;
    ret1 = pthread_create(&thread1, nullptr, message, (void *)message1);
    if (ret1)
    {
        printf("Error - pthread_create() return : %s\n", strerror(ret1));
        return 1;
    }
    ret2 = pthread_create(&thread2, nullptr, message, (void *)message2);
    if (ret2)
    {
        printf("Error - pthread_create() return : %s\n", strerror(ret2));
        return 1;
    }
    // wait for thread
    pthread_join(thread1, nullptr);
    pthread_join(thread2, nullptr);
    return 0;
}

// gaowanlu@DESKTOP-QDLGRDB:/mnt/c/Users/gaowanlu/Desktop/MyProject/note/testcode$ ./main
// thread1
// thread2

线程的调度取决与调度器策略

线程的终止

线程终止的方式
3 种方式:
(1) 线程从启动例程返回,返回值就是线程的退出码
(2) 线程可以被同一进程中的其他线程取消
(3) 线程调用 pthread_exit 函数

#include <pthread.h>
void pthread_exit(void *retval);

样例

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

using namespace std;

void *message(void *message)
{
    char *str = (char *)message;
    cout << str << endl;
    pthread_exit(nullptr);
}

int main(int argc, char **argv)
{
    pthread_t thread1;
    const char *message1 = "thread1";
    int ret1;
    ret1 = pthread_create(&thread1, nullptr, message, (void *)message1);
    if (ret1)
    {
        printf("Error - pthread_create() return code: %s\n", strerror(ret1));
        return 1;
    }
    // wait for thread
    pthread_join(thread1, nullptr);
    return 0;
}

pthread_join 函数

pthread_join() 函数是一个线程函数,用于等待指定线程的终止,并将线程的退出状态存储在一个指定的变量中。该函数会阻塞当前线程,直到指定的线程退出为止。

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//thread 是要等待的线程的标识符;retval 是一个指向指针的指针,用于存储线程的退出状态。
//return: 0成功否则失败
//retval就是pthread_exit传的内容

如果指定的线程没有以 joinable 的状态创建,或者已经被其他线程 join,或者调用线程自己已经被取消,那么 pthread_join() 函数都会失败并返回一个非零值。

使用 pthread_join() 函数时,需要注意以下几点:

调用 pthread_join() 函数的线程会被阻塞,直到指定的线程退出为止。
如果不关心线程的退出状态,可以将 retval 参数设置为 NULL。
线程只能被 join 一次,如果尝试多次 join 同一个线程,或者 join 已经被其他线程 join 的线程,都会导致失败。
如果线程被设置为 detached 状态,则不能使用 pthread_join() 函数等待它的退出,否则会失败。

样例

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

void *thread_func(void *arg)
{
    printf("Thread running\n");
    int *res = (int *)malloc(sizeof(int));
    *res = 42;
    pthread_exit((void *)res);
}

int main()
{
    pthread_t thread;
    int ret;
    void *retval;

    ret = pthread_create(&thread, NULL, thread_func, NULL);
    if (ret != 0)
    {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    ret = pthread_join(thread, &retval);
    if (ret != 0)
    {
        perror("pthread_join");
        exit(EXIT_FAILURE);
    }

    if (retval)
    {
        printf("Thread exited with status %d\n", (*((int *)retval)));
        free(retval);
    }

    return 0;
}

/*
gaowanlu@DESKTOP-QDLGRDB:/$ g++ main.cpp -o main -lpthread
gaowanlu@DESKTOP-QDLGRDB:/$ ./main
Thread running
Thread exited with status 42
*/

线程栈的清理

相关函数 pthread_cleanup_push 和 pthread_cleanup_pop 是一对宏定义,用于在线程中注册清理处理函数
在第七章有类似的钩子函数 atexit,当进程正常终止时可以被逆序调用

pthread_cleanup_push 与 pthread_cleanup_pop

重点:pthread_cleanup_push 与 pthread_cleanup_pop 应该成对出现,否则在预处理后会有语法错误

#include <pthread.h>
//挂钩子函数 routine函数指针,arg函数参数
void pthread_cleanup_push(void (*routine)(void *),void *arg);
//从钩子上区内容execute为1则代表取下来且执行调用 为0则取下来不调用
void pthread_cleanup_pop(int execute);

样例

#include <pthread.h>
#include <stdio.h>
void cleanup_handler(void *arg)
{
    printf("Cleanup: %s\n", (char *)arg);
}
void *thread_func(void *arg)
{
    pthread_cleanup_push(cleanup_handler, (void *)"First handler");
    pthread_cleanup_push(cleanup_handler, (void *)"Second handler");
    printf("Thread running\n");
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(1);
    pthread_exit(NULL);
}

int main()
{
    pthread_t thread;
    int ret;
    ret = pthread_create(&thread, NULL, thread_func, NULL);
    if (ret != 0)
    {
        perror("pthread_create");
        return 1;
    }
    pthread_join(thread, NULL);
    return 0;
}

/*
gaowanlu@DESKTOP-QDLGRDB:/$ g++ main.cpp -o main -lpthread
gaowanlu@DESKTOP-QDLGRDB:/$ ./main
Thread running
Cleanup: Second handler
Cleanup: First handler
*/

为什么要成对出现?

1、语法规定:pthread_cleanup_push() 和 pthread_cleanup_pop() 是一对宏定义,必须按照规定的语法配对使用。

2、栈管理:pthread_cleanup_push() 和 pthread_cleanup_pop() 实现了一个栈结构,用于存储清理处理函数。pthread_cleanup_push() 将一个清理处理函数入栈,pthread_cleanup_pop() 将最后一个清理处理函数出栈。如果它们不成对出现,会导致栈结构不正确,可能会出现内存泄漏或者错误地清理资源的情况。

3、取消处理:当一个线程被取消时,会按照清理处理函数入栈的顺序依次执行清理处理函数,直到栈为空或者遇到不能被取消的清理处理函数为止。如果 pthread_cleanup_push() 和 pthread_cleanup_pop() 不成对出现,就可能会导致清理处理函数没有按照预期的顺序执行,从而产生错误。

因此,为了避免语法错误和资源泄漏,以及确保清理处理函数能够按照正确的顺序执行,pthread_cleanup_push() 和 pthread_cleanup_pop() 必须成对出现。

pop 甚至可以写到 pthread_exit 的后面

void *thread_func(void *arg)
{
    pthread_cleanup_push(cleanup_handler, (void *)"First handler");
    pthread_cleanup_push(cleanup_handler, (void *)"Second handler");
    printf("Thread running\n");
    pthread_exit(NULL);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
}
//在这时即使pthread_cleanup_pop,这时的pop仅仅有语法补全的功能,默认当exit时会执行pop,除非是pop(0)才不会被调用

线程的取消

取消有两种状态:允许和不允许
允许取消又分为:异步 cancel、推迟 cancel(默认 推迟到 cancel 点再响应)
cancel 点:POSIX 定义的 cancel 点,都是可能引发阻塞的系统调用
pthread_cancel 函数向目标线程发送一个取消请求
pthread_setcancelstate(): 设置是否允许取消
pthread_setcanceltype(): 设置取消方式
pthread_testcancel(): 本函数什么都不做,就是一个 cancel 点

实际样例

int fd1 = open();//open前是cancel点
if (fd1 < 0)
{
    perror();
    pthread_exit(nullptr);
}
// cleanup_push(); --> close(fd1);
int fd2 = open();//open前是cancel点
if (fd2 < 0)
{
    perror();
    pthread_exit(nullptr);
}
// cleanup_push(); --> close(fd2);

pthread_cancel 函数

函数向目标线程发送一个取消请求

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

pthread_cancel 函数是 pthread 线程库提供的一个函数,用于请求取消正在运行的线程。
pthread_cancel 函数可以向指定线程发送一个取消请求,这个请求并不能保证线程会立即被取消,而是告知线程在下一个取消点或异步取消时终止自身的执行。在 POSIX 标准中,每个函数都标记了是否是一个取消点,线程在执行这些函数时可能会检查取消请求。
如果线程在取消点时收到取消请求,那么它会自动终止执行。如果线程不在取消点,那么它会在某个取消点时终止执行。
如果线程被成功取消,那么它会执行一个清理函数(cleanup handler)列表,这些清理函数可以用 pthread_cleanup_push 和 pthread_cleanup_pop 函数来注册和取消注册。清理函数可以用于释放线程持有的资源,以及执行一些必要的清理操作。
需要注意的是,pthread_cancel 函数并不能保证线程会被立即取消,因为线程可能处于不可取消状态(如执行了信号处理程序时),或者处于阻塞状态(如等待 I/O 操作完成)。此外,线程必须在创建时设置了可以被取消的属性,否则取消请求也不会生效。
因此,在使用 pthread_cancel 函数时需要仔细考虑线程的状态以及对资源的使用,以避免可能的资源泄露和其他问题。

pthread_setcancelstate 函数

pthread_setcancelstate(): 设置是否允许取消

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

在 POSIX 标准中,每个线程都有一个取消状态(cancelability state),它决定了线程是否可以被取消。线程的取消状态可以是可取消状态(cancelable state)或者不可取消状态(uncancelable state)。
pthread_setcancelstate 函数可以设置线程的取消状态,它有两个参数:
state:要设置的取消状态,可以取以下两个值之一:
PTHREAD_CANCEL_ENABLE:表示线程是可取消状态,可以被取消。
PTHREAD_CANCEL_DISABLE:表示线程是不可取消状态,不能被取消。
oldstate:一个指向 int 类型的变量的指针,用于存储原来的取消状态。如果不需要获取原来的状态,可以将这个参数设置为 NULL。
需要注意的是,如果线程处于不可取消状态,那么调用 pthread_cancel 函数也不能取消该线程,直到线程变为可取消状态为止。
在使用 pthread_setcancelstate 函数时,需要仔细考虑线程的取消状态以及对资源的使用,以避免可能的资源泄露和其他问题。一般来说,线程应该尽可能地保持可取消状态,以便能够及时响应取消请求。

pthread_setcanceltype 函数

pthread_setcanceltype(): 设置取消方式

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

在 POSIX 标准中,线程的取消类型(cancelability type)决定了线程在取消时执行的动作。线程的取消类型可以是异步取消(asynchronous cancellation)或者延迟取消(deferred cancellation)。
异步取消:线程在任何时候都可以被取消,并且取消请求会立即终止线程的执行。
延迟取消:线程只在特定点上被取消,也就是在线程执行到取消点时才会响应取消请求。
pthread_setcanceltype 函数可以设置线程的取消类型,它有两个参数:
type:要设置的取消类型,可以取以下两个值之一:
PTHREAD_CANCEL_ASYNCHRONOUS:表示线程的取消类型是异步取消。
PTHREAD_CANCEL_DEFERRED:表示线程的取消类型是延迟取消。
oldtype:一个指向 int 类型的变量的指针,用于存储原来的取消类型。如果不需要获取原来的类型,可以将这个参数设置为 NULL。
需要注意的是,异步取消可能会导致资源泄露和其他问题,因此在使用异步取消时需要非常小心。一般来说,推荐使用延迟取消。
在使用 pthread_setcanceltype 函数时,需要仔细考虑线程的取消类型以及对资源的使用,以避免可能的资源泄露和其他问题。

pthread_testcancel 函数

pthread_testcancel(): 本函数什么都不做,就是一个 cancel 点

#include <pthread.h>
void pthread_testcancel(void);

pthread_testcancel 函数是 pthread 线程库提供的一个函数,用于检查线程是否收到了取消请求,并在收到请求时立即取消线程。
当一个线程处于可取消状态(cancelable state)时,它可以在任何时候被取消。如果一个线程正在执行一些可能会阻塞的操作,而在等待这些操作完成之前需要检查取消请求,那么就可以使用 pthread_testcancel 函数。
pthread_testcancel 函数会检查当前线程是否收到了取消请求,如果收到了请求,就会立即取消线程。如果没有收到请求,函数将立即返回,并不会对线程产生任何影响。
需要注意的是,只有线程处于可取消状态时,才能够使用 pthread_testcancel 函数。如果线程处于不可取消状态(uncancelable state),那么取消请求将不会生效,线程也不会被立即取消。
在使用 pthread_testcancel 函数时,需要仔细考虑线程的状态以及对资源的使用,以避免可能的资源泄露和其他问题。一般来说,推荐使用 pthread_testcancel 函数来及时响应取消请求,以便尽可能地避免资源泄露和其他问题。

线程分离 pthread_detach

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

在使用 pthread 线程库创建线程时,可以使用 pthread_detach 函数将线程标记为“分离状态”,使得线程在退出时自动释放所有资源。
当一个线程被标记为“分离状态”时,它的资源(包括内存、打开的文件描述符等)将在线程结束时自动被系统回收,而不需要其他线程调用 pthread_join 函数等待该线程结束并回收其资源。
在分离状态下,如果其他线程调用 pthread_join 函数等待该线程结束,将会返回错误码 EINVAL(Invalid argument),因为该线程已经处于分离状态,不再需要其他线程等待它的结束。
需要注意的是,一旦一个线程被标记为“分离状态”,就无法再将它恢复为“非分离状态”,因此在调用 pthread_detach 函数前需要确保该线程不再需要被其他线程等待。同时,需要确保对该线程的所有资源的操作都已经完成,否则可能会出现资源泄露等问题。

线程竞争

概念推荐去看 C++并发编程部分,在此不再展开

线程竞争是指两个或多个线程尝试访问和更新同一个资源的情况。线程竞争会导致程序运行错误,这是因为当多个线程尝试同时访问同一个资源时,可能会导致数据不一致、数据丢失或其他问题。

要避免线程竞争,应该使用互斥量,它允许一个线程访问一个资源,而阻止其他线程访问该资源。如果一个线程正在使用资源,其他线程将被阻塞,直到第一个线程释放该资源。另外,还可以使用信号量来控制对资源的访问。信号量可以控制多个线程可以同时访问某个资源的数量,从而防止线程竞争的发生。

线程同步

概念推荐去看 C++并发编程部分,在此不再展开

Linux 系统编程中,线程同步是指通过确保多个线程在安全地共享资源时不会发生冲突,以确保程序正确执行的一种技术。一般来说,线程同步技术使用信号量、互斥锁、读者/写者锁、条件变量、信号处理器等方法进行实现。

信号量是一种特殊的整数,用于控制对共享资源的访问。它可以用来实现同步,以确保多个线程可以安全地访问共享资源。互斥锁是一种特殊的锁,用于保护共享资源。它可以保证只有一个线程可以对共享资源进行读写,从而避免多个线程之间的冲突。读者/写者锁也是一种特殊的锁,它可以同时允许多个线程进行读操作,但是只允许一个线程进行写操作。条件变量是一种特殊的变量,可以用来通知主线程有其他线程发生了某种变化。信号处理器也是一种特殊的处理器,用于在线程之间传递信号,以实现同步。

互斥量 pthread_mutex_t

互斥量初始化与销毁

#include <pthread.h>
int pthread_mutex_destory(pthread_mutex_t* mutex);
int pthread_mutex_init(pthread_mutex_t* restrict mutex,
                       const pthread_mutexattr_t* restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化

互斥量锁相关函数

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t* mutex);//阻塞获得锁
int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞尝试获取锁,不可获取状态则直接返回
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_timedlock

尝试获取锁,超时返回

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abstime);

简单样例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>

pthread_mutex_t mutex;

void* thread1(void* args)
{
    int ret = 0;
    int count = 0;
    struct timespec abstime;
    //设置绝对时间
    clock_gettime(CLOCK_REALTIME, &abstime);
    abstime.tv_sec += 5;//五秒后
    while(1)
    {
        printf("Thread1 try to get the lock...\n");
        ret = pthread_mutex_timedlock(&mutex, &abstime);
        if(ret != 0)
        {
            printf("Thread1 try to get the lock more than 5 second, failed!\n");
            break;
        }
        count++;
        printf("Thread1 get the lock, count = %d\n", count);
        sleep(2);
        pthread_mutex_unlock(&mutex);
        printf("Thread1 unlock the lock\n");
    }
    pthread_exit(NULL);
    return NULL;
}

pthread_once 函数

pthread_once()函数是一个多线程编程函数,用于保证某一段代码在多线程环境下只被执行一次。其原型如下:

#include <pthread.h>
int pthread_once(pthread_once_t *once_control,
           void (*init_routine)(void));
pthread_once_t once_control = PTHREAD_ONCE_INIT;

该函数接受两个参数:

once_control:一个指向 pthread_once_t 类型变量的指针,该变量用于控制某一段代码是否被执行过。该变量必须初始化为 PTHREAD_ONCE_INIT。
init_routine:一个指向函数的指针,该函数包含需要保证只被执行一次的代码。
pthread_once()函数保证在多线程环境下,init_routine()函数仅被执行一次,无论有多少个线程同时调用该函数。它利用了原子操作和互斥锁等技术来实现。

一般情况下,pthread_once()函数常常用于初始化某些全局变量,或者初始化一些只需要执行一次的操作,比如初始化某个库或系统资源等。

样例

//init函数只会被执行一次
#include <stdio.h>
#include <pthread.h>

pthread_once_t once = PTHREAD_ONCE_INIT;

void init()
{
    printf("Initialization function\n");
}

void* thread_func(void* arg)
{
    pthread_t thread_id = pthread_self();
    printf("Thread %lu is running\n", thread_id);

    pthread_once(&once, init);

    printf("Thread %lu continues execution\n", thread_id);
    return NULL;
}

int main()
{
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, thread_func, NULL);
    pthread_create(&thread2, NULL, thread_func, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

线程池

工程推荐 C++实现

https://github.com/crust-hub/tubekit/tree/main/src/thread

条件变量

在此默认您知道条件变量的概念与基本使用场景

初始化与销毁

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
           const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

等待通知

wait 就是先解锁,当 cond 满足后,即接收到通知后,会 lock mutex 锁。
传递给 pthread_cond_wait 的互斥量对条件进行保护,调用者把锁住的互斥量传给函数,函数自动将调用线程放到等待条件的线程列表上,对互斥量解锁。当 pthread_cond_wait 返回时,互斥量在此被锁住

pthread_cond_timewait 支持指定愿意等待多长时间

#include <pthread.h>
//超时返回
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                           pthread_mutex_t *restrict mutex,
                           const struct timespec *restrict abstime);
//死等
int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);

发出通知

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);//通知所有等待
int pthread_cond_signal(pthread_cond_t *cond);//通知任意一个

样例

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

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex;

void *thread_func(void *arg)
{
    pthread_t thread_id = pthread_self();
    printf("Thread %lu is running\n", thread_id);
    pthread_cond_wait(&cond, &mutex);
    printf("Thread %lu continues execution\n", thread_id);
    pthread_mutex_unlock(&mutex); // 解锁给下一个已经接收到通知的pthread_cond_wait
    return NULL;
}

int main()
{
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, thread_func, NULL);
    pthread_create(&thread2, NULL, thread_func, NULL);
    sleep(3);
    pthread_cond_signal(&cond); // 只让一个pthread_cond_wait接收到通知
    // pthread_cond_broadcast(&cond); // 让所有pthread_cond_wait收到通知
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    return 0;
}

/*
使用pthread_cond_broadcast(&cond);
gaowanlu@DESKTOP-QDLGRDB:/$ ./main
Thread 140021693155072 is running
Thread 140021684700928 is running
Thread 140021693155072 continues execution
Thread 140021684700928 continues execution
*/

/*
使用pthread_cond_signal(&cond);
gaowanlu@DESKTOP-QDLGRDB:/$ ./main
Thread 140459443291904 is running
Thread 140459434837760 is running
Thread 140459443291904 continues execution
//然后卡死了
*/

当 pthread_cond_wait 被通知后,线程会重新尝试获取与条件变量相关联的互斥锁,以便访问共享资源。

如果线程没有抢到锁,它将阻塞在 pthread_cond_wait 函数上,等待互斥锁的释放和通知。当其他线程调用 pthread_cond_signal 或 pthread_cond_broadcast 函数并释放互斥锁时,等待在条件变量上的线程将被唤醒并重新尝试获取互斥锁。

注意,当 pthread_cond_wait 函数被唤醒后,线程需要再次获取互斥锁才能访问共享资源。在获取锁之前,线程仍然是阻塞状态。

如果多个线程在等待条件变量上,当 pthread_cond_signal 或 pthread_cond_broadcast 函数被调用时,可能会有多个线程被唤醒。因此,在使用条件变量时,需要仔细考虑线程同步和竞态条件问题,以避免死锁和数据竞争等问题。

信号量

信号量是操作系统中一种用于实现进程间同步和互斥的机制。它是一个计数器,用来记录某个共享资源的可用数量,同时提供了两个原子操作:P 操作和 V 操作。

P 操作(也称为申请或等待操作)会尝试获取一个资源,如果资源的计数器为 0,则会被阻塞,直到资源变得可用。一旦获取到资源,计数器会减 1,表示该资源已被占用。

V 操作(也称为释放或信号操作)会将一个已经占用的资源释放出来,同时将计数器加 1,表示该资源现在可用。

信号量的使用可以避免多个进程同时访问同一个共享资源,从而保证数据的正确性和一致性。在实际应用中,信号量常常被用来解决生产者-消费者问题、读者-写者问题等并发控制问题。

利用互斥量以及条件变量可以封装出自己的信号量,但是第 15 章 进程间通信有专门的信号量主题,在此不再展开

读写锁 pthread_rwlock_t

pthread_rwlock_t 是一个 POSIX 线程库提供的读写锁,全称为 Pthreads Read-Write Lock。它可以用于同步多个线程之间的读和写操作。

读写锁是一种特殊的锁,它允许多个线程同时持有读锁,但是只允许一个线程持有写锁。当一个线程持有写锁时,其他线程不能持有读锁或写锁,当一个线程持有读锁时,其他线程可以持有读锁,但不能持有写锁。

#include <pthread.h>
//初始化与销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
//上锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

在读操作前

pthread_rwlock_rdlock(&rwlock);
// 读操作
pthread_rwlock_unlock(&rwlock);

在写操作时

pthread_rwlock_wrlock(&rwlock);
// 写操作
pthread_rwlock_unlock(&rwlock);

带有超时的读写锁

使用方式与互斥量的超时返回差不多

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
           const struct timespec *restrict abstime);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
           const struct timespec *restrict abstime);

自旋锁

POSIX 自旋锁是一种线程同步机制,用于实现对共享资源的互斥访问。它是由 POSIX 标准定义的,因此在 POSIX 兼容的操作系统中可用,如 Linux 和 Unix。

自旋锁是一种基于忙等待的锁,也就是说,当线程请求锁时,如果锁已经被另一个线程持有,该线程将一直循环等待,直到锁被释放。自旋锁相对于其他锁的优点在于它能够避免线程的上下文切换,从而提高锁的性能。然而,如果线程一直在循环等待锁,它将占用 CPU 资源,因此在高并发情况下,自旋锁的使用可能会降低系统的整体性能。

注意:使用自旋锁时需要保证锁的作用范围,以避免锁的滥用和竞争条件的出现。

初始化与销毁

#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
// 参数说明:
// lock:指向要初始化的自旋锁的指针。
// pshared:指定自旋锁的共享属性。如果为0,则自旋锁是进程内共享的,如果不为0,则自旋锁可以在进程间共享。 PTHREAD_PROCESS_PRIVATE or  PTHREAD_PROCESS_SHARED
// 函数返回值为0表示成功,否则表示失败。
int pthread_spin_destroy(pthread_spinlock_t *lock);

上锁与解锁

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

屏障

POSIX 屏障(barrier)是一种线程同步机制,可以用于在多个线程中同步执行某些操作。屏障会阻止线程继续执行,直到所有线程都到达了屏障点,然后所有线程都可以继续执行。这对于需要多个线程协作完成某个任务的场景非常有用。

初始化与销毁

#include <pthread.h>
int pthread_barrier_destroy(pthread_barrier_t *barrier);
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
        const pthread_barrierattr_t *restrict attr, unsigned count);
/*
参数说明:
barrier:指向要初始化的屏障的指针。
attr:指向屏障属性的指针。如果为NULL,则使用默认属性。
count:指定屏障的数目。当有count个线程到达屏障点时,它们将被释放。
函数返回值为0表示成功,否则表示失败。
*/

等待

#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);

样例

//10秒后二者才会输出内容,知道10秒后才有2个线程到达屏障点
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_barrier_t barrier;

void *thread_func1(void *arg)
{
    sleep(5);
    pthread_barrier_wait(&barrier);
    printf("thread_func1\n");
    fflush(stdout);
    return NULL;
}

void *thread_func2(void *arg)
{
    sleep(10);
    pthread_barrier_wait(&barrier);
    printf("thread_func2\n");
    fflush(stdout);
    return NULL;
}

int main()
{
    pthread_t thread1, thread2;
    pthread_barrier_init(&barrier, NULL, 2);
    pthread_create(&thread1, NULL, thread_func1, NULL);
    pthread_create(&thread2, NULL, thread_func2, NULL);
    sleep(3);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    return 0;
}