C++多核高级编程 - 06 并发任务的通信和同步(2) 对并发进行同步

一,同步类型

任何计算机系统中,系统资源都是有限的。在多进程或多线程程序组成的系统中,必然会存在对资源的竞争关系。按照资源需求的不同,系统中同步类型大致可分为三种:

-       数据:它允许并发线程/进程安全的访问一个内存块。

-       硬件:当多个进程或线程需要一个或多个硬件支持时,它允许并发进程或线程安全的访问硬件,并且对于实时性和任务的优先级有一定的要求。

-       任务:它强制执行合理过程的前置条件和后置条件。


二,同步对数据的访问

为了控制控制竞争条件及并发线程或进程安全的访问某个内存块,我们需要对数据的访问进行同步。保证对内存的使用和更改都是安全的。在多线程环境中,任务代码试图访问和其他并发的线程或进程共享的内存,全局变量,或文件时,需要数据同步,这被称作临界区。临界区是可以安全读/写,关闭文件,读写全局变量或数据结构的代码块。


1,临界区

临界区是访问共享资源的代码块,它通过一个入口点和出口点来标记。

在使用的过程中有三个条件:

(1) 如果一个任务位于其临界区中,其他共享资源的任务不能在它们的临界区中执行。它们将被阻塞。这被称作互斥(mutual exclusion)。

(2) 如果没有哪个任务位于临界区中,则任何被阻塞的任务可以进入临界区运行,这被称作进行(progress)。

(3) 应当过一定时间,才允许某个任务重新进入临界区,这个时间段被称作有限等待。(bounded wait)


2,PRAM模型 (Parallel Random-Access Machine)

并行随机访问计算机,是一个简化的理论模型,其中包含被标记为P1,P2,P3,... ... ,PN 的N个处理器,这些处理器共享全局内存。所以处理器对共享全局内存进行同时的读写访问。处理器在一个不可中断的时间单元内对共享的全局内存进行访问。PRAM 模型有4种算法可被用来访问全局的共享内存:

(1)  当同时读相同的内存片段时,允许使用并行的读算法,,不会造成数据破坏。

(2)  并发写算法允许多个处理器对共享内存进行写入。

(3)  互斥读算法被用来保证不会有2个处理器同时对相同的内存位置进行读取。

(4)  互斥写算法被用来保证不会有2个处理器同时对相同的内存位置进行写入。


上述的读写算法可以被组合为不同的读写组合算法

  • 互斥读互斥写  (Exclusive Read and Exclusive Write) EREW
  • 并发读互斥写  (Concurrent Read and Exclusive Write) CREW
  • 互斥读并发写  (Exclusive Read and Concurrent Write) ERCW
  • 并发读并发写  (Concurrent Read and Concurrent Write) CRCW
这些算法可以被视为共享数据的访问策略。

互斥读互斥写 (EREW) 意味着对共享内存访问的串行化。

并发读并发写 (CRCW) 最为灵活,但也意味着会产生更多的并行任务的问题。


3,并发任务的执行顺序

并发任务间不光对数据的访问需要同步,任务间的执行顺序也需要进行协调。

协作任务间有4种顺序关系:

  • Start - to - Start (SS)         :   任务B不能在任务A开始之前开始
  • Finish - to - Start (FS)      :   任务A不能结束,直到任务B启动
  • Start  - to - Finish (FS)     :   任务B不能开始,直到任务A结束
  • Finish - to - Finish (FF)   :   任务A不能结束,直到任务B结束


三,同步机制

同步机制可以既适用于进行,也可以用于线程。其中包括:

  • 信号量和互斥变量
  • 读-写锁
  • 条件变量

1,信号量
信号量是一种同步机制,用于管理同步关系并实现访问策略。它是一种特殊的变量,只有通过特殊的操作来访问。它就像一把准许对资源访问的钥匙。进程或线程得到后就可以对资源进行访问和操作。

信号量可以通过一些指定的操作来进行访问。P(), V() 原语。

                 P (Mutex)
                 if (Mutex > 0)
                 {
                         Mutex --;
                 }
                 else
                 {
                         Block on Mutex
                 }


                 V (Mutex)
                 if (Blocked on Mutex N processes)
                 {
                         Pass on Mutex;
                 }
                 else
                 {
                         Mutex++;
                 }

信号量的值取决于信号量的类型
  • 二进制信号量:值为0和1,1为可用。
  • 计数信号量:某个非负整数值,初始值表示可用资源的数目。

POSIX 信号量
信号量基本功能:

基本信号量操作 描                    述
初始化 分配信号量所需要的内存并赋初始值,决定信号量是私有地,共享的,被占有的还是未被占有的。
请求占有权 发出占有信号量的请求,如果信号量被其他线程占有,则当前线程阻塞。
释放占有权 释放信号量,从而使他可被阻塞的线程得到。
尝试占有权 测试信号量的占有权,如果信号量已经被占有,请求方不会阻塞,而是继续运行,在继续之前可以等待一段时间。
销毁 释放信号量相关的内存,如果信号量被占有或还有线程在等待,则该信号量不能被销毁。

例子:进程间使用 Semaphore 对文件读写进行同步

output_f.cpp

using namespace std;

#include <semaphore.h>
#include <iostream>
#include <fstream>
#include <string>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    string str;
    const char* Name;
    sem_t* pSem;
    ifstream oInFile("out_text.txt");

    cout << "check file is open" << endl;
    if (oInFile.is_open())
    {
        Name = "sem_test";
        cout << "open sem" << endl;
        pSem = sem_open(Name, O_CREAT, O_RDWR, 1);
        sem_unlink(Name);

        cout << "file is eof or good" << endl;
        while(!oInFile.eof() && oInFile.good())
        {
            sleep(1);  // for testing with the file output progress
            cout << "wait sem" << endl;
            sem_wait(pSem);
            getline(oInFile, str);
            cout << str << endl;
            cout << "sem post" << endl;
            sem_post(pSem);
        }
        cout << "-------------------------" << endl;
        oInFile.close();
    }
    return 0;
}


input_f.cpp
using namespace std;
#include <semaphore.h>
#include <iostream>
#include <fstream>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int nLoop, PN;
    sem_t *pSem;
    const char* strName;
    ofstream oOutStream("out_text.txt", ios::app);

    PN = 100;
    nLoop = 100;
    strName = "sem_test";

    cout << "open sem" << endl;
    pSem = sem_open(strName, O_CREAT, O_RDWR, 1);
    sem_unlink(strName);

    cout << "writing information" << endl;
    for (int i = 0; i < nLoop; i++)
    {
        cout << "wait sem "<< i << endl;
        sleep(2);    // for testing with the file input progress
        sem_wait(pSem);
        oOutStream << "Process" << PN << " counting: " << i << endl;
        cout << "post sem " << i << endl;
        sem_post(pSem);
    }

    cout << "close file" << endl;
    oOutStream.close();
    return 0;
}


POSIX 互斥量信号量
POSIX定义了类型为pthread_mutex_t 的互斥量信号量,可以被线程或进程使用。互斥量是信号量的一种,必须由对他加锁线程来解锁,而信号量可以由执行wait() 或lock() 以外的线程或进程对他执行 post()  或 unlock()。

互 斥 量 操 作 函数原型 #include<pthread.h>
初始化 int pthread_mutex_init(pthread_mutex_t * mutex const pthread_mtexattr_t * attr)
请求占有权 int pthread_mutex_lock(pthread_mutex_t* mutex)
int pthread_mutex_timedlock(pthread_mutex_t* mutex cosnt struct timespce* abs_timeout)
释放占有权 int pthread_mutex_unlock(pthread_mutex_t* mutex)
尝试占有权 int pthread_mutex_trylock(pthread_mutex_t * mutex)
销毁 int pthread_mutex_destroy(pthread_mutex_t * mutex)

Simple Sample:
using namespace std;

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

int Answer = 10;
pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER; 

void *task1(void* X)
{
    pthread_mutex_lock(&Mutex);
    Answer = Answer * 32;
    pthread_mutex_unlock(&Mutex);
    cout << "Thread A Answer = " << Answer << endl;
}

void *task2(void* X)
{
    pthread_mutex_lock(&Mutex);
    Answer = Answer / 2;
    pthread_mutex_unlock(&Mutex);
    cout << "Thread B Answer = " << Answer << endl;
}

void *task3(void* X)
{
    pthread_mutex_lock(&Mutex);
    Answer = Answer + 5;
    pthread_mutex_unlock(&Mutex);
    cout << "Thread C Answer = " << Answer << endl;
}

int main()
{
    pthread_t ThreadA, ThreadB, ThreadC;

    cout << "Answer = " << Answer << endl;
    pthread_create(&ThreadA, NULL, task1, NULL);
    pthread_create(&ThreadB, NULL, task2, NULL);
    pthread_create(&ThreadC, NULL, task3, NULL);

    pthread_join(ThreadA, NULL);
    pthread_join(ThreadB, NULL);
    pthread_join(ThreadC, NULL);

    cout << "Answer = " << Answer << endl;

    return 0;
}


测试结果:
nswer = 10
Thread A Answer = 320
Thread B Answer = 160
Thread C Answer = 165
Answer = 165


2,读-写锁

互斥量信号量串行化临界区。只有使用共享数据的线程或进程能够进入到临界区。使用读-写锁时,如果多个线程只要读取共享内存,则他们可以进入到临界区,但只有一个进程或线程被允许写入或更改共享内存。

POSIX 的读写锁
读-写锁操作 函数原型 #include <pthread.h>
初始化 int pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattr_t *attr)
请求占有权 int pthread_rwlock_rdlock(pthread_rwlock_t * rwlock)
int pthread_rwlock_wrlock(pthread_rwlock_t * rwlock)
int pthread_rwlock_timedrelock(pthread_rwlock_t* rwlock, const struct timespec* abs_timeout)
int pthread_rwlock_timedwrlock(pthread_rwlock_t* rwlock, const struct timespec* abs_timeout)
释放占有权 int pthread_rwlock_unlock(pthread_rwlock_t* rwlock)
尝试占有权 int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock)
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock)
销毁 int pthread_rwlock_destroy(pthread_rwlock_t* rwlock)

读-写锁与常规互斥量的区别是:读-写锁有读和写两种加锁方式。

一个简单的例子:其中 ThreadA 和 ThreadC为写入线程,ThreadB 和 ThreadD为读线程,所以B和D 可同时访问共享内存,但A和C存在互斥。
using namespace std;

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


pthread_t ThreadA, ThreadB, ThreadC, ThreadD;
pthread_rwlock_t RWLock;
int nCounter = 0;

void *product1(void* X)
{
    while(1)
    {
        sleep(2);  // Sleep must before lock, or it will always lock
        pthread_rwlock_wrlock(&RWLock);
        if (nCounter > 100)
        {
            pthread_rwlock_unlock(&RWLock);
            break;
        }
        nCounter += 2;
        cout << "Thread A counter+2 = " << nCounter << endl; 
        pthread_rwlock_unlock(&RWLock);
    }
}


void *product2(void* X)
{
    while(1)
    {
        sleep(3);  // Sleep must before lock, or it will always lock
        pthread_rwlock_wrlock(&RWLock);

        if (nCounter > 100)
        {
            pthread_rwlock_unlock(&RWLock);
            break;
        }
        nCounter++;
        cout << "Thread C counter++ = " << nCounter << endl; 
        pthread_rwlock_unlock(&RWLock);
    }
}

void *consumer1(void* X)
{
    while(1)
    {
        sleep(1);  // Sleep must before lock, or it will always lock
        pthread_rwlock_rdlock(&RWLock);
        if (nCounter > 100)
        {
            pthread_rwlock_unlock(&RWLock);
            break;
        }
        cout << "Thread B: " << nCounter << endl;
        pthread_rwlock_unlock(&RWLock);
    }
}

void *consumer2(void* X)
{
    while(1)
    {
        sleep(1);  // Sleep must before lock, or it will always lock
        pthread_rwlock_rdlock(&RWLock);
        if (nCounter > 100)
        {
            pthread_rwlock_unlock(&RWLock);
            break;
        }
        cout << "Thread D: " << nCounter << endl;
        pthread_rwlock_unlock(&RWLock);
    }
}

int main()
{
    pthread_rwlock_init(&RWLock, NULL);
    
    pthread_create(&ThreadA, NULL, product1 , NULL);
    pthread_create(&ThreadB, NULL, consumer1, NULL);
    pthread_create(&ThreadC, NULL, product2 , NULL);
    pthread_create(&ThreadD, NULL, consumer2, NULL);


    pthread_join(ThreadA, NULL);
    pthread_join(ThreadB, NULL);
    pthread_join(ThreadC, NULL);
    pthread_join(ThreadD, NULL);

    return 0;
}

测试结果:B和D的值相同,说明在访问共享内存时他们可以同时访问, 不存在互斥现象。
Thread B: 0
Thread D: 0
Thread A counter+2 = 2
Thread B: 2
Thread D: 2

Thread C counter++ = 3
Thread B: 3
Thread D: 3

Thread A counter+2 = 5
Thread B: 5
Thread D: 5
Thread B: 5
Thread D: 5

Thread C counter++ = 6
Thread A counter+2 = 8
Thread B: 8
Thread D: 8
Thread B: 8
Thread D: 8

Thread A counter+2 = 10



3,条件变量

互斥量通过对共享数据的访问来同步任务。条件变量可以根据数据的值来同步任务。条件变量是当一个任务发生时发送信号的信号量。通常用于对操作的顺序进行同步。

执行操作的类型:

  • 初始化
  • 销毁
  • 等待
  • 计时等待
  • 发信号
  • 广播
条件变量操作 函数原型 #include <pthread.h>
初始化 int pthread_cond_init(pthread_cond_t * cond, const pthread_cond_attr* attr)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
发信号 int pthread_cond_signal(pthread_cond_t* cond)
int pthread_cond_broadcast(pthread_cond_t* cond)
销毁 int pthread_cond_destroy(pthread_cond_t * cond)


一个简单的例子:使用条件变量实现管理同步关系

  • Start - to - Start (SS)         :   任务B不能在任务A开始之前开始
  • Finish - to - Start (FS)      :   任务A不能结束,直到任务B启动
  • Start  - to - Finish (FS)     :   任务B不能开始,直到任务A结束
  • Finish - to - Finish (FF)   :   任务A不能结束,直到任务B结束


using namespace std;

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

int Number;
pthread_t ThreadA, ThreadB;
pthread_mutex_t Mutex, EventMutex;
pthread_cond_t Event;

void* worker1(void* X)
{
    for (int i = 1; i < 10; i++)
    {
        sleep(1);
        cout << "worker 1 locking" << endl;
        pthread_mutex_lock(&Mutex);
        Number++;
        pthread_mutex_unlock(&Mutex);
        cout << "worker 1 Number = " << Number << endl;
        if (Number == 7)
            pthread_cond_signal(&Event);
    }

    return 0;
}

void* worker2(void* X)
{
    pthread_mutex_lock(&EventMutex);
    pthread_cond_wait(&Event, &EventMutex);
    pthread_mutex_unlock(&EventMutex);

    for (int i = 1; i < 10; i++)
    {
        sleep(1);
        cout << "worker 2 locking" << endl;
        pthread_mutex_lock(&Mutex);
        Number += 20;
        cout << "worker 2 Number = " << Number << endl;
        pthread_mutex_unlock(&Mutex);
    }

    return 0;
}


int main()
{
    pthread_mutex_init(&Mutex, NULL);
    pthread_mutex_init(&EventMutex, NULL);
    pthread_cond_init(&Event, NULL);

    pthread_create(&ThreadA, NULL, worker1, NULL);
    pthread_create(&ThreadB, NULL, worker2, NULL);

    pthread_join(ThreadA, NULL);
    pthread_join(ThreadB, NULL);

    return 0;
}



worker 1 locking
worker 1 Number = 1
worker 1 locking
worker 1 Number = 2
worker 1 locking
worker 1 Number = 3
worker 1 locking
worker 1 Number = 4
worker 1 locking
worker 1 Number = 5
worker 1 locking
worker 1 Number = 6
worker 1 locking
worker 1 Number = 7  在此时发送信号给worker2 使其开始
worker 1 locking

worker 1 Number = 8
worker 2 locking
worker 2 Number = 28  worker 2 开始
worker 1 locking
worker 1 Number = 29  worker 1 结束
worker 2 locking
worker 2 Number = 49
worker 2 locking
worker 2 Number = 69
worker 2 locking
worker 2 Number = 89
worker 2 locking
worker 2 Number = 109
worker 2 locking
worker 2 Number = 129
worker 2 locking
worker 2 Number = 149
worker 2 locking
worker 2 Number = 169
worker 2 locking
worker 2 Number = 189


你可能感兴趣的:(C++多核高级编程 - 06 并发任务的通信和同步(2) 对并发进行同步)