Linux多线程

 


待我慢慢总结...

又是一个周末呀,明天又要开始码论文了,真是毫无意义......,这群老师真是浪费国家科研经费....


参考文章:

[1] 线程与进程,多线程,多进程,线程并发,线程并行

[2] 《嵌入式Linux开发教程(上册)》


1、多线程概述

线程(thread)是包含在进程内部的顺序执行,是进程中的实际运作单位,也就是系统能够调度的最小单位。一个进程中可以并发多条线程,每条线程并发执行不同任务。

  • 并发

 并发是把CPU运行时间划分成若干个时间段,每个时间段再分配给各个线程执行,当一个线程在运行时,其它线程处于挂起状。从宏观角度是同时进行的,但从微观角度并不是同时进行。

     CPU根据线程调度算法来切换线程。当正在执行的一个线程需要进行IO操作或者需要访问内存的时候,CPU完全可以放弃该线程,转而调度线程就绪队列上的其他线程,被放弃的线程则进入阻塞状态,IO操作或者访问内存操作结束之后,该线程可以进入线程就绪队列上。

     典型的线程调度算法:(1) FIFO算法。在非抢占式系统中,所有的线程构成一个先进先出队列,最先进入队列的线程获得CPU,等到放弃处CPU时,又回到队列尾部,下一个线程继续执行。若有新的线程进来,则添加到队列尾部。(2) 时间片轮转调度算法。(3) 优先级调度算法

 

  • 并行

并行是同一时刻当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,是真正意义上的不同线程在同一时刻同时执行。

引入线程的好处:

  1. 线程占用资源要比进程少的多
  2. 创建一个新的线程花费的代价小
  3. 切换线程方便
  4. 提高并发性

多线程举例:比如用浏览器,同时进行浏览网页、播放视频、下载资源、听音乐等操作

多线程缺点: 

  1. 多线程比多进程成本低,不过性能也更低
  2. 一个线程的崩溃可能影响到整个程序的稳定性
  3. 线程多了之后,线程本身的调度也麻烦,需要消耗较多的CPU
  4. 无法直接获取系统的资源,总体能够达到的性能上限有限制
  5. 线程之间的同步和加锁控制比较麻烦

同一进程的线程共享哪些资源?

  • :由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的)。
  • 全局变量:它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的。
  • 局部静态变量:虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的。
  • 文件等公用资源:这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。

线程独享的资源?

  •  :栈是独享的
  • 寄存器:这个可能会误解,因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC
  • 线程ID

线程与进程的关系:

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有个主线程;
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
  • 线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
  • 进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源;
  • 在创建或者撤销进程时,由于系统要为之分配和回收资源,因此导致系统的开销大于创建或撤销线程时的开销。

为什么需要多线程?

  • 方便通信和数据交流
    • 对于进程来说,他们具有独立的数据空间,要进行数据的穿肚只能通过通信的方式进行,这种方式费时且不便;
    • 同一进程下的线程共享数据空间,所以一个线程的数据可以直接为其他线程所使用,快捷且方便。
  • 更高效地利用CPU
    • 对图形界面的程序尤其有意义,当一个操作耗时很航,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作。而使用多线程技术,将耗时长的操作置于一个新的线程,就可以避免这种尴尬的情况
    • 同时,多线程使用CPU系统更加高效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。(并行

2、POSIX Threads概述

POSIX Threads(通常称为Pthreads)定义了创建和操作线程的一套API接口,一般用于UNIX-like POSIX系统中(如FreeBSD、GNU\Linu、OpenBSD、Mac Os等系统)。

Pthreads接口可以根据功能划分为四个组:

  • 线程管理
  • 互斥量
  • 条件变量
  • 同步

编写Pthreads多线程程序时,源码只要包含pthread.h头文件就可以使用Pthreads库中的所有类型和函数:

#incldue <pthread.h>

在编译Pthreads程序时在编译和链接过程中需要加上-pthread参数。

3、线程管理

线程管理包含了线程的创建终止等待分离设置属性等操作。

3.1 线程ID

线程ID可以看作线程的句柄,用来引用一个线程。

Pthreads线程有一个pthread_t类型的ID,线程可以用过pthread_self()函数来获取自己的ID。pthread_self()函数原型如下:

pthread_t pthread_self(void);

比较两个线程ID是否相等函数:(因为pthread_t可能是结构体)

int pthread_equal(pthread_t t1,pthread_t t2);

如果t1等于t2,则该函数返回一个非0值,否则返回0.

3.2 线程创建和终止

3.2.1 线程创建

在进程中创建一个新线程的函数是:(线程创建后会立即运行)

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

返回值说明:

如果函数成功返回0,否则返回一个非0的错误码

参数说明:

  • thread:新创建的线程ID
  • attr:表示一个封住了线程的各种属性的属性对象,如果attr为NULL,则新线程使用默认的属性
  • start_routine线程开始执行时调用的函数名字,start_routine函数有一个指向void的指针参数,并由pthread_create第四个参数arg指定值,同时start_routine函数返回一个指向void的指针,这个值被pthread_join当做退出状态处理
  • arg为参数start_routine指定函数的参数

3.2.2 线程终止

  • 终止线程可以直接调用exit()、执行main()中的return或者通过进程的某个其他线程调用exit()来实现。所有线程都会终止
  • 调用exit()会使整个进程终止;
  • 调用pthread_exit()(或者说是pthread_eixt(NULL))只会使调用的线程终止,如果进程的最后一个线程调用了pthread_exit(),则进程会带着状态返回值0退出
    • void pthread_exit(void * retval);
      • retval 是一个void类型的指针,可以将线程的返回值当做pthread_exit()的参数传入

关于如何获取Linux系统当前时间:Linux编程--获取当前时间 Linux 下c获取当前时间(精确到秒和毫秒或者微秒)

#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
/*线程1:函数*/
int number = 0;
void saythread1(){
        time_t timep;
        time(&timep);

        struct timeval tv;
        gettimeofday(&tv, NULL);
        number++;
        printf("======================\n");
        printf("time is %s",ctime(&timep));
        printf("microsecond = %ld\n",tv.tv_sec*1000000 + tv.tv_usec);
        printf("i am thread 1 fuction!\n");
        printf("thread1's number = %d\n",number);

        pthread_exit(NULL);
}
/*线程2:函数*/
void saythread2(){
        number++;
        time_t timep;
        time(&timep);

        struct timeval tv;
        gettimeofday(&tv, NULL);
        printf("======================\n");
        printf("time is %s",ctime(&timep));
        printf("microsecond = %ld\n",tv.tv_sec*1000000 + tv.tv_usec);
        printf("i am thread 1 fuction!\n");
        printf("i am thread 2 fuction!\n");
        printf("thread2's number = %d\n",number);
        pthread_exit(NULL);
}
int main(char argc,char **argv){
        int ret1,ret2;
        pthread_t thread1,thread2;
        printf("this is a thread test!\n");
        /* 创建线程1 */
        //使用(void*)saythread1也可以正常编译,输出,为什么?
        ret1 = pthread_create(&thread1,NULL,(void*)saythread1,(void*)NULL);
        if(ret1 != 0)
                printf("thread1 create error!\n");
        /* 创建线程2 */
        printf("number = %d\n",number);
        ret2 = pthread_create(&thread2,NULL,(void*)saythread2,(void*)NULL);
        if(ret2 != 0)
                printf("thread2 create error!\n");
        printf("main thread exit!\n");
        pthread_exit(NULL);
        return 0;
}

编译:

gcc -o pthreadtest1 pthreadtest1.c -pthread

不加-pthread进行编译会发生错误:

输出:

this is a thread test!
number = 0
main thread exit!
======================
time is Sun May 26 21:40:38 2019
microsecond = 1558878038112226
i am thread 1 fuction!
i am thread 2 fuction!
thread2's number = 1
======================
time is Sun May 26 21:40:38 2019
microsecond = 1558878038115158
i am thread 1 fuction!
thread1's number = 2

以上程序看似是先执行完主线程、然后执行线程2,最后执行线程1.这是因为每个线程执行时间小于系统调度的最小时间片,所以导致线程没有并发执行,只需要在printf后面加一个延时函数,人为增加线程执行所需时间就能看到系统对线程之间的调度

#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>
/*线程1:函数*/
int number = 0;
void saythread1(){
        time_t timep;
        time(&timep);
        struct timeval tv;
        gettimeofday(&tv, NULL);
        number++;
        printf("time is %s",ctime(&timep));
        sleep(1);
        printf("microsecond = %ld\n",tv.tv_sec*1000000 + tv.tv_usec);
        sleep(1);
        printf("i am thread 1 fuction!\n");
        sleep(1);
        printf("thread1's number = %d\n",number);

        pthread_exit(NULL);
}
/*线程2:函数*/
void saythread2(){
        number++;
        time_t timep;
        time(&timep);
        struct timeval tv;
        gettimeofday(&tv, NULL);
        printf("time is %s",ctime(&timep));
        sleep(1);
        printf("microsecond = %ld\n",tv.tv_sec*1000000 + tv.tv_usec);
        sleep(1);
        printf("i am thread 2 fuction!\n");
        sleep(1);
        printf("thread2's number = %d\n",number);
        pthread_exit(NULL);
}
int main(char argc,char **argv){
        int ret1,ret2;
        pthread_t thread1,thread2;
        printf("this is a thread test!\n");
        /* 创建线程1 */
        ret1 = pthread_create(&thread1,NULL,(void*)saythread1,(void*)NULL);
        if(ret1 != 0)
                printf("thread1 create error!\n");
        /* 创建线程2 */
        printf("number = %d\n",number);
        ret2 = pthread_create(&thread2,NULL,(void*)saythread2,(void*)NULL);
        if(ret2 != 0)
                printf("thread2 create error!\n");
        printf("main thread exit!\n");
        pthread_exit(NULL);
        return 0;
}

输出:

注意:由于操作系统线程调度的随机性,多线程范例程序实际执行结果可能与本案例有所不同。

3.3 连接与分离

线程可以分为分离线程(DETACHED)非分离线程(JOINABLE)两种:

  • 分离线程:指程序退出时会释放其资源的线程;
  • 非分离线程:退出后不会马上释放资源,需要另一个线程为其调用pthread_join函数或者进程退出时才会释放资源。

只有非分离线程才是可连接的,分离线程退出时不汇报告它的退出状态。

3.3.1 线程分离

pthread_detach()函数可以将非分离线程设置为分离线程,其函数原型如下:

pthread_detach(pthread_t thread)
  • 参数thread是要设置为分离的线程ID。
  • 函数成功返回0,否则返回一个非0的错误码

线程可以由其他线程来设置分离,也可以由自己设置分离(使用pthread_detach(pthread_self())

3.3.2 线程连接

如果一个线程是非分离的线程,那么其他线程可以调用pthread_detach()函数对其进行连接,pthread_join()函数原型如下:

int pthread_join(pthread_t thread, void * retval);
  • pthread_join()函数将调用的线程挂起,直到参数thread指定目标线程终止运行为止。
  • 参数retval为指向线程返回值的指针提供一个位置,这个返回值是目标线程通用pthread_exit()或者return所提供的值。当目标线程无需返回的时候可以使用NULL值,调用线程如果不需要对目标线程的返回状态进行检查可直接将retval幅值为NULL。
  • 如果pthread_join()成功调用,函数返回0,如果不成功,返回一个错误码
  • 调用该函数的线程会一直阻塞,直到指定的tid线程退出,如果这个指定的线程通过return返回或pthread_exit()退出,那么这个线程的退出码会被保存到第二个参数rval里面。如果这个线程是被取消的,这时就没有退出码,rval就会被置为PTHREAD_CANCELED。
  • 调用pthread_join()会是指定的线程处于分离状态,如果指定的线程已经处于分离状态就会调用失败
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>
/*线程1:函数*/
int number = 0;
void saythread1(){
        time_t timep;
        time(&timep);
        struct timeval tv;
        gettimeofday(&tv, NULL);
        number++;
        printf("time is %s",ctime(&timep));
        sleep(1);
        printf("microsecond = %ld\n",tv.tv_sec*1000000 + tv.tv_usec);
        sleep(1);
        printf("i am thread 1 fuction!\n");
        sleep(1);
        printf("thread1's number = %d\n",number);

        pthread_exit(NULL);
}
/*线程2:函数*/
void saythread2(){
        number++;
        time_t timep;
        time(&timep);
        struct timeval tv;
        gettimeofday(&tv, NULL);
        printf("time is %s",ctime(&timep));
        sleep(1);
        printf("microsecond = %ld\n",tv.tv_sec*1000000 + tv.tv_usec);
        sleep(1);
        printf("i am thread 2 fuction!\n");
        sleep(1);
        printf("thread2's number = %d\n",number);
        pthread_exit(NULL);
}
int main(char argc,char **argv){
        int ret1,ret2;
        pthread_t thread1,thread2;
        printf("this is a thread test!\n");
        /* 创建线程1 */
        ret1 = pthread_create(&thread1,NULL,(void*)saythread1,(void*)NULL);
        if(ret1 != 0)
                printf("thread1 create error!\n");
        /* 创建线程2 */
        printf("number = %d\n",number);
        ret2 = pthread_create(&thread2,NULL,(void*)saythread2,(void*)NULL);
        if(ret2 != 0)
                printf("thread2 create error!\n");
        /*****************************************************
        ******************************************************
         * 等待线程1 结束
        ******************************************************
        ******************************************************/
        pthread_join(thread1,NULL);         
        printf("main thread exit!\n");
        pthread_exit(NULL);
        return 0;
}

 

主线程等待线程1结束运行,待线程1结束运行之后,主线程继续运行。

注意:为了防止内存泄露,长时间运行的程序最终应该为每一个线程调用pthread_detach()或者pthread_join()

3.4 线程属性

线程的属性包括:

  • 栈大小
  • 调度策略
  • 线程状态

通常先创建一个属性对象,然后在属性对象上设置属性的值,再将属性对象传给pthread_create函数的第二个参数用来创建含有该属性的线程。

一个属性对象可以多次传给pthread_create()函数,以创建多个含有相同属性的线程

3.4.1 属性对象

(1)初始化属性对象

首先看一下线程创建函数:

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

线程属性参数是:const pthread_attr_t * attr

通常,我们使用pthread_attr_init()函数来将属性对象使用默认值进行初始化,该函数原型如下:

int pthread_attr_init(pthread_attr_t *attr);

函数只有一个参数,是一个指向pthread_attr_t的属性对象的指针。

函数成功返回0,否则返回一个非零的错误码.

(2)销毁属性对象

销毁属性对象使用pthread_attr_destroy()函数,函数的原型是:

int pthread_attr_destroy(pthread_arrt_t *attr);

函数只有一个参数,是一个指向pthread_attr_t的属性对象的指针。

函数成功返回0,否则返回一个非零的错误码.

3.4.2 线程状态

线程具有两种状态,分别是:

  • PTHREAD_CREATE_JOINABLE:非分离线程
  • PTHREAD_CREATE_DETACHED:分离线程

(1)获取线程状态

获取线程状态的函数一般是pthread_attr_getdetachstate(),其原型如下:

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
  • attr是一个指向已初始化对象的指针;
  • detachstate是所获状态值的指针。

函数成功返回0,失败返回一个非0的错去码。

(2)设置线程状态

用来获取线程状态的函数是:pthread_attr_setdetachstate(),其原型如下:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
  • attr是一个指向已初始化对象的指针;
  • detachstate是要设置的值。

函数成功返回0,失败返回一个非0的错去码。

3.4.3 线程栈

[1] 栈的作用

每个线程都有一个独立的调用栈,线程的栈大小在线程创建的时候就已经固定下来了。Linux系统线程默认栈的大小为8MB只有主线程的栈大小会在运行过程中自动增长,用户可以通过属性对象来设置和获取栈的大小。

(1)获取线程栈

获取线程栈大小的函数是:pthread_atttr_getstacksize(),原型如下:

int pthread_attr_getstachsize(pthread_attr_t *attr, size_t *stacksize)
  • attr是一个指向已初始化对象的指针;
  • stacksize是所获栈大小的指针。

函数成功返回0,失败返回一个非0的错去码。

(2)设置线程栈

设置线程栈大小的函数是:pthread_atttr_setstacksize(),原型如下:

int pthread_attr_setstachsize(pthread_attr_t *attr, size_t *stacksize)
  • attr是一个指向已初始化对象的指针;
  • stacksize是需要设置栈的大小。

函数成功返回0,失败返回一个非0的错去码。

 

4、线程安全

        多线程编程环境中,多个线程同时调用 某些函数可能会产生错误的结果,这些函数成为非线程安全函数。如果库函数能够在多个线程中同时执行并且互不干扰,那么这些库函数就是线程安全(thread_safe)函数。

POSIX.1-2008规定除了下表中的特定函数以外,所有标准库的函数都应该是线程安全函数。有些库函数虽然不是线程安全函数,但系统有后最为_r的线程安全版本,如strtok_r。

 

5、互斥量

5.1 临界区

在计算机系统中有许多共享资源不允许用户并行使用,例如打印设备,如果它同时打印两份文件,就会产生交错,从而无法获得正确的文档。像打印机这样额共享设备被称为排它性资源,因为他一次只能有一个执行流访问。执行流必须以互斥的方式执行访问排它性资源的代码。

临界区是必须以互斥方式执行的代码段,也就是说,在临界区的范围内只能有一个活动的执行线程

5.2 什么是互斥量

互斥量(Mutex)又称为互斥锁,是一种用来保护临界区的特殊变量,它可以处于锁定状态,也可以处于解锁状态:

  • 如果互斥锁是锁定的,就是某个线程正持有这个互斥锁;
  • 如果没有线程持有这个互斥锁,那么这个互斥锁就处于解锁状态

每个互斥锁内部都有一个线程等待队列,用来保存等待该互斥锁的线程。

  • 当互斥锁出于解锁状态,一个线程试图获取这个互斥锁时,这个线程就可以得到这个互斥锁而不会阻塞
  • 当互斥锁出于锁定状态,一个线程试图获取这个互斥锁时,这个线程将阻塞在互斥锁的等待队列

互斥量有最简单、最有效的线程同步机制。程序可以用它来保护临界区,以获得对排它性资源的访问权。另外,互斥量只能被短时间的持有,使用完临界资源后应该立即释放锁。

5.3 互斥量的创建和销毁

5.3.1 创建互斥量

pthread使用pthread_mutex_t类型的变量来表示互斥量,同时在使用互斥量进行通不值钱需要对它进行初始化,可以使用静态方式或者动态方式对互斥量队形初始化。

(1)静态方式:使用PTHREAD_MUTEX_INITIALIZER

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

(2)动态方式

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • 参数mutex是一个要初始化的互斥量指针;
  • 参数attr传递NULL来初始化一个带有默认属性的互斥量,否则就要用类似于线程属性对象所使用的方法,先创建互斥量属性对象,再用该属性对象来创建互斥量

函数成功返回0,否则返回一个非0的错误码。

静态初始化程序往往比调用pthread_mutex_init()函数更加有效,而且在任何线程开始执行之前,都要确保互斥锁被初始一次。

5.3.2 销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数mutex是指要销毁的互斥量指针。

5.4 互斥量加锁与解锁

5.4.1 加锁

线程试图访问互斥锁的过程称之为加锁

Pthreads中有两个试图访问互斥锁的函数,分别是pthread_mutex_lock()、pthread_mutex_trylock()

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);

pthread_mutex_lock()函数会一直阻塞直到互斥锁可用为止,pthread_mutex_trylock()则会尝试加锁,但通常会立即返回。

参数mutex是需要加锁的互斥量。函数成功返回0,失败返回个非0 的错误码。

其中在一个线程已持有锁的情况下,调用pthread_mutex_trylock()函数是错误码为EBUSY

5.4.2 解锁

解锁是线程将互斥量有锁定状态变为解锁状态。

pthread_mutex_unlock(pthread_mutex_t *mutex);

参数mutex是需要解锁的互斥量,函数成功返回0,否则返回一个非0的错误码。

只有在线程进入了临界区之前正确的获取了适当的互斥量,才能在线程离开临界区时释放互斥量。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t lock;
void *printftest(void *argv){
        int* id = (int*)argv;
        //加锁
        pthread_mutex_lock(&lock);
        for(int i = 0;i < 5;i++){
                printf("pthread %d starts!\n",*id);
                sleep(1);
        }
        //解锁
        pthread_mutex_unlock(&lock);
}

int main(){
        printf("test demo is running...\n");
        pthread_t thread1,thread2;
        //初始化互斥量
        int lock_ret = pthread_mutex_init(&lock,NULL);
        if(lock_ret != 0)
                printf("lock init error!\n");
        //create
        int i = 1;
        int ret1 = pthread_create(&thread1,NULL,&printftest,(void*)&i);
        if(ret1 != 0)
                printf("thread 1 create error!\n");
        int j = 2;
        int ret2 = pthread_create(&thread2,NULL,&printftest,(void*)&j);
        if(ret2 !=0)
                printf("thread 2 create error!\n");
        pthread_join(thread1,NULL);
        pthread_join(thread2,NULL);
        printf("main exit!\n");
}

输出:

test demo is running...
pthread 2 starts!
pthread 2 starts!
pthread 2 starts!
pthread 2 starts!
pthread 2 starts!
pthread 1 starts!
pthread 1 starts!
pthread 1 starts!
pthread 1 starts!
pthread 1 starts!
main exit!

不加锁的状态...

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t lock;
void *printftest(void *argv){
        int* id = (int*)argv;
        //加锁
//      pthread_mutex_lock(&lock);
        for(int i = 0;i < 5;i++){
                printf("pthread %d starts!\n",*id);
                sleep(1);
        }
        //解锁
//      pthread_mutex_unlock(&lock);
}

int main(){
        printf("test demo is running...\n");
        pthread_t thread1,thread2;
        //初始化互斥量
        int lock_ret = pthread_mutex_init(&lock,NULL);
        if(lock_ret != 0)
                printf("lock init error!\n");
        //create
        int i = 1;
        int ret1 = pthread_create(&thread1,NULL,&printftest,(void*)&i);
        if(ret1 != 0)
                printf("thread 1 create error!\n");
        int j = 2;
        int ret2 = pthread_create(&thread2,NULL,&printftest,(void*)&j);
        if(ret2 !=0)
                printf("thread 2 create error!\n");
        pthread_join(thread1,NULL);
        pthread_join(thread2,NULL);
        printf("main exit!\n");
}

输出:

test demo is running...
pthread 2 starts!
pthread 1 starts!
pthread 2 starts!
pthread 1 starts!
pthread 2 starts!
pthread 1 starts!
pthread 2 starts!
pthread 1 starts!
pthread 2 starts!
pthread 1 starts!
main exit!

可以看出,不加锁是乱序输出。

5.5 死锁和死锁的避免

5.5.1 死锁

死锁是指两个或者个以上的执行程序在执行过程中因争夺资源而造成的一种互相等待的现象。

例如,一个进行T1已锁定了一个资源R1,又想去锁定资源R2,而此时另一个线程T2已经锁定了资源R2,却又想去锁定资源R1,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待而无法执行。

根据我自己的理解,应该是:互斥锁就是1班教室门上的锁,当你想进教室偷偷干点事,就得上锁不然别人访问教室里面的资源,但是你临幸完以后,走的时候就得解锁,不能吃着碗里的还惦记着锅里的。现在1班的门上你进去以后上了两把锁,走的时候显然两把都要打开,不然别人还是进不去。其中这两把锁中有一把是2班的,你把锁给锁住了,别人就以为二班门锁上了,无法访问(死锁),最后就都排在了门口(阻塞)。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t lock1,lock2;
void *printftest1(void *argv){
        int* id = (int*)argv;
        //加锁
        pthread_mutex_lock(&lock1);
        for(int i = 0;i < 5;i++){
                printf("pthread %d starts!\n",*id);
                sleep(1);
        }
        //解锁
        pthread_mutex_unlock(&lock1);
}

void *printftest2(void *argv){
        int* id = (int*)argv;
        printf("pthread %d starts!\n",*id);

        //加锁
        pthread_mutex_lock(&lock2);
        pthread_mutex_lock(&lock1);
        for(int i = 0;i < 5;i++){
                printf("pthread %d starts!\n",*id);
                sleep(1);
        }
        pthread_mutex_unlock(&lock1);
        //解锁
//      pthread_mutex_unlock(&lock2);
        for(int i = 0;i < 5;i++)
                printf("pthread %d starts!\n",*id);

}
int main(){
        printf("test demo is running...\n");
        pthread_t thread1,thread2,thread3;
        //初始化互斥量
        int lock_ret1 = pthread_mutex_init(&lock1,NULL);
        if(lock_ret1 != 0)
                printf("lock1 init error!\n");
        int lock_ret2 = pthread_mutex_init(&lock2,NULL);
        if(lock_ret2 != 0)
                printf("lock2 init error!\n");
        //create
        int i = 1;
        int ret1 = pthread_create(&thread1,NULL,&printftest1,(void*)&i);
        if(ret1 != 0)
                printf("thread 1 create error!\n");
        int j = 2;
        int ret2 = pthread_create(&thread2,NULL,&printftest2,(void*)&j);
        if(ret2 !=0)
                printf("thread 2 create error!\n");
        int k = 3;
        int ret3 = pthread_create(&thread3,NULL,&printftest2,(void*)&k);
        if(ret3 !=0)
                printf("thread 3 create error!\n");
        pthread_join(thread1,NULL);
        pthread_join(thread2,NULL);
        pthread_join(thread3,NULL);
        printf("main exit!\n");
}

输出:

分析:进程2上锁之前的代码可以访问,之后的不行。进程2刚开始把1的锁也给锁住了,所以进程1不能执行,2后来把1的锁还回去了,1进行执行。但是2走的时候没有把lock2打开,导致自己班的同学不能继续访问进程2.

5.5.2 死锁的避免

当多个线程都需要相同的锁,但是按照不同的顺序加锁,死锁就很容易发生,如果能确保所有线程都是按照相同的顺序获得锁,那么死锁就不会发生。

例如,规定程序内有三个互斥锁的加锁顺序为mutexA -> mutexB -> mutexC,则线程t1、t2、t3线程操作顺序为:

t1t2

t3

lock(mutexA)lock(mutexA)lock(mutexB)
lock(mutexB)lock(mutexC)lock(mutexC)
lock(mutexC)  

6、条件变量

6.1 为什么需要条件变量

 

6.2 创建和销毁

6.2.1 创建条件变量

Pthreads使用pthread_cond_t类型的变量来表示条件变量。程序必须再使用pthread_cond_t变量之前对其进行初始化。

(1)静态初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

(2)动态初始化

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

参数cond是指向一个需要初始化的pthread_cond_t变量的指针,参数attr传递NULL值时,pthread_cond_init()将cond初始化为默认属性的条件变量。

函数成功返回0,否则返回一个非零的错误码。

静态初始化程序通常比动态初始化更有效。

6.2.2 销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

函数成功返回0,否则返回一个非零的错误码

6.3 等待和通知

6.3.1 等待

条件变量和条件测试是一起的,通常线程会对一个条件进行测试,如果条件不满足就会调用条件等待函数来等待条件满足。

条件等待函数有:pthread_cond_wait()、pthread_cond_timedwait(),函数原型如下:

int pthread_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrcit mutex);

int pthread_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t * restrcit mutex, const struct timespec *restrcit abstime);

pthread_cond_wait()在条件不满足时候一直等待

pthread_cond_timedwait()在条件不满足时候等待一段时间

  • 参数cond是指向一个条件变量的指针;
  • 参数mutex是一个指向互斥量的还真,线程在调用前应该拥有这个互斥量,当线程要加入条件变量的等待队列时,等待操作会使线程释放这个互斥量;
  • pthread_cond_timedwait()函数的第三个参数abstime是一个指向返回时间的指针,如果条件变量通知信号在没有此等待时间之前出现,则等待时间超时退出,abstime是绝对时间,而不是时间间隔。

以上函数执行公共返回0,否则返回一个非零的错误码。如果abstime指定的时间到期,其中pthread_cond_timedwait()错误码为:ETIMEOUT。

6.3.2 通知

当一个线程修改了某个参数可能使得条件变量所关联的条件变为true,他应该通知一个或多个等待在条件变量队列中的线程。

条件通知函数有:pthread_cond_siganl()、pthread_cond_broadcast()。其中pthread_cond_siganl()可以唤醒一个在条件变量等待队列中的线程,pthread_cond_broadcast()可以唤醒所有在条件变量等待队列中的线程。其函数原型如下:

int pthread_signal(pthread_cond_t *cond);

int pthread_broadcast(pthread_cond)t *cond);

参数cond是一个指向条件变量的指针。

函数执行成功返回0,否则返回一个非零的错误码。

这个类似于事件触发机制。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值