7、Linux多线程开发

1. 线程

  • 线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是CPU处理器调度和分派的基本单位
  • 与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传统意义上的 UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程)
  • 线程是轻量级的进程LWP:Light Weight Process),在 Linux 环境下线程的本质仍是进程
  • 查看指定进程的 LWP 号:ps –Lf pid

进程和线程的区别:

  • 进程是对运行时程序的封装,是操作系统进行资源(CPU、内存等)调度和分配的基本单位。
    线程CPU调度和分配的基本单位(程序执行的最小单位)。
  • 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信(IPC)方式,在进程间进行信息交换。
  • 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。
  • 线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。
  • 创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。

线程之间共享和非共享的资源:

共享资源非共享资源
进程 ID 和父进程 ID线程 ID
进程组 ID 和会话 ID信号掩码
用户 ID 和 用户组 ID线程特有数据
文件描述符表error 变量
信号处置实时调度策略和优先级
文件系统的相关信息:文件权限掩码(umask)、当前工作目录
虚拟地址空间【除栈、.text代码段(为每个线程分配区域)】栈,本地变量和函数的调用链接信息

1. 创建线程 pthread_create

一般情况下,main函数所在的线程我们称之为主线程(main线程),其余创建的线程称之为子线程

程序中默认只有一个进程,fork() 函数调用,2个进程。
程序中默认只有一个线程,pthread_create() 函数调用,2个线程。

每个刚创建的线程,会从其创建者处继承信号掩码,即从创建该线程的线程中继承,这个新的线程可以调用 pthread_sigmask() 函数来改变它的信号掩码。

#include <pthread.h>
 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                         void *(*start_routine) (void *), void *arg);
     - 功能:创建一个子线程
     - 参数:
         - thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
         - attr : 设置线程的属性,一般使用默认值,NULL
         - start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
         - arg : 给第三个参数使用,传参
     - 返回值:
         成功:0
         失败:返回错误号。这个错误号和之前errno不太一样。
         获取错误号的信息:  char * strerror(int errnum);

 Compile and link with -pthread.
// pthread_create.c
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

struct params
{
    int key;
    int value;
    char* name;
};

void * callback(void * arg) {
    printf("child thread...\n");

    // printf("arg value 0: %d\n", *(int*)arg);    // 需要先将void* 转化为int* 然后再解引用

    // printf("arg char*: %s\n", (char*)arg);      // 传递字符串

    // int* nums = (int*)arg;
    // int size = nums[0];
    // printf("size = %d\n", size);
    // for(int i = 0; i < size; ++i){
    //     printf("num%d: %d\n", i, nums[i+1]);           // 传递数组
    // }

    struct params *st = (struct params *)arg;       // 传递结构体
    printf("key = %d.\n", st->key);
    printf("value = %d.\n", st->value);
    printf("name = %s.\n", st->name);
    
    printf("child thread exit.\n");
    return NULL;
}

int main() {
    pthread_t tid;      // 存放线程ID
    int num = 10;       // 线程传递的参数
    int nums[] = {3, 6, 7, 14};
    char* str = "hello";
    struct params *st;
    st->key = 1;
    st->value = 11;
    st->name = "struct";
    // 创建一个子线程
    // int ret = pthread_create(&tid, NULL, callback, (void*)&num);		// 数字
    // int ret = pthread_create(&tid, NULL, callback, (void*)str);		// 字符串
    // int ret = pthread_create(&tid, NULL, callback, (void*)&nums);	// 数组	
    int ret = pthread_create(&tid, NULL, callback, (void*)st);			// 结构体
    if(ret != 0) {
        char * errstr = strerror(ret);      // 将返回的错误号ret转化为错误信息文本
        printf("error : %s\n", errstr);
    } 
    for(int i = 0; i < 5; i++) {
        printf("%d\n", i);
    }
    sleep(1);   // 防止进程退出后,线程还未执行
    return 0;   // exit(0);
}

线程 pthread 不是标准系统库,是第三方库,需要通过-l指定库的名称进行链接。

 gcc pthread_create.c -o pthread_create -pthread

2. 终止线程 pthread_exit

#include <pthread.h>
void pthread_exit(void *retval);
    功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程
    参数:
        retval:需要传递一个指针,作为一个返回值,可以在 pthread_join()中获取到。

pthread_t pthread_self(void);
    功能:获取当前的线程的线程ID

int pthread_equal(pthread_t t1, pthread_t t2);
    功能:比较两个线程ID是否相等
    不同的操作系统,pthread_t类型的实现不一样,有的是无符号的长整型,有的是使用结构体去实现的。
#include <stdio.h>
#include <pthread.h>
#include <string.h>

void * callback(void * arg) {
    printf("child thread id : %ld\n", pthread_self());
    return NULL;    // pthread_exit(NULL);
} 

int main() {
    // 创建一个子线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);

    // 主线程
    for(int i = 0; i < 100; i++) {
        printf("%d\n", i);
    }

    printf("child tid : %ld, main thread id : %ld\n", tid ,pthread_self());

    // 让主线程退出,当主线程退出时,不会影响其他正常运行的线程。
    pthread_exit(NULL);

    printf("main thread exit\n");   // 主线程已退出,main 不会再往下执行

    return 0;   // exit(0);
}

3. 连接已终止的线程(回收资源)pthread_join 阻塞

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
    - 功能:和一个已经终止的线程进行连接,回收子线程的资源
           - 这个函数是【 阻塞函数 】,调用一次只能回收一个子线程
           - 一般在主线程中使用,其他线程都可以使用
    - 参数:
        - thread:需要回收的子线程的ID
        - retval: 接收子线程退出时的返回值
    - 返回值:
        0 : 成功
        非0 : 失败,返回的错误号
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

int value = 10;

void * callback(void * arg) {
    printf("child thread id : %ld\n", pthread_self());
    // int value = 10; // 局部变量,线程结束后栈空间将被释放,因此需要返回全局变量
    value = 20;
    pthread_exit((void *)&value);   // return (void *)&value;
} 

int main() {
    // 创建一个子线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);
    // 主线程
    for(int i = 0; i < 5; i++) {
        printf("%d\n", i);
    }
    printf("child tid : %ld, main thread id : %ld\n", tid, pthread_self());
    // 主线程调用pthread_join()回收子线程的资源
    int * thread_retval;	// 存放返回值,传出参数
    // 阻塞  &thread_retval 为 (int**) 类型二级指针
    ret = pthread_join(tid, (void **)&thread_retval);   
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error : %s\n", errstr);
    }
    printf("exit data : %d\n", *thread_retval);
    printf("回收子线程资源成功!\n");
    return 0; 
}

4. 线程的分离 pthread_detach

一个线程既可以将另一个线程分离,同时也可以将自己分离,一旦线程处于分离状态,就不能再使用 pthread_join() 来获取其终止状态,此过程是不可逆的,一旦处于分离状态之后便不能再恢复到之前的状态。处于分离状态的线程,当其终止后,能够自动回收线程资源。

#include <pthread.h>
int pthread_detach(pthread_t thread);
   - 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
     1. 不能多次分离,会产生不可预料的行为。
     2. 不能去连接一个已经分离的线程,会报错。
   - 参数:需要分离的线程的ID
   - 返回值:
       成功:0
       失败:返回错误号
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void * callback(void * arg) {
    printf("chid thread id : %ld\n", pthread_self());
    return NULL;
}

int main() {
    // 创建一个子线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);
    // 输出主线程和子线程的id
    printf("child tid : %ld, main thread id : %ld\n", tid, pthread_self());

    // 设置子线程分离,子线程分离后,子线程结束时对应的资源就不需要主线程释放
    ret = pthread_detach(tid);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error2 : %s\n", errstr);
    }

    // 设置分离后,对分离的子线程进行连接 pthread_join()
    // ret = pthread_join(tid, NULL);
    // if(ret != 0) {
    //     char * errstr = strerror(ret);
    //     printf("error3 : %s\n", errstr);
    // }

    pthread_exit(NULL);
    return 0;
}

5. 线程取消 pthread_cancel

#include <pthread.h>
int pthread_cancel(pthread_t thread);
    - 功能:取消线程(让线程终止)
        取消某个线程,可以终止某个属性满足可取消条件的线程的运行,(线程属性可以在创建时设置)
        但是并不是立马终止,而是当子线程执行到一个取消点,线程才会终止。
        取消点cancellation point:系统规定好的一些系统调用,我们可以粗略的理解为从用户区到内核区的切换,这个位置称之为取消点。
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void * callback(void * arg) {
    printf("chid thread id : %ld\n", pthread_self());
    for(int i = 0; i < 5; i++) {
        printf("child : %d\n", i);
        sleep(1);
    }
    return NULL;
}

int main() {
    // 创建一个子线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);

    // 取消线程
    sleep(2);
    pthread_cancel(tid);

    for(int i = 0; i < 5; i++) {
        printf("main %d\n", i);
    }
    // 输出主线程和子线程的id
    printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());
    pthread_exit(NULL);
    return 0;
}

6. 线程的属性

int pthread_attr_init(pthread_attr_t *attr);
    - 初始化线程属性变量

int pthread_attr_destroy(pthread_attr_t *attr);
    - 释放线程属性的资源

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
    - 获取线程分离的状态属性

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    - 设置线程分离的状态属性
...
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void * callback(void * arg) {
    printf("chid thread id : %ld\n", pthread_self());
    return NULL;
}

int main() {

    // 创建一个线程属性变量
    pthread_attr_t attr;
    // 初始化属性变量
    pthread_attr_init(&attr);

    // 设置属性
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // 创建一个子线程
    pthread_t tid;
    int ret = pthread_create(&tid, &attr, callback, NULL);  // 创建时设置了属性

    // 获取线程的栈的大小
    size_t size;
    pthread_attr_getstacksize(&attr, &size);
    printf("thread stack size : %ld Byte\n", size);

    // 输出主线程和子线程的id
    printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());

    // 释放线程属性资源
    pthread_attr_destroy(&attr);

    pthread_exit(NULL);
    return 0;
}

7. 线程同步

/*
    有3个窗口,一共是100张票。
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局变量,所有的线程都共享这一份资源。
int tickets = 10;

void * sellticket(void * arg) {
    // 卖票
    while(tickets > 0) {
        usleep(6000);		// 凸显线程安全问题
        printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
        tickets--;
    }
    return NULL;
}

int main() {
    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);
    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
    pthread_exit(NULL); // 退出主线程
    return 0;
}

以上代码使用多线程实现卖票,无线程同步机制,每个线程对同一个全局变量(共享资源)进行操作,将产生线程(数据)安全问题。

140159541257984 正在卖第 10 张门票
140159549650688 正在卖第 10 张门票
140159532865280 正在卖第 10 张门票
140159549650688 正在卖第 7 张门票
140159541257984 正在卖第 7 张门票
140159532865280 正在卖第 5 张门票
140159541257984 正在卖第 4 张门票
140159549650688 正在卖第 3 张门票
140159532865280 正在卖第 2 张门票
140159541257984 正在卖第 1 张门票
140159549650688 正在卖第 1 张门票
140159532865280 正在卖第 -1 张门票
  • 线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。
  • 临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作(该段代码在一个线程中必须一口气执行完成,不会被其他线程抢占CPU,类似串行),也就是同时访问同一共享资源的其他线程不应中断该片段的执行
  • 线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。(针对临界区的代码进行同步,降低了并发执行的效率,但是保证了数据安全性

7.1 互斥锁 / 互斥量 pthread_mutex_t

为避免线程更新共享变量时出现问题,可以使用互斥量mutex 是 mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源互斥)。可以使用互斥量来保证对任意共享资源的原子访问

互斥量有两种状态已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。

一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量

每一线程在访问同一资源时将采用如下协议(步骤):

  1. 针对共享资源锁定互斥量
  2. 访问共享资源
  3. 对互斥量解锁

如果多个线程试图执行这一块代码(临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:

在这里插入图片描述
互斥量的属性包括两个:进程共享属性和类型属性。

  • 进程共享属性包括两个值:

PTHREAD_PROCESS_PRIVATE(默认):即进程间不共享,只有同一进程的多个线程可以访问同一个同步对象;
PTHREAD_PROCESS_SHARED:即进程间共享,从多个进程共享的内存区域中分配的互斥量就可以用于这些进程的同步。(共享内存允许相互独立的多个进程把同一个内存区域映射它们各自独立的地址空间中;就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。)

pthread_mutexattr_setpshared 可用来设置互斥锁变量的作用域;
pthread_mutexattr_getpshared 可用来返回所定义的互斥锁变量的范围。

  • 类型属性包括四个值:

PTHREAD_MUTEX_NORMAL:标准互斥锁:第一次上锁成功,第二次上锁会阻塞;
PTHREAD_MUTEX_ERRORCHECK:检错互斥锁:第一次上锁成功,第二次上锁会出错;
PTHREAD_MUTEX_RECURSIVE:递归互斥锁:第一次上锁成功,第二次上锁也会成功,内部计数;
PTHREAD_MUTEX_DEFAULT:默认互斥锁:同标准互斥锁。

pthread_mutexattr_settype可用来设置互斥锁的 type 属性;
pthread_mutexattr_gettype 可用来获取所设置的互斥锁的 type 属性。

pthread_mutex_t	mutex; 	// 互斥量的类型 
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    - 初始化(创建)互斥量
    - 参数 :
        - mutex : 需要初始化的互斥量变量
        - attr : 互斥量相关的属性,NULL
    - restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
        pthread_mutex_t *restrict mutex = xxx;
        pthread_mutex_t * mutex1 = mutex;	// 无法操作

int pthread_mutex_destroy(pthread_mutex_t *mutex);
    - 释放互斥量的资源

int pthread_mutex_lock(pthread_mutex_t *mutex);
    - 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待

int pthread_mutex_trylock(pthread_mutex_t *mutex);
    - 尝试加锁,如果加锁失败,不会阻塞,会直接返回。

int pthread_mutex_unlock(pthread_mutex_t *mutex);
    - 解锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局变量,所有的线程都共享这一份资源。
int tickets = 500;		// 票数多能够更好看出对比

// 创建一个互斥量,全局变量,避免丢失
pthread_mutex_t mutex;

void * sellticket(void * arg) { // 卖票
    // 加锁
    pthread_mutex_lock(&mutex);
    // 临界区
    while(tickets > 0) {
        usleep(6000);
        printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
        tickets--;
    }
    // 解锁
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    // 初始化全局互斥量
    pthread_mutex_init(&mutex, NULL);

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
	
    pthread_mutex_destroy(&mutex);	// 释放互斥量资源
    pthread_exit(NULL); // 退出主线程
    return 0;
}

以上代码将while段的代码作为临界区进行保护,会导致一个线程加锁后,直到所有票卖完退出循环才解锁互斥量,相当于只有一个线程。

因此作如下改进,使临界区在循环内进行保护,此时三个线程将并发执行卖票任务:

void * sellticket(void * arg) { // 卖票
    while(1) {
        // 加锁
        pthread_mutex_lock(&mutex);
        // 临界区
        if(tickets > 0) {
            usleep(6000);
            printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
            tickets--;
        }else {
            // 解锁
            pthread_mutex_unlock(&mutex);
            break;	// 退出循环
        }
        // 解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

7.2 死锁 deadlock

有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。

死锁是指在多道程序系统中,一组进程中的每一个进程都无限期等待被该组进程中的另一个进程所占有且永远不会释放的资源

多个进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 此时称系统处于死锁状态或系统产生了死锁。

死锁的发生必须同时满足四个条件

  • 互斥: 至少有一个资源必须属于非共享模式, 即一次只能被一个进程使用; 若其他申请使用该资源, 那么申请进程必须等到该资源被释放为止
  • 占有并等待: 一个进程必须占有至少一个资源, 并等待另一个资源, 而该资源为其他进程所占有
  • 非抢占: 进程不能被抢占, 即资源只能被进程在完成任务后自愿释放
  • 循环等待: 若干进程之间形成一种头尾相接的环形等待资源关系

产生死锁的几种场景:

  • 未解锁(拿了锁的线程重新加锁也无法进入锁内)
  • 重复加锁(同一个线程内调用多次加锁操作,会阻塞在第二次加锁操作中)
  • 多线程多锁,抢占锁资源(如图)
    在这里插入图片描述

(1)未解锁

只卖出一次票,所有线程都阻塞在加锁上。

void * sellticket(void * arg) {
    while(1) {
        // 加锁
        pthread_mutex_trylock(&mutex);
        if(tickets > 0) {
            usleep(6000);
            printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
            tickets--;
        }else {
            // 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return NULL;
}

(2)重复加锁

一张票都没卖出,第一个线程阻塞在第二次加锁,其他线程阻塞在第一次加锁。

void * sellticket(void * arg) {
    while(1) {
        // 加锁
        pthread_mutex_lock(&mutex);
        pthread_mutex_lock(&mutex);
        if(tickets > 0) {
            usleep(6000);
            printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
            tickets--;
        }else {
            // 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
        // 解锁
        pthread_mutex_unlock(&mutex);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

(3)多线程多锁

两个线程都阻塞在加锁时,无输出。

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

// 创建两个互斥量
pthread_mutex_t mutex1, mutex2;

void* work1(void* arg){
    // 先加锁1,然后被阻塞在加锁2
    pthread_mutex_lock(&mutex1);
    sleep(1);
    pthread_mutex_lock(&mutex2);

    printf("work 1 ...\n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);

    return NULL;
}

void* work2(void* arg){
    // 先加锁2,然后被阻塞在加锁1
    pthread_mutex_lock(&mutex2);
    sleep(1);
    pthread_mutex_lock(&mutex1);

    printf("work 2 ...\n");

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);

    return NULL;
}

int main(){
    // 互斥量初始化
    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);
    // 创建线程
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, work1, NULL);
    pthread_create(&tid2, NULL, work2, NULL);
    // 回收线程
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    // 释放互斥量
    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);
    return 0;
}

7.3 读写锁

  • 互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。

  • 但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

  • 在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

  • 读写锁的特点:
    (1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。(读时不阻塞读,但阻塞写
    (2)如果有其它线程写数据,则其它线程都不允许读、写操作。(写时阻塞读写
    (3)写是独占的,写的优先级高。(在未锁的状态下,先允许写锁的线程加锁)
    (4)当读写锁在读加锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,避免读模式锁长期占有,而等待的写模式锁请求一直得不到满足,出现饿死情况。

读写锁和互斥量(互斥锁)很类似,是另一种线程同步机制,但不属于POSIX标准,可以用来同步同一进程中的各个线程。当然如果一个读写锁存放在多个进程共享的某个内存区中,那么还可以用来进行进程间的同步。

读写锁也称为共享-独占(shared-exclusive)锁,当读写锁以模式加锁时,它是以共享模式锁住,当以模式加锁时,它是以独占模式锁住。读写锁非常适合读数据的频率远大于写数据的频率从的应用中。

在Linux中,读写锁只有一个属性,那便是进程共享属性,它与互斥锁以及自旋锁的进程共享属性相同。

函数 pthread_rwlockattr_getpshared() 用于获取共享属性,函数 pthread_rwlockattr_setpshared() 用于设置共享属性。

pthread_rwlock_t rwlock;	// 读写锁的类型
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,	// 初始化
							 const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);		// 释放
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);		// 读锁
int pthread_rwlock_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);		// 解锁

示例:8个线程操作同一个全局变量, 3个线程不定时写这个全局变量,5个线程不定时的读这个全局变量。

利用读写锁,可以允许多个线程同时读数据。

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

// 创建一个共享数据
int num = 1, out = 0;
pthread_rwlock_t rwlock;

void * writeNum(void * arg) {
    while(1) {
        pthread_rwlock_wrlock(&rwlock);
        num++;
        printf("++write, tid : %ld, num : %d\n", pthread_self(), num);
        pthread_rwlock_unlock(&rwlock);
        if(num > 1000){
            out = 1;
            break;
        }
        usleep(100);
    }
    return NULL;
}

void * readNum(void * arg) {
    while(1) {
        pthread_rwlock_rdlock(&rwlock);
        printf("===read, tid : %ld, num : %d\n", pthread_self(), num);
        pthread_rwlock_unlock(&rwlock);
        if(out == 1) break;
        usleep(100);    // 不sleep,当前线程就会一直占用cpu,其他线程也拿不到时间片,没法运行
    }
    return NULL;
}

int main() {
   pthread_rwlock_init(&rwlock, NULL);
    // 创建3个写线程,5个读线程
    pthread_t wtids[3], rtids[5];
    for(int i = 0; i < 3; i++) {
        pthread_create(&wtids[i], NULL, writeNum, NULL);
    }
    for(int i = 0; i < 5; i++) {
        pthread_create(&rtids[i], NULL, readNum, NULL);
    }
    // 设置线程分离(不阻塞)
    for(int i = 0; i < 3; i++) pthread_detach(wtids[i]);
    for(int i = 0; i < 5; i++) pthread_detach(rtids[i]);

    pthread_exit(NULL); // 退出主线程,但非关闭进程
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

7.4 条件变量 pthread_cond_t

生产者和消费者模型:

生产者 -> 容器 -> 消费者

容器满了的时候,生产者无法进行生产,应该通知消费者进行消费,然后等待,而不是一直循环尝试生产;
容器空了的时候,消费者无法消费,应该通知生产者进行生产然后等待,而不是一直循环尝试消费。

为了实现条件通知功能,可以使用条件变量 pthread_cond_t。

条件变量也仅有进程共享属性
条件变量进程共享属性查询与修改:pthread_condattr_getpshared()、pthread_condattr_setpshared()。

pthread_cond_t cond; // 条件变量的类型
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
	- 初始化
int pthread_cond_destroy(pthread_cond_t *cond);
	- 销毁
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
    - 等待,调用了该函数,线程会阻塞,会对互斥锁进行解锁;当不阻塞了,继续向下执行,会重新加锁。
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
    - 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。
int pthread_cond_signal(pthread_cond_t *cond);
    - 唤醒一个或者多个等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
    - 唤醒所有的等待的线程
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;

struct Node{        // 链表结构
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {
    // 不断的创建新的节点,添加到链表中
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;   // 头部插入
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        
        // 只要生产了一个,就通知消费者消费
        pthread_cond_signal(&cond);

        pthread_mutex_unlock(&mutex);
        usleep(100);
    }
    return NULL;
}

void * customer(void * arg) {
    while(1) {
        pthread_mutex_lock(&mutex);
        // 判断是否有数据
        if(head != NULL) {  // 有数据
            struct Node * tmp = head;   // 保存头结点的指针
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
        } else {
            // 没有数据,需要等待
            // 当这个函数调用阻塞的时候,会对互斥锁进行解锁,
            // 当不阻塞了,继续向下执行,会重新加锁。
            pthread_cond_wait(&cond, &mutex);  
        }
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }
    return  NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];
    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }
    // 线程分离
    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }
    pthread_exit(NULL);
    return 0;
}

7.5 信号量 sem_t

信号量(semaphore)是操作系统用来解决并发中的(进程或者线程)互斥和同步问题的一种方法。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待和发送信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。

  • 本质:计数器。当前资源个数。
  • 生命周期:随内核
  • 本身不具有数据交换的功能,是通过控制其他通信资源(文件、外部设备)来实现进程间通信,本身是一种外部资源的标识。在此过程中负责数据操作的互斥与同步功能。
  • 互斥:独占临界资源(排他性)
  • 同步:建立在互斥基础上(顺序性)
  • 主要作用:协调进程对共享资源的访问,保证在任一时刻,只有一个执行线程访问代码的临界区域。(其中共享内存的使用就要用到信号量)。
  • 操作:
    p操作:等待,申请资源(以信号量集为单位申请)
    v操作:发送,释放资源

对于信号量的值 n(允许进入临界区的线程/进程数):
n > 0:当前有可用资源,可用资源数量为 n
n = 0:资源都被占用,可用资源数量为 0
n < 0:资源都被占用,并且还有 n 个进程正在排队

信号量可以但不一定实现互斥(不是说不能,一种情况是不存在共享临界区,谈不上互斥,另一种情况是允许共同进入临界区,比如读操作),肯定实现了同步

sem_t sem; 	// 信号量的类型
int sem_init(sem_t *sem, int pshared, unsigned int value);
    - 初始化信号量
    - 参数:
        - sem : 信号量变量的地址
        - pshared : 0 用在线程间 ,非0 用在进程间
        - value : 信号量中的值
int sem_destroy(sem_t *sem);
    - 释放资源
int sem_wait(sem_t *sem);
    - 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
    - 对信号量解锁,调用一次对信号量的值+1
int sem_getvalue(sem_t *sem, int *sval);
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {       // 生产者调用
    // 不断的创建新的节点,添加到链表中
    while(1) {
        sem_wait(&psem);    // 等待生产者信号量>0,有容量可以生产,生产者信号量-1
        pthread_mutex_lock(&mutex);     // 互斥锁
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;           // 头结点插入
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);   // 解互斥锁
        sem_post(&csem);    // 消费者信号量+1,可消费数据+1
    }

    return NULL;
}

void * customer(void * arg) {       // 消费者调用
    while(1) {
        sem_wait(&csem);    // 等待消费者信号量>0,有数据可以消费,消费者信号量-1
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;       // 取出头结点(后进先出)
        head = head->next;
        printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);
        sem_post(&psem);    // 生产者信号量+1,可生产容量+1
    }
    return  NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);
    sem_init(&psem, 0, 8);      // 初始化生产者信号量大小为8(容量 > 0,可以生产)
    sem_init(&csem, 0, 0);      // 初始化消费者信号量大小为0(还未生产,需要等待)

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];
    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }
    // 线程分离
    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    pthread_exit(NULL);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值