C++多线程

时间:2024-10-14 07:32:47

线程的基本概念

基本概念:线程,即轻量级进程(LWP:LightWeight Process),是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程不拥有系统资源,近拥有少量运行必须的资源。

线程的基本状态

基本状态:就绪、阻塞和运行三种基本状态。

就绪状态:指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;

运行状态:指线程占有处理机正在运行;

阻塞状态:指线程在等待一个事件(如信号量),逻辑上不可执行。

进程和线程的关系

简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

为什么使用多线程

  1. 避免阻塞 大家知道,单个进程只有一个主线程,当主线程阻塞的时候,整个进程也就阻塞 了,无法再去做其它的一些功能了。
  2. 避免 CPU 空转 应用程序经常会涉及到 RPC,数据库访问,磁盘 IO 等操作,这些操作的速度比 CPU 慢很多,而在等待这些响应时,CPU 却不能去处理新的请求,导致这种单线 程的应用程序性能很差。 cpu > 内存 > 磁盘
  3. 提升效率 一个进程要独立拥有 4GB 的虚拟地址空间,而多个线程可以共享同一地址空间, 线程的切换比进程的切换要快得多。

创建线程

CreateThread
需要添加<windows.h>

HANDLE CreateThread(
 LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD 
 SIZE_T dwStackSize,//initialstacksize 
 LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
 LPVOID lpParameter,//threadargument 
 DWORD dwCreationFlags,//creationoption
 LPDWORD lpThreadId//threadidentifier 
 )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

1.第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入 NULL 表示使用默认设置。

2.第二个参数 dwStackSize 表示线程栈空间大小。传入 0 表示使用默认大小 (1MB)。

3.第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程 可以使用同一个函数地址。

4.第四个参数 lpParameter 是传给线程函数的参数。

5.第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为 0 表 示线程创建之后立即就可以进行调度,如果为 CREATE_SUSPENDED 则表 示线程创建后暂停运行,这样它就无法调度,直到调用 ResumeThread()。

6.第六个参数 lpThreadId 将返回线程的 ID 号,传入 NULL 表示不需要返回 该线程 ID 号

例子:

#include <iostream>
#include <>

DWORD WINAPI LaoWang(void* arg)
{
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(4000);
		puts("老王唱歌");
	}
	return 0;
}

DWORD WINAPI XiaoHong(void* arg)
{
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(3000);
		puts("小红俯卧撑");
	}
	return 0;
}

DWORD WINAPI XiaoMing(void* arg)
{
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(2000);
		puts("小明甩头发");
	}
	return 0;
}

int main() 
{
	HANDLE threadArry[3];
	int laowang = 30, xiaohong = 30, xiaoming = 30;
	threadArry[0] = CreateThread(NULL, 0, LaoWang, (void*)&laowang, 0, NULL);
	threadArry[1] = CreateThread(NULL, 0, XiaoHong, (void*)&xiaohong, 0, NULL);
	threadArry[2] = CreateThread(NULL, 0, XiaoMing, (void*)&xiaoming, 0, NULL);
	WaitForMultipleObjects(3,threadArry,TRUE, INFINITE);
	for (int i = 0; i < 3; i++)
	{
		CloseHandle(threadArry[i]);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

_beginthreadex
需要添加<>库
需要添加<<>库

unsigned long _beginthreadex( 
void *security, // 安全属性, 为 NULL 时表示默认安全性 
unsigned stack_size, // 线程的堆栈大小, 一般默认为 0 
unsigned(_stdcall *start_address)(void *), // 线程函数 
void *argilist, // 线程函数的参数 
unsigned initflag, // 新线程的初始状态,0 表示立即执行,//CREATE_SUSPENDED 表示创建之后挂起 
unsigned *threaddr // 用来接收线程 ID 
);
//返回值 : // 成功返回新线程句柄, 失败返回 0
// __stdcall 表示 1.参数从右向左压入堆栈 2.函数被调用者修改堆栈
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

例子:

#include <iostream>
#include <>
#include <>

unsigned WINAPI LaoWang(void* arg)
{
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(4000);
		puts("老王唱歌");
	}
}

unsigned WINAPI XiaoHong(void* arg)
{
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(3000);
		puts("小红俯卧撑");
	}
	return 0;
}

unsigned WINAPI  XiaoMing(void* arg)
{
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(2000);
		puts("小明甩头发");
	}
	return 0;
}

int main() 
{
	uintptr_t threadArry[3];
	hMutex = CreateMutex(NULL,false,NULL);
	int laowang = 30, xiaohong = 30, xiaoming = 30;
	threadArry[0] = _beginthreadex(NULL,0,LaoWang,(void*)&laowang,0,NULL);
	threadArry[1] = _beginthreadex(NULL, 0, XiaoHong, (void*)&xiaohong, 0, NULL);
	threadArry[2] = _beginthreadex(NULL, 0, XiaoMing, (void*)&xiaoming, 0, NULL);
	WaitForMultipleObjects(3,(HANDLE*)threadArry,TRUE, INFINITE);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

线程同步—互斥对象

互斥对象什么意思

互斥对象是用在多线程里,是为了访问共享变量用的。在多线程中,有时候多个线程可以共用一个共享变量,但是这就有问题了:如果多个线程同时修改一个变量,会使编程值出错。互斥对象就是使线程互斥的访问变量。这样统一时刻只会有一个线程在使用变量。一般使用互斥东西的步骤如下:进入互斥对象(如果有其他线程使用了,就等待)对变量操作释放互斥对象

CreateMutex

HANDLE WINAPI CreateMutexW(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, //指向安全属性 
_In_ BOOL bInitialOwner, //初始化互斥对象的所有者 TRUE 立即拥有互斥体
_In_opt_ LPCWSTR lpName //指向互斥对象名的指针 L“Bingo” 
);
  • 1
  • 2
  • 3
  • 4
  • 5

例子:

#include <iostream>
#include <>
#include <>

HANDLE hMutex;
int num;

DWORD WINAPI LaoWang(void* arg)
{
	WaitForSingleObject(hMutex, INFINITE);
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(4000);
		num += 1;
		puts("老王唱歌");
		ReleaseMutex(hMutex);
	}
	return 0;
}

DWORD WINAPI XiaoHong(void* arg)
{
	WaitForSingleObject(hMutex, INFINITE);
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(3000);
		num += 1;
		puts("小红俯卧撑");
		ReleaseMutex(hMutex);
	}
	return 0;
}

DWORD WINAPI XiaoMing(void* arg)
{
	WaitForSingleObject(hMutex, INFINITE);
	int count = *(int*)arg;
	for (int i = 0; i < count; i++)
	{
		Sleep(2000);
		num += 1;
		puts("小明甩头发");
		ReleaseMutex(hMutex);
	}
	return 0;
}

int main() 
{
	HANDLE threadArry[3];
	hMutex = CreateMutex(NULL,false,NULL);
	int laowang = 30, xiaohong = 30, xiaoming = 30;
	threadArry[0] = CreateThread(NULL, 0, LaoWang, (void*)&laowang, 0, NULL);
	threadArry[1] = CreateThread(NULL, 0, XiaoHong, (void*)&xiaohong, 0, NULL);
	threadArry[2] = CreateThread(NULL, 0, XiaoMing, (void*)&xiaoming, 0, NULL);
	WaitForMultipleObjects(3,threadArry,TRUE, INFINITE);
	for (int i = 0; i < 3; i++)
	{
		CloseHandle(threadArry[i]);
	}
	std::cout << "Num=" << num << std::endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

线程同步之事件对象

事件对象也属于内核对象,它包含以下三个成员
● 使用计数;
● 用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;
● 用于指明该事件处于已通知状态还是未通知状态的布尔值。

事件对象有两种类型
人工重置的事件对象和自动重置的事件对象。这两种事件对象的区别 在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个 自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。

  1. 创建事件对象
    调用 CreateEvent 函数创建或打开一个命名的或匿名的事件对象。
  2. 设置事件对象状态
    调用 SetEvent 函数把指定的事件对象设置为有信号状态。
  3. 重置事件对象状态
    调用 ResetEvent 函数把指定的事件对象设置为无信号状态。
  4. 请求事件对象
    线程通过调用 WaitForSingleObject 函数请求事件对象。

CreateEvent

HANDLE CreateEvent( 
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性 
BOOL bManualReset, // 复位方式 TRUE 必须用 ResetEvent 手动复原 FALSE 自动还原为无信号状态 
BOOL bInitialState, // 初始状态 TRUE 初始状态为有信号状态 FALSE 无信号状态 
LPCTSTR lpName //对象名称 NULL 无名的事件对象 
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

事件对象例子:

#include <iostream>
#include <>
#include <>

char Str[100] = { 0 };
HANDLE hEvent;

unsigned WINAPI HasA(void* arg) 
{
	int count = 0;
	WaitForSingleObject(hEvent, INFINITE);
	for (int i=0;Str[i]!=0;i++)
	{
		if (Str[i] == 'A')
		{
			count++;
		}
	}
	printf("字符串含A字母为%d个", count);
	return 0;
}
unsigned WINAPI HasOther(void* arg) 
{
	int count = 0;
	for (int i = 0; Str[i] != 0; i++)
	{
		if (Str[i] != 'A')
		{
			count++;
		}
	}
	printf("字符串不含A字母为%d个", count - 1);
	SetEvent(hEvent);
	return 0;
}

int main() 
{
	fputs("请输入大写英文字母:", stdout);
	fgets(Str, 100, stdin);
	HANDLE hThread1, hThread2;
	hEvent = CreateEvent(NULL, true, false, L"A");
	hThread1 = (HANDLE)_beginthreadex(NULL, 0, HasA, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, HasOther, NULL, 0, NULL);
	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);
	CloseHandle(hEvent);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

windows 内核对象与句柄

1.内核对象
Windows 中每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核进 行访问,应用程序不能在内存中定位这些数据结构并直接更改其内容。这个内存块是一个数据结 构,其成员维护着与对象相关的信息。少数成员(安全描述符和使用计数)是所有内核对象都有 的,但大多数成员都是不同类型对象特有的。

2. 内核对象的使用计数与生命期
在这里插入图片描述

线程同步之信号量

信号量的组成
①计数器:
该内核对象被使用的次数
②最大资源数量:
标识信号量可以控制的最大资源数量(带符号的 32 位)
③当前资源数量:
标识当前可用资源的数量(带符号的 32 位)。即表示当前 开放资源的个数(注意不是剩下资源的个数),只有开放的资源才能被线程所申 请。但这些开放的资源不一定被线程占用完。比如,当前开放 5 个资源,而只有 3 个线程申请,则还有 2 个资源可被申请,但如果这时总共是 7 个线程要使用信 号量,显然开放的资源 5 个是不够的。这时还可以再开放 2 个,直到达到最大资 源数量。

信号量的规则如下
(1)如果当前资源计数大于 0,那么信号量处于触发状态(有信号状态),表示有 可用资源。
(2)如果当前资源计数等于 0,那么信号量属于未触发状态(无信号状态),表示没有可用资源。
(3)系统绝对不会让当前资源计数变为负数 (4)当前资源计数绝对不会大于最大资源计数

信息量大致使用流程
在这里插入图片描述
信号量与互斥量不同的地方是,它允许多个线程在同一时刻访问同一资源,但是需 要限制在同一时刻访问此资源的最大线程数目。信号量对象对线程的同步方式与前 面几种方法不同,信号允许多个线程同时使用共享资源。

CreateSemaphore
函数功能:创建信号量

HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,// Null 安全属性
  LONG lInitialCount,//初始化时,共有多少个资源是可以用的。 0:未触发状//态(无信号 状态),表示没有可用资源
  LONG lMaximumCount,//能够处理的最大的资源数量
  LPCTSTR lpName//NULL 信号量的名称
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ReleaseSemaphore
函数功能:增加信号量,但不超过最大信号量

`WINAPI ReleaseSemaphore( 
_In_ HANDLE hSemaphore, //信号量的句柄 
_In_ LONG lReleaseCount, //将lReleaseCount值加到信号量的当前资源计数上面 0-> 1 
_Out_opt_ LPLONG lpPreviousCount //当前资源计数的原始值 );`
  • 1
  • 2
  • 3
  • 4

例子:

#include <> 
#include <> 
#include <> 

unsigned WINAPI Read(void* arg);
unsigned WINAPI Accu(void* arg);
static HANDLE semOne;
static HANDLE semTwo;
static int num;

int main(int argc, char* argv[]) {
	HANDLE hThread1, hThread2;
	semOne = CreateSemaphore(NULL, 0, 1, NULL); //semOne 没有可用资源 只能表示0或者1的二进制信号量 无信号 
	semTwo = CreateSemaphore(NULL, 1, 1, NULL); //semTwo 有可用资源,有信号状态 有信号 
	hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);
	CloseHandle(semOne);
	CloseHandle(semTwo);
	system("pause");
	return 0;
}

unsigned WINAPI Read(void* arg) {
	int i; for (i = 0; i < 5; i++) {
		fputs("Input num: ", stdout); // 1 5 11 
		printf("begin read\n"); // 3 6 12 //等待内核对象semTwo的信号,如果有信号,继续执行;如果没有信号,等待 
		WaitForSingleObject(semTwo, INFINITE); printf("beginning read\n"); //4 10 16 
		scanf("%d", &num);
		ReleaseSemaphore(semOne, 1, NULL);
	}
	return 0;
}

unsigned WINAPI Accu(void* arg) {
	int sum = 0, i; for (i = 0; i < 5; i++) {
		printf("begin Accu\n"); //2 9 15 //等待内核对象semOne的信号,如果有信号,继续执行;如果没有信号,等待 
		WaitForSingleObject(semOne, INFINITE);
		printf("beginning Accu\n"); //7 13 
		sum += num; printf("sum = %d \n", sum); // 8 14 
		ReleaseSemaphore(semTwo, 1, NULL);
	}
	printf("Result: %d \n", sum);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

线程同步之关键代码段

关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必 须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。

VOID InitializeCriticalSection(PCRITICAL_SECTION pcs)

功能:初始化临界区

参数:psc指向CRITICAL_SECTION结构体的地址

该函数只有一个指向 CRITICAL_SECTION 结构体的指针。在调用 InitializeCriticalSection 函 数之前,首先需要构造一个 CRITICAL_SCTION 结构体类型的对象,然后将该对象的地址传递给 InitializeCriticalSection 函数

VOID DeleteCriticalSection(PCRITICAL_SECTION pcs)

功能:重置结构体中的成员变量(而不是删除它)。注意:如果删除的话,别的线程还在使用一个关键段,就会产生不可预料的结果。

参数:psc指向CRITICAL_SECTION结构体的地址

当临界区不再需要时,可以调用 DeleteCriticalSection 函数释放该对象,该函数将释放一 个没有被任何线程所拥有的临界区对象的所有资源。

VOID EnterCriticalSection(PCRITICAL_SECTION pcs)

功能:EnterCriticalSection会检查结构中的成员变量,这些变量表示是否有线程正在进行访问。以及哪个线程正在访问。如果没有线程访问,则获取访问权限。如果有线程正在访问,则等待。

注意:这个API内部的实现,其实只是对这个结构体做了一些列的检测,它的价值就在于它可以原子性的执行所有的检测。

参数:psc指向CRITICAL_SECTION结构体的地址

调用 EnterCriticalSection 函数,以获得指定的临界区对象的所有权,该函数等待指定的临界 区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而 导致线程等待。

BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs)

功能:和EnterCriticalSection作用相同,只是这个API不会因为另一个线程正在使用pcs这个结构而进入等待状态,它会根据返回值判断是否可以访问。然后继续执行下面的代码

参数:psc指向CRITICAL_SECTION结构体的地址

VOID LeaveCriticalSection(PCRITICAL_SECTION pcs)

功能:更新成员函数,以表示没有任何线程访问被保护资源,同时检查其他线程有没有因为调用EnterCriticalSection而处于等待状态。如果有,则更新成员函数,将一个等待的线程切回调度状态

参数:psc指向CRITICAL_SECTION结构体的地址

线程使用完临界区所保护的资源之后,需要调用 LeaveCriticalSection 函数,释放指定的临 界区对象的所有权。之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进 入关键代码段,访问保护的资源。

例子:

#include <iostream>
#include <>
#include <>

int tickets = 10000;
CRITICAL_SECTION c_arg;

unsigned WINAPI Thread1(void* arg) 
{
	while (true)
	{
		EnterCriticalSection(&c_arg);
		if (tickets<=0)
		{
			LeaveCriticalSection(&c_arg);
			break;
		}
		else
		{
			tickets--;
			printf("1号窗口买票,还剩%d张\n", tickets);
			LeaveCriticalSection(&c_arg);
		}
	}
	return 0;
}
unsigned WINAPI Thread2(void* arg)
{
	while (true)
	{
		EnterCriticalSection(&c_arg);
		if (tickets <= 0)
		{
			LeaveCriticalSection(&c_arg);
			break;
		}
		else
		{
			tickets--;
			printf("2号窗口买票,还剩%d张\n", tickets);
			LeaveCriticalSection(&c_arg);
		}
	}
	return 0;
}

int main() 
{
	HANDLE hThread1, hThread2;
	InitializeCriticalSection(&c_arg);
	hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, Thread2, NULL, 0, NULL);
	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);
	CloseHandle(hThread1);
	CloseHandle(hThread2);
	DeleteCriticalSection(&c_arg);
	system("pause");
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60