C++实战-Linux多线程(入门到精通)(二)

简介: C++实战-Linux多线程(入门到精通)(二)

线程属性

Linux下的线程属性是可以根据实际项目需求进行设置,之前我们讨论的是采用线程默认的属性。默认属性已经可以解决大多数问题。如果我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存使用从而增加最大线程数量。

主要属性:作用域、栈尺寸、栈地址、优先级、分离状态、调度策略

线程属性值不能直接设置,需要通过相关函数(可以理解为接口)进行操作:

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);        //设置线程分离的状态属性

查看线程的属性方法: man  pthread_attr_XXX

案例:

       //创建一个线程属性变量

         pthread_attr_t  attr;

       //初始化属性变量

         pthread_attr_init(&attr);

       //设置属性

       pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);

       //设置线程栈的大小

       pthread_attr_setstacksize(&attr,size);

       .....

#include <stdio.h>
#include <pthread.h>
#include <string.h>
void* func(void *arg)
{
    printf("子线程:%lu\n",pthread_self());
    return NULL;
}
int main(void)
{
    pthread_t tid;
    //创建线程属性变量
    pthread_attr_t attr;
    //初始化
    pthread_attr_init(&attr);
    //设置线程分离
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    //设置栈大小
    int size = 256*1024;
    pthread_attr_setstacksize(&attr,size);
    pthread_create(&tid,&attr,func,NULL);
    while(1)
    {
        sleep(1);
        void* retval;
        int err = pthread_join(tid,&retval);
        if(err)
            printf("-------------err= %s\n", strerror(err));
        else
            printf("-----------%d\n",(int)retval);
    }
    return 0;
}

线程使用注意事项

1.主线程退出其他线程不退出,主线程调用 pthread_exit

2.避免僵尸线程

       pthread_join

       pthread_detach

       设置线程属性为分离,然后 pthread_create

3.malloc和mmap申请的内存可以被其他线程释放

4.应避免在多线程模型中调用fork,除非马上exec。子进程中只有调用frok的线程存在,其他线程在子进程中均pthread_exit

5.信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

线程同步

先说同步的概念(不要觉得啰嗦,方便我们去理解线程同步):

       所谓同步,对于不同的研究对象而言是具有不同的含义的。例如:设备同步,是指在两个设备之间规定一个共同的时间参考。秦始皇的“书同文,车同轨”岂不也是一种同步。而在编程中的同步是指协同、协助、相互配合,主要是为了协调步骤,按预定的先后次序运行

线程同步

       同步即协同步骤,按预定的先后次序运行。大家有没有想过一个问题,为什么我们要强调按预定的先后次序,主要是因为同一个进程内的线程之间是资源共享的,加上并发的原因,假设一个线程想要修改每一个数据,还没修改完,另一个线程就把它取出,是不是就会产生问题。

       专业一点说:线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。(一个线程对某一共享的资源没有调用完,其它线程不能调用)

       

详细分析:

1.线程的主要优势在于能够通过全局变量来共享信息。不过,这种便捷(便捷是与进程间通信比较得出的)的共享是有代价的,必须确保多个线程不会同时修改同一变量或者某一线程不会读取正在由其他线程修改的变量(你会发现可以同时读)。

2.临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作。也就是同时访问同一共享资源的其他线程不应终断该片段的执行。

3.当有一个线程在对内存操作时,其他线程都不可以对这个内存地址进程操作,直到该线程完成操作,其他线程才能对该内存进行操作,而其他线程则处于等待状态

那么如何才能更好的保持这种原子操作呢?

互斥量、信号量、XXX锁....机制

不晓得有没有发现,很多时候的发展就是为了解决某一个问题。在任意条件下,很难做到十全十美,或者说很难画出一个完美的圆,我们一直在不断的创新不断的去接近这个完美的圆。好似我们永远没有算不完π一样。

互斥量(互斥锁)

先说一个通俗的理解:现在有一个房间,并且这个房间同一个时刻只能容纳一个人,防止两个人或以上的人进入,现在给这个房间置办一把锁,当有人进去时,看门的人就把房间锁上,当人出来,把锁打开。又有人进去时,把锁锁上,如此而已。其实很多计算机中解决问题的办法跟我们实际生活有很大的联系的,细细体会

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

2.互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。(我们回到上面的例子,我们管理人员每次只对一把锁打开,如果加了两把锁,那么屋子里的人出不来,屋子外的人进不去,而且管理员已经开了一把锁,他认为房间已经空了,等待人进去。导致永远阻塞在这了,呜呜...

3.一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一个共享资源会使用不同的互斥量,每一个线程在访问同一资源时将采用如下协议:

  •        针对共享资源锁定互斥量(加锁)
  •        访问共享资源(访问)
  •        对互斥量解锁(解锁)

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

举个例子:

       通过"锁"将资源的访问变长了互斥操作,而后与时间有关的错误也不会产生(按预计的次序执行)。

说明:当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

所以,互斥锁实质上是一把"建议锁"(又称为”协同锁“),建议程序中有多线程访问共享资源的时候使用该机制,但不是强制使用。(什么意思呢,就是某一个线程在访问共享资源之前,不访问锁,直接去访问共享资源,也可以访问到。我们使用互斥量需要按照规定步骤来,防止数据混乱。我直接进房间,我不管是否有管理员)

相关函数

互斥量的类型:pthread_mutex_t

pthread_mutex_init函数        

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,

                                            const pthread_mutexattr_t *restrict attr);

       作用:初始化互斥量

       参数:mutex  需要初始化的互斥量变量

                  attr       互斥量相关属性,通常传 NULL

       restrict:C语言的修饰符,被修饰的指针不能由另外一个指针进行操作

pthread_mutex_destroy函数

       int pthread_mutex_destroy(pthread_mutex_t *mutex);

       作用:释放互斥量的资源

pthread_mutex_lock函数

       int pthread_mutex_lock(pthread_mutex_t *mutex);

       作用:加锁,阻塞的(如果一个线程加锁了,那么其他线程只能阻塞等待)

pthread_mutex_trylock函数

       int  pthread_mutex_trylock(pthread_mutex_t *mutex);

       作用:尝试加锁,非阻塞(如果加锁失败,不会阻塞,会直接返回)

pthread_mutex_unlock函数

      int pthread_mutex_unlock(pthread_mutex_t *mutex);

       作用:解锁

上个案例:(目标->能够完整打印HELLO WORLD或hello world)

      不加互斥锁:

               

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;      //定义为全局变量,不能定义为栈上的临时变量
void *func(void *arg)
{
    srand(time(NULL));
    while(1)
    {
        //pthread_mutex_lock(&mutex); //加锁
        printf("hello");
        sleep(rand()%3);
        printf("world\n");
        //pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }
    return NULL;
}
int main(void)
{
    int n = 5;
    pthread_t tid;
    srand(time(NULL));  //设置随机种子
    //初始化互斥量,在创建线程之前
    pthread_mutex_init(&mutex,NULL);
    //创建线程
    pthread_create(&tid,NULL,func,NULL);
    while(n--)
    {
        //pthread_mutex_lock(&mutex); //加锁
        printf("HELLO");
        sleep(rand()%3);
        printf("WORLD\n");
        //pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }
    //销毁锁
    pthread_mutex_destroy(&mutex);
    //关闭子线程
    pthread_cancel(tid);
    //回收子线程,或者设置线程分离
    pthread_join(tid,NULL);
    //pthread_detach(tid);
    return 0;
}

      加互斥锁

 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;      //定义为全局变量,不能定义为栈上的临时变量
void *func(void *arg)
{
    srand(time(NULL));
    while(1)
    {
        pthread_mutex_lock(&mutex); //加锁
        printf("hello");
        sleep(rand()%3);
        printf("world\n");
        pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }
    return NULL;
}
int main(void)
{
    int n = 5;
    pthread_t tid;
    srand(time(NULL));  //设置随机种子
    //初始化互斥量,在创建线程之前
    pthread_mutex_init(&mutex,NULL);
    //创建线程
    pthread_create(&tid,NULL,func,NULL);
    while(n--)
    {
        pthread_mutex_lock(&mutex); //加锁
        printf("HELLO");
        sleep(rand()%3);
        printf("WORLD\n");
        pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }
    //销毁锁
    pthread_mutex_destroy(&mutex);
    //关闭子线程
    pthread_cancel(tid);
    //回收子线程,或者设置线程分离
    pthread_join(tid,NULL);
    //pthread_detach(tid);
    return 0;
}

我们的讨论到这里就结束了吗?

当然没有,我们来看一下特殊的情况

我现在把代码改成这个样子,会得到什么结果呢....

  进入了死循环,主线程竞争不到CPU了

线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁。这两个库函数本身不会阻塞。所以在这两行代码之间失去CPU的概率很小。因此,另一个线程很难得到加锁的机会。

我们再来修改代码:

发现子线程没有结束,父线程阻塞等待回收子线程

这个原因就很明显了,pthread_join会阻塞等待子线程结束,子线程进入死循环中..所以...

死锁

死锁产生的四个必要条件当时学习操作系统时(课本上定义):

1.互斥条件(我们互斥锁解决的就是互斥的共享资源,某一时刻只能有一个线程进入)

2.请求和保持条件(每一个进程都有保持现有状态)

3.不剥夺条件(没有外力的影响)

4.循环等待条件(等待其他进程释放资源)

这是课本中给我们的定义,后面加上了一些解释,方便理解。

那么在实际编程过程中的场景(主要有三种情况):

1.忘记释放锁

2.重复加锁

3.多线程多锁,抢占锁资源

(第一个种情况很好理解,这里就不过多的解释了。我们重点分析下第二种和第三种情况)

       

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

解决:访问完共享资源后立即解锁,等待步骤完成之后再加锁

       两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。(线程1拥有锁A,请求锁B,线程2拥有锁B,请求锁A)

解决:trylock替代lock函数并解锁(当不获取所有锁时主动放弃所有锁)      

上案例:

 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex1;
void* deadlock1(void *arg)
{
    pthread_mutex_lock(&mutex1);
    printf("hello");
    pthread_mutex_lock(&mutex1);
    printf("world1\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}
int main(void)
{
    pthread_t tid1;
    //初始化
    pthread_mutex_init(&mutex1,NULL);
    //创建线程
    pthread_create(&tid1,NULL,deadlock1,NULL);
    //设置线程分离
    pthread_detach(tid1);
    //退出主线程
    pthread_exit(0);
    return 0;
}
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
void* deadlock1(void *arg)
{
    pthread_mutex_lock(&mutex1);
    printf("hello");
    sleep(4);
    pthread_mutex_lock(&mutex2);
    printf("world1\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}
void* deadlock2(void *arg)
{
    // sleep(1);
    pthread_mutex_lock(&mutex2);
    printf("HELLOE");
    sleep(3);
    pthread_mutex_lock(&mutex1);
    printf("WORLD\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}
int main(void)
{
    pthread_t tid1,tid2;
    //初始化
    pthread_mutex_init(&mutex1,NULL);
    pthread_mutex_init(&mutex2,NULL);
    //创建线程
    pthread_create(&tid1,NULL,deadlock1,NULL);
    pthread_create(&tid2,NULL,deadlock2,NULL);
    //设置线程分离
    pthread_detach(tid1);
    pthread_detach(tid2);
    //退出主线程
    pthread_exit(0);
    return 0;
}
相关文章
|
2月前
|
安全 数据处理 开发者
Python中的多线程编程:从入门到精通
本文将深入探讨Python中的多线程编程,包括其基本原理、应用场景、实现方法以及常见问题和解决方案。通过本文的学习,读者将对Python多线程编程有一个全面的认识,能够在实际项目中灵活运用。
|
1月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
53 6
|
1月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
1月前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
2月前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
288 10
|
1月前
|
Unix Linux Shell
linux入门!
本文档介绍了Linux系统入门的基础知识,包括操作系统概述、CentOS系统的安装与远程连接、文件操作、目录结构、用户和用户组管理、权限管理、Shell基础、输入输出、压缩打包、文件传输、软件安装、文件查找、进程管理、定时任务和服务管理等内容。重点讲解了常见的命令和操作技巧,帮助初学者快速掌握Linux系统的基本使用方法。
77 3
|
1月前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
36 1
|
1月前
|
Java 数据处理 开发者
Java多线程编程的艺术:从入门到精通####
【10月更文挑战第21天】 本文将深入探讨Java多线程编程的核心概念,通过生动实例和实用技巧,引导读者从基础认知迈向高效并发编程的殿堂。我们将一起揭开线程管理的神秘面纱,掌握同步机制的精髓,并学习如何在实际项目中灵活运用这些知识,以提升应用性能与响应速度。 ####
49 3
|
1月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
2月前
|
Java
Java中的多线程编程:从入门到精通
本文将带你深入了解Java中的多线程编程。我们将从基础概念开始,逐步深入探讨线程的创建、启动、同步和通信等关键知识点。通过阅读本文,你将能够掌握Java多线程编程的基本技能,为进一步学习和应用打下坚实的基础。