2017-07-17 23:35:46 qq_29422251 阅读数 296
  • linux线程全解-linux应用编程和网络编程第7部分

    本课程讲解linux中线程,首先使用多进程解决上个课程中提出的并发式读取按键和鼠标的任务,然后引出多线程并讲解多线程的优势,后详细讲了多线程的同步技术。学习本课程的目的是学会在linux应用编程中使用多线程技术。

    5140 人正在学习 去看看 朱有鹏

将3中线程同步机制分别封装成3个类,实现在locker.h文件中

#ifndef LOCKER_H
#define LOCKER_H

#include <exception>
#include <pthread.h>
#include <semaphore.h>

/*封装信号量的类*/
class sem{
  public:
    /*创建并初始化信号量*/
    sem()
    {
        if( sem_init( &m_sem, 0, 0 ) != 0 )
        {
            /*构造函数没有返回值,可以通过抛出异常来报告错误*/
            throw std::exception();
        }
    }
    /*销毁信号量*/
    ~sem(){
        sem_destroy( &m_sem );
    }
    /*等待信号量*/
    bool wait(){
        return sem_wait( &m_sem ) == 0;
    }
    /*增加信号量*/
    bool post(){
        return sem_port( &_sem ) == 0;
    }
  private:
    sem_t m_sem;
};

/*封装互斥锁类*/
class locker{
   public:
     /*创建并初始化互斥锁*/
     locker(){
         if( pthread_mutex_init( &m_mutex, NULL ) != 0 )
         {
             throw std::exception();
         }
     }
     /*销毁互斥锁*/
     ~locker(){
         pthread_mutex_destroy( &m_mutex );
     }
     /*获取互斥锁*/
     bool lock(){
         return pthread_mutex_lock( &m_mutex ) == 0;
     }
     /*释放互斥锁*/
     bool unlock(){
         return pthread_mutex_unlock( &m_mutex ) == 0;
     }
    private:
      pthread_mutex_t m_mutex;
};

/*封装条件变量类*/
class cond(){
    public:
    /*创建并初始化条件变量*/
    cond(){
        if( pthread_mutex_init( &m_mutex, NULL ) != 0)
        {
            throw std:exception();
        }
        if( pthread_cond_init( &m_cond, NULL ) != 0 ){
            /*构造函数中一旦出现问题,就应该立即释放已经成功分配了的资源*/
            pthread_mutex_destroy( &m_mutex );
            throw std::exception();
        }
    }
     /*销毁条件变量*/
    ~cond(){
        pthread_mutex_destroy( &m_mutex );
        pthread_cond_destroy( &m_cond );
    }
    /*等待条件变量*/
    bool wait(){
        int ret = 0;
        pthread_mutex_lock( &m_mutex );
        ret = pthread_cond_wait( &m_cond, &m_mutex );
        pthread_mutex_unlock( &m_mutex );
        return ret == 0;
    }
    /*唤醒等待条件变量的线程*/
    bool signal(){
        return pthread_cond_signal( &m_cond );
    }
    
    private:
      pthread_mutex_t m_mutex;
      pthread_cond_t m_cond;
};
#endif



2019-06-03 12:06:44 qq_22847457 阅读数 709
  • linux线程全解-linux应用编程和网络编程第7部分

    本课程讲解linux中线程,首先使用多进程解决上个课程中提出的并发式读取按键和鼠标的任务,然后引出多线程并讲解多线程的优势,后详细讲了多线程的同步技术。学习本课程的目的是学会在linux应用编程中使用多线程技术。

    5140 人正在学习 去看看 朱有鹏

同步概念

  • 所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等
  • 而编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。

线程同步

  • 同步即协同步调,按预定的先后次序运行
  • 线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能
  • 举例1: 银行存款 5000。柜台,折:取3000;提款机,卡:取 3000。剩余:2000
  • 举例2: 内存中100字节,线程T1欲填入全1, 线程T2欲填入全0。但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续 从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0。
  • 产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。
  • “同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步

数据混乱原因

  1. 资源共享(独享资源则不会)
  2. 调度随机(意味着数据访问会出现竞争)
  3. 线程间缺乏必要的同步机制。

以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

互斥量(mutex)

  • Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。资源还是共享的,线程间也还是竞争的, 但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
  • 但,应注意:同一时刻,只能有一个线程持有该锁。
  • 当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱
  • 互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。
  • 因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

主要应用函数:

pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_trylock
pthread_mutex_unlock

//以上5个函数的返回值都是:成功返回0, 失败返回错误号。
//pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
//pthread_mutex_t mutex; 变量mutex只有两种取值1、0。

pthread_mutex_init  初始化一个互斥锁(互斥量) ---> 初值可看作1

  • int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • 参数1:传出参数,调用时应传 &mutex
  • restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
  • 参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。 参APUE.12.4同步属性
  1. 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。e.g.  pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
  2. 动态初始化:局部变量应采用动态初始化。e.g.  pthread_mutex_init(&mutex, NULL)

pthread_mutex_destroy  销毁一个互斥锁

  • int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_lock  加锁。可理解为将mutex--(或-1)

  • int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_unlock 解锁。可理解为将mutex ++(或+1)

  • int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_trylock  尝试加锁 

  • int pthread_mutex_trylock(pthread_mutex_t *mutex);

加锁与解锁 

 lock与unlock:

  • lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
  • unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
  • 例如:T1、T2、 T3、 T4 使用一把mutex锁。T1加锁成功,其他线程均阻塞,直至T1解锁。T1解锁后,T2、 T3、 T4均被唤醒,并自动再次尝试加锁。
  • 可假想mutex锁 init成功初值为1。 lock 功能是将mutex--。 unlock将mutex++

lock与trylock:

  • lock加锁失败会阻塞,等待锁释放。
  • trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。

看如下程序:该程序是非常典型的,由于共享、竞争而没有加任何同步机制,导致产生于时间有关的错误,造成数据混乱:

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

void *tfn(void *arg)
{
	srand(time(NULL));
	while (1) 
	{

		printf("hello ");
		sleep(rand() % 3);	/*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/
		printf("world\n");
		sleep(rand() % 3);
	}
	
	return NULL;
}

int main(void)
{
	pthread_t tid;
	srand(time(NULL));
	pthread_create(&tid, NULL, tfn, NULL);
	
	while (1) 
	{
		printf("HELLO ");
		sleep(rand() % 3);
		printf("WORLD\n");
		sleep(rand() % 3);
	}
	
	pthread_join(tid, NULL);
	return 0;
}

修改该程序,使用mutex互斥锁进行同步

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

pthread_mutex_t mutex;      //定义锁

void *tfn(void *arg)
{
	srand(time(NULL));

	while (1) 
	{
		pthread_mutex_lock(&mutex);   // mutex--
		printf("hello ");
		sleep(rand() % 3);	         /* 模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误 */
		printf("world\n");
		pthread_mutex_unlock(&mutex); // mutex++
		sleep(rand() % 3);
	}

	return NULL;
}

int main(void)
{
	int flg = 5;
	pthread_t tid;
	srand(time(NULL));

	pthread_mutex_init(&mutex, NULL);  // mutex==1
	pthread_create(&tid, NULL, tfn, NULL);
	while (flg--) 
	{
		pthread_mutex_lock(&mutex); // mutex--  
		printf("HELLO ");
		sleep(rand() % 3);
		printf("WORLD\n");
		pthread_mutex_unlock(&mutex);  // mutex++
		sleep(rand() % 3);

	}
	pthread_cancel(tid);
	pthread_join(tid, NULL);

	pthread_mutex_destroy(&mutex);  

	return 0;
}

/* 线程之间共享资源stdout */

运行结果

将unlock挪至第二个sleep后,发现交替现象很难出现。

  • 线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁,这两个库函数本身不会阻塞。所以在这两行代码之间失去cpu的概率很小。因此,另外一个线程很难得到加锁的机会。
  • 在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。
  • 在使用互斥量的时候,要避免死锁。死锁:(1)线程试图对同一个互斥量加锁两次(解决:写代码时加锁环节不写复杂);(2)交叉锁:线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁(解决:每个线程申请锁的顺序要抑制,如果申请一把锁,申请另外一把锁的时候,申请失败,应该释放已经掌握的)。

读写锁

  • 读写锁适合于对数据结构的读次数比写次数多得多的情况。因为,读模式锁定时可以共享以写模式锁住时意味着独占,所以读写锁又叫共享-独占锁(共享互斥锁)

读写锁的行为

 

读写锁状态:

一把读写锁具备三种状态:

  • 1. 读模式下加锁状态 (读锁)
  • 2. 写模式下加锁状态 (写锁)
  • 3. 不加锁状态

读写锁特性

  1. 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。
  2. 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
  3. 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。
  4. 读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
  5. 读写锁非常适合于对数据结构读的次数远大于写的情况。

主要应用函数

pthread_rwlock_init函数

pthread_rwlock_destroy函数

pthread_rwlock_rdlock函数  

pthread_rwlock_wrlock函数

pthread_rwlock_tryrdlock函数

pthread_rwlock_trywrlock函数

pthread_rwlock_unlock函数

以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。

pthread_rwlock_t类型 用于定义一个读写锁变量。

pthread_rwlock_t rwlock;

pthread_rwlock_init  初始化一把读写锁

  • int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
  • 参2:attr表读写锁属性,通常使用默认属性,传NULL即可。

pthread_rwlock_destroy  销毁一把读写锁

  • int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

pthread_rwlock_rdlock  以读方式请求读写锁。(常简称为:请求读锁)

  • int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

pthread_rwlock_wrlock  以写方式请求读写锁。(常简称为:请求写锁)

  • int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

pthread_rwlock_unlock  解锁

  • int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

pthread_rwlock_tryrdlock   非阻塞以读方式请求读写锁(非阻塞请求读锁)

  • int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

pthread_rwlock_trywrlock  非阻塞以写方式请求读写锁(非阻塞请求写锁)

  • int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int counter;                          // 全局资源
pthread_rwlock_t rwlock;

void *th_write(void *arg)
{
	int t;
	int i = (int)arg;

	while (1) 
	{
		t = counter;
		usleep(1000);

		pthread_rwlock_wrlock(&rwlock);  // 请求写锁
		printf("=======write %d: %lu: counter = %d ++counter = %d\n", i, pthread_self(), t, ++counter);
		pthread_rwlock_unlock(&rwlock);

		sleep(1);
	}
    
	return NULL;
}

void *th_read(void *arg)
{
	int i = (int)arg;

	while (1) 
	{
		pthread_rwlock_rdlock(&rwlock);   // 请求读锁
		printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
		pthread_rwlock_unlock(&rwlock);

		sleep(1);
	}
    
	return NULL;
}

int main(void)
{
	int i;
	pthread_t tid[8];

	pthread_rwlock_init(&rwlock, NULL);

	for (i = 0; i < 3; i++)
		pthread_create(&tid[i], NULL, th_write, (void *)i);

	for (i = 0; i < 5; i++)
		pthread_create(&tid[i+3], NULL, th_read, (void *)i);

	for (i = 0; i < 8; i++)
		pthread_join(tid[i], NULL);

	pthread_rwlock_destroy(&rwlock);    //  释放读写琐

	return 0;
}

运行结果

条件变量

  • 与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
  • 条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)
  • 条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
  • 使用条件变量之前要先进行初始化。可以在单个语句中生成和初始化一个条件变量如:pthread_cond_t my_condition=PTHREAD_COND_INITIALIZER;(用于进程间线程的通信)。可以利用函数pthread_cond_init动态初始化。

条件变量分为两部分: 条件和变量。

  • 条件本身是由互斥量保护的.。
  • 线程在改变条件状态前先要锁住互斥量.。
  • 它利用线程间共享的全局变量进行同步的一种机制。

主要应用函数

pthread_cond_init函数

pthread_cond_destroy函数

pthread_cond_wait函数

pthread_cond_timedwait函数

pthread_cond_signal函数

pthread_cond_broadcast函数

以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。

pthread_cond_t类型 用于定义条件变量

pthread_cond_t cond;

pthread_cond_init  初始化一个条件变量

  • int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
  • 参2:attr表条件变量属性,通常为默认值,传NULL即可,也可以使用静态初始化的方法,初始化条件变量:
  • pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_cond_destroy  销毁一个条件变量

  • int pthread_cond_destroy(pthread_cond_t *cond);

pthread_cond_wait   阻塞等待一个条件变量

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

函数作用:

  1. 阻塞等待条件变量cond(参1)满足
  2. 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex); 1.2.两步为一个原子操作。
  3. 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

pthread_cond_timedwait   限时等待一个条件变量

  • int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime)
  • 参3: 参看man sem_timedwait函数,查看struct timespec结构体。
​struct timespec
{
    time_t tv_sec; /* seconds */ 秒

    long   tv_nsec; /* nanosecondes*/ 纳秒
}
  • 形参abstime:绝对时间。
  • 如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
  • struct timespec t = {1, 0};
  • pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970年1月1日 00:00:01秒(早已经过去) 

正确用法:

  • time_t cur = time(NULL); 获取当前时间。
  • struct timespec t; 定义timespec 结构体变量t
  • t.tv_sec = cur+1; 定时1秒
  • pthread_cond_timedwait (&cond, &mutex, &t); 传参 参APUE.11.6线程同步条件变量小节

在讲解setitimer函数时我们还提到另外一种时间类型:     

struct timeval 
{
    time_t      tv_sec;  /* seconds */ 秒
    
    suseconds_t tv_usec; /* microseconds */ 微秒
};

pthread_cond_signal  唤醒至少一个阻塞在条件变量上的线程

  • int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_broadcast   唤醒全部阻塞在条件变量上的线程

  • int pthread_cond_broadcast(pthread_cond_t *cond);

生产者消费者条件变量模型

  • 线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。
/*借助条件变量模拟 生产者-消费者 问题*/
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>

/*链表作为公享数据,需被互斥量保护*/
struct msg 
{
	struct msg *next;
	int num;
};

struct msg *head;
struct msg *mp;

/* 静态初始化 一个条件变量和一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *consumer1(void *p)
{
	for (;;)
	{
		pthread_mutex_lock(&lock);
		while (head == NULL) 
		{     //头指针为空,说明没有节点
			pthread_cond_wait(&has_product, &lock);
		}
		mp = head;      
		head = head->next;    // 模拟消费掉一个产品
		pthread_mutex_unlock(&lock);

		printf("-Consume1 ---%d\n", mp->num);
		free(mp);
		mp = NULL;
		sleep(rand() % 5);
	}
}

void *consumer2(void *p)
{
	for (;;)
	{
		pthread_mutex_lock(&lock);
		while (head == NULL)
		{     //头指针为空,说明没有节点   
			pthread_cond_wait(&has_product, &lock);
		}
		mp = head;
		head = head->next;    // 模拟消费掉一个产品
		pthread_mutex_unlock(&lock);

		printf("-Consume2 ---%d\n", mp->num);
		free(mp);
		mp = NULL;
		sleep(rand() % 5);
	}
}

void *producer(void *p)
{
	for (;;) 
	{
		mp = malloc(sizeof(struct msg));
		mp->num = rand() % 1000 + 1;        //模拟生产一个产品
		printf("-Produce ---%d\n", mp->num);

		pthread_mutex_lock(&lock);
		mp->next = head;   // 头插法
		head = mp;
		pthread_mutex_unlock(&lock);

		pthread_cond_signal(&has_product);  // 将等待在该条件变量上的一个线程唤醒
		sleep(rand() % 5);
	}
}

int main(int argc, char *argv[])
{
	pthread_t pid, cid[2];
	srand(time(NULL));

	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid[0], NULL, consumer1, NULL);
	pthread_create(&cid[1], NULL, consumer2, NULL);

	pthread_join(pid, NULL);
	pthread_join(cid[0], NULL);
	pthread_join(cid[1], NULL);

	return 0;
}

运行结果

条件变量的优点

  • 相较于mutex而言,条件变量可以减少竞争。
  • 如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。 

信号量

  • 进化版的互斥锁(1 --> N
  • 由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
  • 信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

主要应用函数

sem_init函数

sem_destroy函数

sem_wait函数

sem_trywait函数

sem_timedwait函数

sem_post函数

以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno。(注意,它们没有pthread前缀)

sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。

sem_t sem; 规定信号量sem不能 < 0。头文件 <semaphore.h>

信号量基本操作:

sem_wait:            1. 信号量大于0,则信号量-- (类比pthread_mutex_lock)

  |                          2. 信号量等于0,造成线程阻塞

对应

  |

sem_post: 将信号量++,同时唤醒阻塞在信号量上的线程 (类比pthread_mutex_unlock)

但,由于sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。

信号量的初值,决定了占用信号量的线程的个数。

sem_init  初始化一个信号量

  • int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 参1:sem信号量
  • 参2:pshared取0用于线程间;取非0(一般为1)用于进程间
  • 参3:value指定信号量初值

sem_destroy  销毁一个信号量

  • int sem_destroy(sem_t *sem);

sem_wait  给信号量加锁 --

  • int sem_wait(sem_t *sem);

sem_post  给信号量解锁 ++

  • int sem_post(sem_t *sem);

sem_trywait  尝试对信号量加锁 -- (与sem_wait的区别类比lock和trylock)

  • int sem_trywait(sem_t *sem);

sem_timedwait  限时尝试对信号量加锁 

  • int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • 参2:abs_timeout采用的是绝对时间。
  • 定时1秒:
  • time_t cur = time(NULL); 获取当前时间。
  • struct timespec t; 定义timespec 结构体变量t
  • t.tv_sec = cur+1; 定时1秒
  • t.tv_nsec = t.tv_sec +100;
  • sem_timedwait(&sem, &t); 传参
/*信号量实现 生产者 消费者问题*/
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>

#define NUM 5               

int queue[NUM];                                     // 全局数组实现环形队列
sem_t blank_number, product_number;                 // 空格子信号量, 产品信号量

void *producer(void *arg)
{
	int i = 0;

	while (1) 
	{
		sem_wait(&blank_number);                    // 生产者将空格子数--,为0则阻塞等待
		queue[i] = rand() % 1000 + 1;               // 生产一个产品
		printf("----Produce---%d\n", queue[i]);        
		sem_post(&product_number);                  // 将产品数++

		i = (i+1) % NUM;                            // 借助下标实现环形
		sleep(rand()%3);
	}
}

void *consumer1(void *arg)
{
	int i = 0;

	while (1) 
	{
		sem_wait(&product_number);                  // 消费者将产品数--,为0则阻塞等待
		printf("-Consume1---%d\n", queue[i]);
		queue[i] = 0;                               // 消费一个产品 
		sem_post(&blank_number);                    // 消费掉以后,将空格子数++

		i = (i+1) % NUM;
		sleep(rand()%3);
	}
}

void *consumer2(void *arg)
{
	int i = 0;

	while (1)
	{
		sem_wait(&product_number);                  // 消费者将产品数--,为0则阻塞等待
		printf("-Consume2---%d\n", queue[i]);
		queue[i] = 0;                               // 消费一个产品 
		sem_post(&blank_number);                    // 消费掉以后,将空格子数++

		i = (i + 1) % NUM;
		sleep(rand() % 3);
	}
}

int main(int argc, char *argv[])
{
	pthread_t pid, cid;

	sem_init(&blank_number, 0, NUM);                // 初始化空格子信号量为5
	sem_init(&product_number, 0, 0);                // 产品数为0

	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid, NULL, consumer1, NULL);
	pthread_create(&cid, NULL, consumer2, NULL);


	pthread_join(pid, NULL);
	pthread_join(cid, NULL);

	sem_destroy(&blank_number);
	sem_destroy(&product_number);

	return 0;
}

条件变量与互斥锁、信号量的区别

  1. 互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。
  2. 互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)。
  3. 由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。
  4. 互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。

哲学家用餐模型分析

多线程版:

选用互斥锁mutex,如创建5个, pthread_mutex_t m[5];

模型抽象:

  • 5个哲学家 --> 5个线程    5支筷子 --> 5把互斥锁    食物:共享资源   int left(左手), right(右手)
  • 5个哲学家使用相同的逻辑,可通用一个线程主函数,void *tfn(void *arg),使用参数来表示线程编号:int i = (int)arg;
  • 哲学家线程根据编号知道自己是第几个哲学家,而后选定锁,锁住,吃饭。否则哲学家thinking。

       A   B   C   D   E

      5支筷子,在逻辑上形成环: 0   1   2   3   4   分别对应5个哲学家:

所以有:

if(i == 4)

        left = i, right = 0;

else

        left = i, right = i+1;

振荡:如果每个人都攥着自己左手的锁,尝试去拿右手锁,拿不到则将锁释放。过会儿五个人又同时再攥着左手锁尝试拿右手锁,依        然拿不到。如此往复形成另外一种极端死锁的现象——振荡。

避免振荡现象:只需5个人中,任意一个人,拿锁的方向与其他人相逆即可(如:E,原来:左:4,右:0 现在:左:0, 右:4)。

所以以上if else语句应改为:

if(i == 4)

        left = 0, right = i;

else

        left = i, right = i+1;

而后, 首先应让哲学家尝试加左手锁:

while {

pthread_mutex_lock(&m[left]); 如果加锁成功,函数返回再加右手锁,

                                                 如果失败,应立即释放左手锁,等待。

若,左右手都加锁成功 --> 吃 --> 吃完 --> 释放锁(应先释放右手、再释放左手,是加锁顺序的逆序)

}

主线程(main)中,初始化5把锁,销毁5把锁,创建5个线程(并将i传递给线程主函数),回收5个线程。

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

pthread_mutex_t m[5];

void *tfn(void *arg)
{
	int i, l, r;

	srand(time(NULL));
	i = (int)arg;

	if (i == 4)
		l = 0, r = i;
	else
		l = i; r = i+1;

	while (1) 
	{
		pthread_mutex_lock(&m[l]);
		if (pthread_mutex_trylock(&m[r]) == 0) 
		{
			printf("\t%c is eating \n", 'A'+i);
			pthread_mutex_unlock(&m[r]);
		}
		pthread_mutex_unlock(&m[l]);
		sleep(rand() % 5);
}

	return NULL;
}

int main(void)
{
	int i;
	pthread_t tid[5];
	
	for (i = 0; i < 5; i++)
		pthread_mutex_init(&m[i], NULL);

	for (i = 0; i < 5; i++)
		pthread_create(&tid[i], NULL, tfn, (void *)i);

	for (i = 0; i < 5; i++)
		pthread_join(tid[i], NULL);

	for (i = 0; i < 5; i++)
		pthread_mutex_destroy(&m[i]);

	return 0;
}

避免死锁的方法:

  • 1. 当得不到所有所需资源时,放弃已经获得的资源,等待。
  • 2. 保证资源的获取顺序,要求每个线程获取资源的顺序一致。如:A获取顺序1、2、3;B顺序应也是1、2、3。若B为3、2、1则易出现死锁现象。

多进程版

相较于多线程需注意问题:

  • 需注意如何共享信号量 (注意:坚决不能使用全局变量 sem_t s[5])

实现:

main函数中:

循环 sem_init(&s[i], 0, 1); 将信号量初值设为1,信号量变为互斥锁。

循环 sem_destroy(&s[i]);

循环创建 5 个子进程。 if(i < 5) 中完成子进程的代码逻辑。

循环回收 5 个子进程。

子进程中:

if(i == 4)  

       left = 0, right == 4;

else

       left = i, right = i+1;

while (1) {

使用 sem_wait(&s[left]) 锁左手,尝试锁右手,若成功 --> 吃; 若不成功 --> 将左手锁释放。

吃完后, 先释放右手锁,再释放左手锁。

}

【重点注意】:

直接将sem_t s[5]放在全局位置,试图用于子进程间共享是错误的!应将其定义放置与mmap共享映射区中。main中:

sem_t *s = mmap(NULL, sizeof(sem_t) * 5, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);

使用方式:将s当成数组首地址看待,与使用数组s[5]没有差异。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/wait.h>

int main(void)
{
	int i;
	pid_t pid;

	sem_t *s;
	s = mmap(NULL, sizeof(sem_t)*5, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
	if (s == MAP_FAILED) 
	{
		perror("fail to mmap");
		exit(1);
	}

	for (i = 0; i < 5; i++)
		sem_init(&s[i], 0, 1);  // 信号量初值制定为1,信号量,变成了互斥锁

	for (i = 0; i < 5; i++)
		if ((pid = fork()) == 0)
			break;

	if (i < 5) 
	{				// 子进程
		int l, r;
		srand(time(NULL));

		if (i == 4) 
			l = 0, r = 4;
		else
			l = i, r = i+1;
		while (1) 
		{
			sem_wait(&s[l]);
			if (sem_trywait(&s[r]) == 0) 
			{
				printf(" %c is eating\n", 'A'+i);
				sem_post(&s[r]);
			}
			sem_post(&s[l]);
			sleep(rand() % 5);
		}
		exit(0);
	} 

	for (i = 0; i < 5; i++)
		wait(NULL);	

	for (i = 0; i < 5; i++)
		sem_destroy(&s[i]);

	munmap(s, sizeof(sem_t)*5);

	return 0;
}

                                                                                                                                                  

2017-05-12 09:48:50 cmm0401 阅读数 247
  • linux线程全解-linux应用编程和网络编程第7部分

    本课程讲解linux中线程,首先使用多进程解决上个课程中提出的并发式读取按键和鼠标的任务,然后引出多线程并讲解多线程的优势,后详细讲了多线程的同步技术。学习本课程的目的是学会在linux应用编程中使用多线程技术。

    5140 人正在学习 去看看 朱有鹏

2017·0512_ Linux 线程同步的三种方法


同步和异步的区别:
1、同步就是说多个任务之间是有先后关系的,一个任务需要等待另一个任务执行完毕才能继续执行。
2、异步就是说多个任务之间没有先后关系,不需要相互等待各做各的事。

同步编程方法:
1、信号量
2、互斥量

异步无需考虑资源冲突,不需特别处理。


线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。Linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。

一、互斥锁(mutex)

通过锁机制实现线程间的同步。

  1. 初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行初始化。
    静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    动态分配:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);
  2. 加锁。对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。
    int pthread_mutex_lock(pthread_mutex *mutex);
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
  3. 解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
  4. 销毁锁。锁在是使用完成后,需要进行销毁以释放资源。
    int pthread_mutex_destroy(pthread_mutex *mutex);
#include <cstdio>  
#include <cstdlib>  
#include <unistd.h>  
#include <pthread.h>  
#include "iostream"  
using namespace std;  
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  
int tmp;  
void* thread(void *arg)  
{  
    cout << "thread id is " << pthread_self() << endl;  
    pthread_mutex_lock(&mutex);  
    tmp = 12;  
    cout << "Now a is " << tmp << endl;  
    pthread_mutex_unlock(&mutex);  
    return NULL;  
}  
int main()  
{  
    pthread_t id;  
    cout << "main thread id is " << pthread_self() << endl;  
    tmp = 3;  
    cout << "In main func tmp = " << tmp << endl;  
    if (!pthread_create(&id, NULL, thread, NULL))  
    {  
        cout << "Create thread success!" << endl;  
    }  
    else  
    {  
        cout << "Create thread failed!" << endl;  
    }  
    pthread_join(id, NULL);  
    pthread_mutex_destroy(&mutex);  
    return 0;  
}  
//编译:g++ -o thread testthread.cpp -lpthread 

二、条件变量(cond)

互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分为两部分: 条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

  1. 初始化条件变量。
    静态态初始化,pthread_cond_t cond = PTHREAD_COND_INITIALIER;
    动态初始化,int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
  2. 等待条件成立。释放锁,同时阻塞等待条件变量为真才行。timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)
    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
  3. 激活条件变量。pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
    int pthread_cond_signal(pthread_cond_t *cond);
    int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞
  4. 清除条件变量。无线程等待,否则返回EBUSY
    int pthread_cond_destroy(pthread_cond_t *cond);
#include <stdio.h>  
#include <pthread.h>  
#include "stdlib.h"  
#include "unistd.h"  
pthread_mutex_t mutex;  
pthread_cond_t cond;  
void hander(void *arg)  
{  
    free(arg);  
    (void)pthread_mutex_unlock(&mutex);  
}  
void *thread1(void *arg)  
{  
    pthread_cleanup_push(hander, &mutex);  
    while(1)  
    {  
        printf("thread1 is running\n");  
        pthread_mutex_lock(&mutex);  
        pthread_cond_wait(&cond, &mutex);  
        printf("thread1 applied the condition\n");  
        pthread_mutex_unlock(&mutex);  
        sleep(4);  
    }  
    pthread_cleanup_pop(0);  
}  
void *thread2(void *arg)  
{  
    while(1)  
    {  
        printf("thread2 is running\n");  
        pthread_mutex_lock(&mutex);  
        pthread_cond_wait(&cond, &mutex);  
        printf("thread2 applied the condition\n");  
        pthread_mutex_unlock(&mutex);  
        sleep(1);  
    }  
}  
int main()  
{  
    pthread_t thid1,thid2;  
    printf("condition variable study!\n");  
    pthread_mutex_init(&mutex, NULL);  
    pthread_cond_init(&cond, NULL);  
    pthread_create(&thid1, NULL, thread1, NULL);  
    pthread_create(&thid2, NULL, thread2, NULL);  
    sleep(1);  
    do  
    {  
        pthread_cond_signal(&cond);  
    }while(1);  
    sleep(20);  
    pthread_exit(0);  
    return 0;  
}

    #include <pthread.h>  
    #include <unistd.h>  
    #include "stdio.h"  
    #include "stdlib.h"  
    static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;  
    static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  
    struct node  
    {  
        int n_number;  
        struct node *n_next;  
    }*head = NULL;  
      
    static void cleanup_handler(void *arg)  
    {  
        printf("Cleanup handler of second thread./n");  
        free(arg);  
        (void)pthread_mutex_unlock(&mtx);  
    }  
    static void *thread_func(void *arg)  
    {  
        struct node *p = NULL;  
        pthread_cleanup_push(cleanup_handler, p);  
        while (1)  
        {  
            //这个mutex主要是用来保证pthread_cond_wait的并发性  
            pthread_mutex_lock(&mtx);  
            while (head == NULL)  
            {  
                //这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何  
                //这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线  
                //程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。  
                //这个时候,应该让线程继续进入pthread_cond_wait  
                // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,  
                //然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立  
                //而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源  
                //用这个流程是比较清楚的  
                pthread_cond_wait(&cond, &mtx);  
                p = head;  
                head = head->n_next;  
                printf("Got %d from front of queue/n", p->n_number);  
                free(p);  
            }  
            pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁  
        }  
        pthread_cleanup_pop(0);  
        return 0;  
    }  
    int main(void)  
    {  
        pthread_t tid;  
        int i;  
        struct node *p;  
        //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而  
        //不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大  
        pthread_create(&tid, NULL, thread_func, NULL);  
        sleep(1);  
        for (i = 0; i < 10; i++)  
        {  
            p = (struct node*)malloc(sizeof(struct node));  
            p->n_number = i;  
            pthread_mutex_lock(&mtx); //需要操作head这个临界资源,先加锁,  
            p->n_next = head;  
            head = p;  
            pthread_cond_signal(&cond);  
            pthread_mutex_unlock(&mtx); //解锁  
            sleep(1);  
        }  
        printf("thread 1 wanna end the line.So cancel thread 2./n");  
        //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出  
        //线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。  
        pthread_cancel(tid);  
        pthread_join(tid, NULL);  
        printf("All done -- exiting/n");  
        return 0;  
    }  

三、信号量(sem)

如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。

  1. 信号量初始化。
    int sem_init (sem_t *sem , int pshared, unsigned int value);
    这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux 只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。
  2. 等待信号量。给信号量减1,然后等待直到信号量的值大于0。
    int sem_wait(sem_t *sem);
  3. 释放信号量。信号量值加1。并通知其他等待线程。
    int sem_post(sem_t *sem);
  4. 销毁信号量。我们用完信号量后都它进行清理。归还占有的一切资源。
    int sem_destroy(sem_t *sem);
    #include <stdlib.h>  
    #include <stdio.h>  
    #include <unistd.h>  
    #include <pthread.h>  
    #include <semaphore.h>  
    #include <errno.h>  
    #define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!/n", __func__);return;}  
    typedef struct _PrivInfo  
    {  
        sem_t s1;  
        sem_t s2;  
        time_t end_time;  
    }PrivInfo;  
      
    static void info_init (PrivInfo* thiz);  
    static void info_destroy (PrivInfo* thiz);  
    static void* pthread_func_1 (PrivInfo* thiz);  
    static void* pthread_func_2 (PrivInfo* thiz);  
      
    int main (int argc, char** argv)  
    {  
        pthread_t pt_1 = 0;  
        pthread_t pt_2 = 0;  
        int ret = 0;  
        PrivInfo* thiz = NULL;  
        thiz = (PrivInfo* )malloc (sizeof (PrivInfo));  
        if (thiz == NULL)  
        {  
            printf ("[%s]: Failed to malloc priv./n");  
            return -1;  
        }  
        info_init (thiz);  
        ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, thiz);  
        if (ret != 0)  
        {  
            perror ("pthread_1_create:");  
        }  
        ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, thiz);  
        if (ret != 0)  
        {  
            perror ("pthread_2_create:");  
        }  
        pthread_join (pt_1, NULL);  
        pthread_join (pt_2, NULL);  
        info_destroy (thiz);  
        return 0;  
    }  
    static void info_init (PrivInfo* thiz)  
    {  
        return_if_fail (thiz != NULL);  
        thiz->end_time = time(NULL) + 10;  
        sem_init (&thiz->s1, 0, 1);  
        sem_init (&thiz->s2, 0, 0);  
        return;  
    }  
    static void info_destroy (PrivInfo* thiz)  
    {  
        return_if_fail (thiz != NULL);  
        sem_destroy (&thiz->s1);  
        sem_destroy (&thiz->s2);  
        free (thiz);  
        thiz = NULL;  
        return;  
    }  
    static void* pthread_func_1 (PrivInfo* thiz)  
    {  
        return_if_fail(thiz != NULL);  
        while (time(NULL) < thiz->end_time)  
        {  
            sem_wait (&thiz->s2);  
            printf ("pthread1: pthread1 get the lock./n");  
            sem_post (&thiz->s1);  
            printf ("pthread1: pthread1 unlock/n");  
            sleep (1);  
        }  
        return;  
    }  
    static void* pthread_func_2 (PrivInfo* thiz)  
    {  
        return_if_fail (thiz != NULL);  
        while (time (NULL) < thiz->end_time)  
        {  
            sem_wait (&thiz->s1);  
            printf ("pthread2: pthread2 get the unlock./n");  
            sem_post (&thiz->s2);  
            printf ("pthread2: pthread2 unlock./n");  
            sleep (1);  
        }  
        return;  
    }  













2017-08-03 09:33:04 zhaoxd200808501 阅读数 626
  • linux线程全解-linux应用编程和网络编程第7部分

    本课程讲解linux中线程,首先使用多进程解决上个课程中提出的并发式读取按键和鼠标的任务,然后引出多线程并讲解多线程的优势,后详细讲了多线程的同步技术。学习本课程的目的是学会在linux应用编程中使用多线程技术。

    5140 人正在学习 去看看 朱有鹏

一、互斥锁基本原理

  互斥锁以排他方式防止共享数据被并发访问。互斥锁为一个二元变量,其状态分为开锁上锁,将某个共享资源与某个特定互斥锁在逻辑上绑定(即要申请该资源必须先获取锁),对该共享资源的访问操作如下:
  1.在访问该资源前,首先申请该互斥锁,如果该互斥锁处于开锁状态,则申请到该锁对象,并占有该锁(使该锁处于锁定状态),以防止其他线程访问该资源;如果该锁处于锁定状态,默认阻塞当前线程。
  2.只有锁定改互斥锁的进程才能释放该互斥锁,其他线程释放操作无效。
  3.互斥锁的主要作用是保证线程执行完整性

二、互斥锁操作流程

  1.定义一个全局的锁;
  2.初始化锁;
  3.创建线程;
  4.上锁、操作公共资源、解锁;
  5.线程退出,释放资源(销毁锁)。

三、互斥锁基本操作函数

  互斥锁基本操作函数如下表所示:
 这里写图片描述

1.初始化互斥锁

  在使用互斥锁前,需要定义互斥锁(全局变量),定义互斥锁的代码入下:

phtread_mutex_t lock;

  初始化互斥锁的函数为pthread_mutex_init,互斥锁函数声明如下:

/*Initialize a mutex*/
int pthread_mutex_init(pthread_mutex_t* mutex, pthread_mutexattr_t* mutexattr);

  第1个参数mutex是指向要初始化的互斥锁的指针。
  第2个参数mutexattr是指向属性对象的指针,该属性对象定义要初始化的互斥锁的属性。若该指针为NULL时,使用默认属性

  使用默认属性初始化互斥锁代码如下:

phtread_mutex_t mp;
int ret = pthread_mutex_init(&mp, NULL);

  使用自定义属性初始化互斥锁代码如下:

pthread_mutexattr_t mattr;
phtread_mutex_t mp;
int ret = pthread_mutex_init(&mp, &mattr);

  pthread_mutex_init()函数返回值为0表示函数执行成功。否则,将返回错误编号以指明错误

  此外,还可以使用宏PTHREAD_MUTEX_INITIALIZER初始化今天分配的互斥锁。此宏定义如下:

/*come from /usr/include/pthread.h*/
#define PTHREAD_MUTEX_INITIALIZER { { 0, } }

  使用宏PTHREAD_MUTEX_INITIALIZER初始化互斥锁代码如下:

phtread_mutex_t mp = PTHREAD_MUTEX_INITIALIZER;

2.申请互斥锁
  如果一线程要占用一共享资源,必须先申请对应的互斥锁。pthread_mutex_lock()函数以阻塞的方式申请互斥锁,其函数申明如下:

int pthread_mutex_lock(pthread_mutex_t* mutex);

  pthread_mutex_trylock()函数以非阻塞方式申请互斥锁,函数申明如下:

int pthread_mutex_trylock(pthread_mutex_t* mutex);

  pthread_mutex_lock()pthread_mutex_trylock()执行成功时返回0。否则,返回一个错误编号,以指明错误。

3.释放互斥锁
  pthread_mutex_unlock()函数用来释放互斥锁,其函数申明如下:

/*Unlock a mutex*/
int pthread_mutex_unlock(pthread_mutex_t* mutex);

  参数mutex为执行要解锁的互斥锁的指针。释放操作只能由占有该互斥锁的线程完成。该函数执行成功时返回0。否则,返回指明错误的错误编号(未设置errno变量)。

4.销毁互斥锁
  pthread_mutex_destroy()函数用来释放互斥锁,其函数申明如下:

/*Destroy a mutex*/
int pthread_mutex_destroy(pthread_mutex_t* mutex);

  参数mutex为执行要解锁的互斥锁的指针。该函数执行成功时返回0。否则,返回指明错误的错误编号。

四、示例

  下面通过3个线程来计算1+2+3+…+100的值,代码如下:

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

int sum = 0;

void* pth_add1(void* arg)
{
    int i;

    for (i=1; i<20; i++) {
        sum += i;
        usleep(10000);
    }

    pthread_exit(0);
}

void* pth_add2(void* arg)
{
    int i;

    for (i=20; i<60; i++) {
        usleep(10000);
        sum += i;
    }

    pthread_exit(0);
}

void* pth_add3(void* arg)
{
    int i;

    for (i=60; i<=100; i++) {
        sum += i;
        usleep(10000);
    }

    pthread_exit(0);
}


int main()  
{  
    pthread_t tid1, tid2, tid3;
    int ret;

    ret = pthread_create(&tid1, NULL, pth_add1, NULL);
    if (0 != ret) {
        perror("create pthead error");
        return -1;
    }

    ret = pthread_create(&tid2, NULL, pth_add2, NULL);
    if (0 != ret) {
        perror("create pthead error");
        return -1;
    }

    ret = pthread_create(&tid3, NULL, pth_add3, NULL);
    if (0 != ret) {
        perror("create pthead error");
        return -1;
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    printf("sum = %d\n", sum);

    return 0;
}

运行结果如下所示:
这里写图片描述

  由上图可知,四次运行结果中只有一次结果是对的,为什么会出现这样的情况呢?由于3个线程访问共享资源sum,而没有使用互斥锁导致,下面给每个线程加互斥锁:

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

pthread_mutex_t lock;
int sum = 0;

void* pth_add1(void* arg)
{
    int i;

    // 申请互斥锁
    pthread_mutex_lock(&lock);

    for (i=1; i<20; i++) {
        sum += i;
        usleep(10000);
    }
    // 释放互斥锁
    pthread_mutex_unlock(&lock);

    pthread_exit(0);
}

void* pth_add2(void* arg)
{
    int i;
    // 申请互斥锁
    pthread_mutex_lock(&lock);

    for (i=20; i<60; i++) {
        usleep(10000);
        sum += i;
    }
    // 释放互斥锁
    pthread_mutex_unlock(&lock);

    pthread_exit(0);
}

void* pth_add3(void* arg)
{
    int i;
    // 申请互斥锁
    pthread_mutex_lock(&lock);

    for (i=60; i<=100; i++) {
        sum += i;
        usleep(10000);
    }
    // 释放互斥锁
    pthread_mutex_unlock(&lock);

    pthread_exit(0);
}


int main()  
{  
    pthread_t tid1, tid2, tid3;
    int ret;

    // 初始化锁
    pthread_mutex_init(&lock, NULL);

    ret = pthread_create(&tid1, NULL, pth_add1, NULL);
    if (0 != ret) {
        perror("create pthead error");
        return -1;
    }

    ret = pthread_create(&tid2, NULL, pth_add2, NULL);
    if (0 != ret) {
        perror("create pthead error");
        return -1;
    }

    ret = pthread_create(&tid3, NULL, pth_add3, NULL);
    if (0 != ret) {
        perror("create pthead error");
        return -1;
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    // 销毁锁
    pthread_mutex_destroy(&lock);

    printf("sum = %d\n", sum);

    return 0;
}

运行结果如下:
这里写图片描述
  由上图可知,4次运行结果均正确,这便证明互斥锁可以确保线程执行的完整性,故对共享资源的访问一定要用互斥锁保护起来

2019-04-28 08:59:02 fanyun_01 阅读数 194
  • linux线程全解-linux应用编程和网络编程第7部分

    本课程讲解linux中线程,首先使用多进程解决上个课程中提出的并发式读取按键和鼠标的任务,然后引出多线程并讲解多线程的优势,后详细讲了多线程的同步技术。学习本课程的目的是学会在linux应用编程中使用多线程技术。

    5140 人正在学习 去看看 朱有鹏

       线程本地存储可以避免线程访问共享数据,但是线程之间的大部分数据始终还是共享的。在涉及到对共享数据进行读写操作时,就必须使用同步机制,否则就会造成线程们哄抢共享数据的结果,这会把你的数据弄的七零八落理不清头绪。

Linux提供的线程同步机制主要有互斥锁和条件变量。其它形式的线程同步机制用得并不多。

       首先我们看一下互斥锁。所谓的互斥就是线程之间互相排斥,获得资源的线程排斥其它没有获得资源的线程。Linux使用互斥锁来实现这种机制。

       既然叫锁,就有加锁和解锁的概念。当线程获得了加锁的资格,那么它将独享这个锁,其它线程一旦试图去碰触这个锁就立即被系统“拍晕”。当加锁的线程解开并放弃了这个锁之后,那些被“拍晕”的线程会被系统唤醒,然后继续去争抢这个锁。至于谁能抢到,只有天知道。但是总有一个能抢到。于是其它来凑热闹的线程又被系统给“拍晕”了……如此反复。感觉线程的“头”很痛:)

       从互斥锁的这种行为看,线程加锁和解锁之间的代码相当于一个独木桥,同意时刻只有一个线程能执行。从全局上看,在这个地方,所有并行运行的线程都变成了排队运行了。比较专业的叫法是同步执行,这段代码区域叫临界区。同步执行就破坏了线程并行性的初衷了,临界区越大破坏得越厉害。所以在实际应用中,应该尽量避免有临界区出现。实在不行,临界区也要尽量的小。如果连缩小临界区都做不到,那还使用多线程干嘛?

互斥锁在Linux中的名字是mutex。这个似乎优点眼熟。对,在前面介绍NPTL的时候提起过,但是那个叫futex,是系统底层机制。对于提供给用户使用的则是这个mutex。Linux初始化和销毁互斥锁的接口是pthread_mutex_init()和pthead_mutex_destroy(),对于加锁和解锁则有pthread_mutex_lock()、pthread_mutex_trylock()和pthread_mutex_unlock()。这些接口的完整定义如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);  
int pthread_mutex_destory(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); 

从这些定义中可以看到,互斥锁也是有属性的。只不过这个属性在绝大多数情况下都不需要改动,所以使用默认的属性就行。方法就是给它传递NULL。

phtread_mutex_trylock()比较特别,用它试图加锁的线程永远都不会被系统“拍晕”,只是通过返回EBUSY来告诉程序员这个锁已经有人用了。至于是否继续“强闯”临界区,则由程序员决定。系统提供这个接口的目的可不是让线程“强闯”临界区的。它的根本目的还是为了提高并行性,留着这个线程去干点其它有意义的事情。当然,如果很幸运恰巧这个时候还没有人拥有这把锁,那么自然也会取得临界区的使用权。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t g_mutex;
int g_lock_var = 0;
void* thread1( void *arg )
{
    int i, ret;
    time_t end_time;
    end_time = time(NULL) + 10;
    while( time(NULL) < end_time ) {
        ret = pthread_mutex_trylock( &g_mutex );
        if( EBUSY == ret ) {
            printf( "thread1: the varible is locked by thread2.\n" );
        } else {
            printf( "thread1: lock the variable!\n" );
            ++g_lock_var;
            pthread_mutex_unlock( &g_mutex );
        }
        sleep(1);
    }
    return NULL;
}
void* thread2( void *arg )
{
    int i;
    time_t end_time;
    end_time = time(NULL) + 10;
    while( time(NULL) < end_time ) {
        pthread_mutex_lock( &g_mutex );
        printf( "thread2: lock the variable!\n" );
        ++g_lock_var;
        sleep(1);
        pthread_mutex_unlock( &g_mutex );
    }
    return NULL;
}
int main( int argc, char *argv[] )
{
    int i;
    pthread_t pth1,pth2;
    pthread_mutex_init( &g_mutex, NULL );
    pthread_create( &pth1, NULL, thread1, NULL );
    pthread_create( &pth2, NULL, thread2, NULL );
    pthread_join( pth1, NULL );
    pthread_join( pth2, NULL );
    pthread_mutex_destroy( &g_mutex );
    printf( "g_lock_var = %d\n", g_lock_var );
    return 0;                            
}

使用条件变量

从代码中会发现,等待“事件”发生的接口都需要传递一个互斥锁给它。而实际上这个互斥锁还要在调用它们之前加锁,调用之后解锁。不单如此,在调用操作“事件”发生的接口之前也要加锁,调用之后解锁。这就面临一个问题,按照这种方式,等于“发生事件”和“等待事件”是互为临界区的。也就是说,如果“事件”还没有发生,那么有线程要等待这个“事件”就会阻止“事件”的发生。更干脆一点,就是这个“生产者”和“消费者”是在来回的走独木桥。但是实际的情况是,“消费者”在缓冲区满的时候会得到这个“事件”的“通知”,然后将字符逐个打印出来,并清理缓冲区。直到缓冲区的所有字符都被打印出来之后,“生产者”才开始继续工作。

为什么会有这样的结果呢?这就要说明一下pthread_cond_wait()接口对互斥锁做什么。答案是:解锁。pthread_cond_wait()首先会解锁互斥锁,然后进入等待。这个时候“生产者”就能够进入临界区,然后在条件满足的时候向“消费者”发出信号。当pthead_cond_wait()获得“通知”之后,它还要对互斥锁加锁,这样可以防止“生产者”继续工作而“撑坏”缓冲区。另外,“生产者”在缓冲区不满的情况下才能工作的这个限定条件是很有必要的。因为在pthread_cond_wait()获得通知之后,在没有对互斥锁加锁之前,“生产者”可能已经重新进入临界区了,这样“消费者”又被堵住了。也就是因为条件变量这种工作性质,导致它必须与互斥锁联合使用。

此外,利用条件变量和互斥锁,可以模拟出很多其它类型的线程同步机制,比如:event、semaphore等。

Linux线程同步机制读写锁

博文 来自: zhaoxd200808501

Linux线程同步机制条件变量

博文 来自: zhaoxd200808501
没有更多推荐了,返回首页