- 在并行程度中,当两个并行的线程,在没有任何约束的情况下,访问一个共享变量或者共享对象的一个域,而且至少要有一个操作是写操作,就可能发生数据竞争错误。
- 原语Compare-and-swap(CAS)是实现无锁数据结构的通用原语。
- 获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。
- volatile变量具有synchronized的可见性特性,但是不具备原子特性。
- 减小竞争发生可能性的有效方式是尽可能缩短把持锁的时间
1 基本概念
计算机进程:
在计算机操作系统中,进程是指当可执行文件运行时,系统所创建的内核对象。
计算机线程:
线程是计算机中最小的执行单元。
同步:
不同进程间的若干程序段,它们的运行必须严格按照规定的某种次序来运行,这种先后次序依赖于要完成的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上,通过其它机制实现访问者对资源的有序访问。
互斥:
散布在不同进程之间的若干程序片段,当某个进程运行其中一个程序片时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才能运行。如果用对资源的访问来定义的话,互斥某一资源只能允许一个访问者对其进行访问,具有唯一性和拍它性。互斥无法限制访问者对资源的访问顺序,即访问是无序的。
2 线程间的同步方法:
用户模式和内核模式:
内核模式是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态和用户态,而用户模式就是不用切换到内核态,只在用户态完成的操作。
用户态模式下的方法有:原子操作(例如一个单一的全局变量),临界区。
内核模式下的方法有:事件、信号量、互斥量。
1 临界区:
通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
2 互斥量:
为协调共同对一个共享资源的单独访问而设计的。
3 信号量:
为控制一个具有有限数量用户资源而设计
4 事件:
用来通知线程有一些事件已经发生,从而启动后继任务。
3 进程间的通信方式:
1 管道及有名管道:
管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系的进程间通信。
2 信号:
信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程某事发生了,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
3 消息队列:
消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列添加新信息,对消息队列具有读权限的进程则可以从消息队列中读信息。
4 共享内存:
可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对象进程中对共享内存中的数据更新情况。这种方法需要依靠某种同步操作,如互斥锁和信号量等。
5 信号量
主要作为进程之间以及同一进程的不同线程之间的同步或互斥手段。
6 套接字
这是一种更为一般的进程间通信方式,它可以用于网络中的不同机器之间的进程间通信,应用非常广泛。
4 线程同步方法代码实例:
1 使用C++标准库的thread,mutex头文件
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void Fun_1(int &iCounter)
{
while (true)
{
std::lock_guard<std::mutex>mtx_locker(mtx);
iCounter++;
if (iCounter < 100)
{
std::cout << "Fun_1()->" << iCounter << std::endl;
}
else
{
break;
}
}
}
void Fun_2(int &iCounter)
{
while (true)
{
std::lock_guard<std::mutex>mtx_locker(mtx);
iCounter++;
if (iCounter < 100)
{
std::cout << "Fun_2()->" << iCounter << std::endl;
}
else
{
break;
}
}
}
int main()
{
int iCounter = 0;
std::thread thread1(Fun_1, std::ref(iCounter));
std::thread thread2(Fun_2, std::ref(iCounter));
thread1.join();
thread2.join();
system("pause");
return 0;
}
以上代码通过构造std::mutex的实例来创建互斥元,标准库提供了std::lock_guard类模板,实现了互斥元的RALL惯用法(资源获取及初始化)。该对象在构造时锁定所给的互斥元,析构时解锁该互斥元,从而保证被锁定的互斥元始终被正确解锁。
2 使用WindowsAPI的临界区对象:
// MultiThread.h
#ifndef _MultiThread_H_
#define _MultiThread_H_
#include <windows.h>
class RAII_CrtcSec
{
private:
CRITICAL_SECTION crtc_sec;
public:
RAII_CrtcSec()
{
::InitializeCriticalSection(&crtc_sec);
}
~RAII_CrtcSec()
{
::DeleteCriticalSection(&crtc_sec);
}
RAII_CrtcSec(const RAII_CrtcSec&) = delete;
RAII_CrtcSec& operator=(const RAII_CrtcSec&) = delete;
void Lock()
{
::EnterCriticalSection(&crtc_sec);
}
void Unlock()
{
::LeaveCriticalSection(&crtc_sec);
}
};
#endif // _MultiThread_H_
// main.cpp
#include <windows.h>
#include <iostream>
#include "MultiThread.h"
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
int iCounter = 0;
RAII_CrtcSec cs;
int main()
{
HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, 0);
HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, 0);
CloseHandle(h1);
CloseHandle(h2);
system("pause");
return 0;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
cs.Lock();
++iCounter;
if (iCounter < 100)
{
std::cout << "Fun_1()->" << iCounter << std::endl;
}
else
{
break;
}
cs.Unlock();
}
return 0;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
cs.Lock();
++iCounter;
if (iCounter < 100)
{
std::cout << "Fun_2()->" << iCounter << std::endl;
}
else
{
break;
}
cs.Unlock();
}
return 0;
}
上面的代码使用了Windows API中的临界区对象来实现线程同步。临界区是指一个访问共享资源的代码段,临界区对象则是指当用户使用某个线程访问共享资源时,必须使代码段独占资源,不允许其它线程访问该资源。在该线程访问资源后,其它线程才能访问该资源。Windows API提供了临界区对象的结构体CRITICAL_SECTION,对该对象的使用可以总结为如下几步:
- InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量。
- EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程进入已经初始话的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会导致该函数的线程也一直等待。如果不想要调用线程等待(非阻塞),则应该使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。
- LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是使调用该函数的线程离开临界区并释放该临界区的所有权,以便让其它线程也获得访问该共享资源的机会。一定要在程序不适用临界区的时候调用该函数,释放临界区所有权,否则程序将一直等待造成程序假死。
- DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),该函数的作用是删除程序中已经初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。
3 使用WIndows API的事件对象
#include <windows.h>
#include <iostream>
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
int iCounter = 0;
HANDLE h_Event;
int main()
{
h_Event = CreateEvent(nullptr, false, true, nullptr);
SetEvent(h_Event);
HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr);
HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr);
CloseHandle(h1);
CloseHandle(h2);
system("pause");
return 0;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_Event, INFINITE);
ResetEvent(h_Event);
if (iCounter < 100)
{
++iCounter;
std::cout << "Fun_1()->" << iCounter << std::endl;
SetEvent(h_Event);
}
else
{
break;
SetEvent(h_Event);
}
}
return 0;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_Event, INFINITE);
ResetEvent(h_Event);
if (iCounter < 100)
{
++iCounter;
std::cout << "Fun_2()->" << iCounter << std::endl;
SetEvent(h_Event);
}
else
{
break;
SetEvent(h_Event);
}
}
return 0;
}
事件对象是一种内核对象,用户在程序中使用内核对象的有无信号状态来实现线程同步。使用事件对象的步骤可包括如下:
- 创建事件对象,函数原型为:
HANDLE WINAPI CreateEvent( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, _In_ BOOL bManualReset, _In_ BOOL bInitialState, _In_opt_ LPCSTR lpName);
如果函数调用成功,则返回新创建的事件对象,否则返回NULL。函数参数的含义如下:
-lpEventAttributes:表示创建的事件对象的安全属性,若设为NULL,则表示该程序使用的是默认的安全属性。
-bManualReset:表示所创建的事件对象是人工重置还是自动重置。若为true则表示使用人工重置,在调用线程获得事件对象所有权后用户显示地调用ResetEvent()将对象设置为无信号状态。
-bInitialState:表示事件对象的初始状态。若为true,则表示该对象初始时有信号状态,则线程可以使用事件对象。
-lpName:表示事件对象的名称,若为NULL,则表示创建的是匿名事件对象。
2. 若事件对象初始状态设置为无信号,则需调用SetEvent(HANDLE hEvent)将其设置为无信号状态。ResetEvent(HANDLE hEvent)则用于将事件对象设置为无信号状态。
3. 线程通过调用WaitForSingleObject()主动请求事件对象,该函数原型如下:
DWORD WINAPI WaitForSingleObject( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds);
该函数将在用户指定的事件对象上等待。如果事件对象处于有信号状态,函数将返回。否则函数将一直等待,知道用户所制定的事件到达。
4 使用Windows API的互斥对象:
#include <windows.h>
#include <iostream>
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
HANDLE h_Mutex;
int iCounter = 0;
int main()
{
h_Mutex = CreateMutex(nullptr, false, nullptr);
HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr);
HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr);
CloseHandle(h1);
CloseHandle(h2);
system("pause");
return 0;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_Mutex, INFINITE);
if (iCounter < 100)
{
++iCounter;
std::cout << "Fun_1()->" << iCounter << std::endl;
ReleaseMutex(h_Mutex);
}
else
{
ReleaseMutex(h_Mutex);
break;
}
}
return 0;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_Mutex, INFINITE);
if (iCounter < 100)
{
++iCounter;
std::cout << "Fun_2()->" << iCounter << std::endl;
ReleaseMutex(h_Mutex);
}
else
{
ReleaseMutex(h_Mutex);
break;
}
}
return 0;
}
互斥对象的使用方法和C++标准库的mutex类似,互斥对象使用完记得释放。