目录

一 线程创建与使用

线程创建函数 CreateThread与_beginthreadex

等待函数 WaitForSingleObject 和 WaitForMultipleObjects

例程代码

二 线程同步

互斥对象

互斥对象使用

CreateMutex函数原型

例程代码

事件对象

事件对象的使用

函数原型

例程代码

关键代码段-(临界区)

关键代码段的使用

例程代码

信号量 Semaphore

信号量详细介绍

函数原型

例程代码

线程同步的四种方式对比

线程死锁


一 线程创建与使用

线程创建函数 CreateThread与_beginthreadex

CreateThread是一种微软在Windows API中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle函数来关闭该线程对象。函数原型如下

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD

SIZE_T dwStackSize,//initialstacksize

LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction

LPVOID lpParameter,//threadargument

DWORD dwCreationFlags,//creationoption

LPDWORD lpThreadId//threadidentifier

)

  • 第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。
  • 第二个参数 dwStackSize 表示线程栈空间大小。传入0表示使用默认大小(1MB)。
  • 第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。
  • 第四个参数 lpParameter 是传给线程函数的参数。
  • 第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。
  • 第六个参数 lpThreadId 将返回线程的ID号,传入NULL表示不需要返回该线程ID号

返回值 : // 成功返回新线程句柄, 失败返回0

_beginthreadex是对CreateThread函数的优化,使用方法基本一致。函数原型如下

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

等待函数 WaitForSingleObject 和 WaitForMultipleObjects

函数功能:等待函数 – 使线程进入等待状态,直到指定的内核对象被触发。函数原型如下

DWORDWINAPIWaitForSingleObject(

HANDLEhHandle,

DWORDdwMilliseconds

);

函数说明:

第一个参数为要等待的内核对象。

第二个参数为最长等待的时间,以毫秒为单位,如传入5000就表示5秒,传入0就立即返回,传入INFINITE表示无限等待。

因为线程的句柄在线程运行时是未触发的,线程结束运行,句柄处于触发状态。所以可以用WaitForSingleObject()来等待一个线程结束运行。

函数返回值:

在指定的时间内对象被触发,函数返回WAIT_OBJECT_0。超过最长等待时间对象仍未被触发返回WAIT_TIMEOUT。传入参数有错误将返回WAIT_FAILED

WaitForMultipleObjects(

_In_ DWORD nCount,    // 要监测的句柄的组的句柄的个数

_In_reads_(nCount) CONST HANDLE* lpHandles,   //要监测的句柄的组

_In_ BOOL bWaitAll,  // TRUE 等待所有的内核对象发出信号, FALSE 任意一个内核对象发出信号

_In_ DWORD dwMilliseconds //等待时间

);

例程代码

#include <stdio.h>
#include <windows.h>
#include <process.h>unsigned int WINAPI ThreadFun(LPVOID p)
{int cnt = *((int*)p);for (int i = 0; i < cnt; i++){Sleep(1000);puts("running thread");}return 0;
}int main()
{printf("main begin\n");int iParam = 5;unsigned int dwThreadID;DWORD wr;HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun,(void*)&iParam, 0, &dwThreadID);if (hThread == NULL){puts("_beginthreadex() error");return -1;}// printf("WaitForSingleObject begin\n");if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED){puts("thread wait error");return -1;}printf("WaitForSingleObject end\n");printf("main end\n");system("pause");return 0;
}

二 线程同步

互斥对象

互斥对象使用

(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。

互斥对象包含一个使用数量,一个线程ID和一个计数器。其中线程ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

创建互斥对象:调用函数CreateMutex。调用成功,该函数返回所创建的互斥对象的句柄。

请求互斥对象所有权:调用函数WaitForSingleObject函数。线程必须主动请求共享对象的所有权才能获得所有权。

释放指定互斥对象的所有权:调用ReleaseMutex函数。线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。

CreateMutex函数原型

HANDLE WINAPI CreateMutexW(

_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,   //指向安全属性

_In_ BOOL bInitialOwner,   //初始化互斥对象的所有者  TRUE 立即拥有互斥体,false表示创建的这个mutex不属于任何线程;所以处于激发状态,也就是有信号状态

_In_opt_ LPCWSTR lpName    //指向互斥对象名的指针  L“Bingo”

);

例程代码

#include <stdio.h>
#include <windows.h>
#include <process.h>#define NUM_THREAD    50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);long long num=0;
HANDLE hMutex;int main(int argc, char *argv[])
{HANDLE tHandles[NUM_THREAD];int i;hMutex=CreateMutex(NULL, FALSE, NULL);for(i=0; i<NUM_THREAD; i++){if(i%2)tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);elsetHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);}WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);CloseHandle(hMutex);printf("result: %lld \n", num);return 0;
}unsigned WINAPI threadInc(void * arg)
{int i;WaitForSingleObject(hMutex, INFINITE);for(i=0; i<500000; i++)num+=1;ReleaseMutex(hMutex);return 0;
}
unsigned WINAPI threadDes(void * arg)
{int i;WaitForSingleObject(hMutex, INFINITE);for(i=0; i<500000; i++)num-=1;ReleaseMutex(hMutex);return 0;
}

事件对象

事件对象的使用

事件对象也属于内核对象,它包含以下三个成员:

●    使用计数;

●    用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;

●   用于指明该事件处于已通知状态还是未通知状态的布尔值。

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

1.      创建事件对象

调用CreateEvent函数创建或打开一个命名的或匿名的事件对象。

2.      设置事件对象状态

调用SetEvent函数把指定的事件对象设置为有信号状态。

3.      重置事件对象状态

调用ResetEvent函数把指定的事件对象设置为无信号状态。

4.      请求事件对象

线程通过调用WaitForSingleObject函数请求事件对象。

函数原型

HANDLE CreateEvent(   

LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性   

BOOL bManualReset,   // 复位方式  TRUE 必须用ResetEvent手动复原  FALSE 自动还原为无信号状态

BOOL bInitialState,   // 初始状态   TRUE 初始状态为有信号状态  FALSE 无信号状态

LPCTSTR lpName     //对象名称  NULL  无名的事件对象 

);

例程代码

/*输入一个全局字串: ABCD  AAADDD
通过多线程的方式来判断有几个字母A,必须用线程同步的方式实现;
事件对象来实现:*/#include <stdio.h>
#include <windows.h>
#include <process.h>
#define STR_LEN     100unsigned WINAPI NumberOfA(void* arg);
unsigned WINAPI NumberOfOthers(void* arg);static char str[STR_LEN];
static HANDLE hEvent;int main(int argc, char* argv[])
{HANDLE  hThread1, hThread2;fputs("Input string: ", stdout);fgets(str, STR_LEN, stdin);//NUll 默认的安全符   手动   FALSE 初始状态为无信号状态hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);WaitForSingleObject(hThread1, INFINITE);WaitForSingleObject(hThread2, INFINITE);//直到2个线程执行完之后,再把事件设置为无信号状态ResetEvent(hEvent);CloseHandle(hEvent);system("pause");return 0;
}unsigned WINAPI NumberOfA(void* arg)
{int i, cnt = 0;//再没有执行fputs("Input string: ", stdout);//fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在//WaitForSingleObjectWaitForSingleObject(hEvent, INFINITE);for (i = 0; str[i] != 0; i++){if (str[i] == 'A')cnt++;}printf("Num of A: %d \n", cnt);return 0;
}
unsigned WINAPI NumberOfOthers(void* arg)
{int i, cnt = 0;//再没有执行fputs("Input string: ", stdout);//fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在//WaitForSingleObject
//  WaitForSingleObject(hEvent, INFINITE);for (i = 0; str[i] != 0; i++){if (str[i] != 'A')cnt++;}printf("Num of others: %d \n", cnt - 1);//把事件对象设置为有信号状态SetEvent(hEvent);return 0;
}

关键代码段-(临界区)

关键代码段的使用

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

初始化关键代码段-调用InitializeCriticalSection函数初始化一个关键代码段。

InitializeCriticalSection(

_Out_ LPCRITICAL_SECTION lpCriticalSection

);

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

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

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

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

例程代码

#include <stdio.h>
#include <windows.h>
#include <process.h> int iTickets = 5000;
CRITICAL_SECTION g_cs;// A窗口     B窗口DWORD WINAPI SellTicketA(void* lpParam)
{while (1){EnterCriticalSection(&g_cs);//进入临界区if (iTickets > 0){Sleep(1);iTickets--;printf("A remain %d\n", iTickets);LeaveCriticalSection(&g_cs);//离开临界区}else{LeaveCriticalSection(&g_cs);//离开临界区break;}}return 0;
}DWORD WINAPI SellTicketB(void* lpParam)
{while (1){EnterCriticalSection(&g_cs);//进入临界区if (iTickets > 0){Sleep(1);iTickets--;printf("B remain %d\n", iTickets);LeaveCriticalSection(&g_cs);//离开临界区}else{LeaveCriticalSection(&g_cs);//离开临界区break;}}return 0;
}int main()
{HANDLE hThreadA, hThreadB;hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL);  //2hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL);  //2CloseHandle(hThreadA); //1CloseHandle(hThreadB); //1InitializeCriticalSection(&g_cs); //初始化关键代码段Sleep(40000);DeleteCriticalSection(&g_cs);//删除临界区system("pause");return 0;
}

信号量 Semaphore

信号量详细介绍

-内核对象的状态:

触发状态(有信号状态),表示有可用资源。

未触发状态(无信号状态),表示没有可用资源

-信号量的组成

  ①计数器:该内核对象被使用的次数

  ②最大资源数量:标识信号量可以控制的最大资源数量(带符号的32位)

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

信号量的规则如下:

(1)如果当前资源计数大于0,那么信号量处于触发状态(有信号状态),表示有可用资源。

(2)如果当前资源计数等于0,那么信号量属于未触发状态(无信号状态),表示没有可用资源。

(3)系统绝对不会让当前资源计数变为负数

(4)当前资源计数绝对不会大于最大资源计数

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

函数原型

创建信号量 CreateSemaphoreW

HANDLE WINAPI CreateSemaphoreW(

_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  // Null 安全属性

   _In_ LONG lInitialCount,  //初始化时,共有多少个资源是可以用的。 0:未触发状//态(无信号状态),表示没有可用资源

_In_ LONG lMaximumCount,  //能够处理的最大的资源数量

_In_opt_ LPCWSTR lpName   //NULL 信号量的名称

);

增加信号量 ReleaseSemaphore

WINAPI ReleaseSemaphore(

_In_ HANDLE hSemaphore,   //信号量的句柄

_In_ LONG lReleaseCount,   //将lReleaseCount值加到信号量的当前资源计数上面

_Out_opt_ LPLONG lpPreviousCount  //当前资源计数的原始值

);

例程代码

#include <stdio.h>
#include <windows.h>
#include <process.h>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   11printf("begin read\n"); // 3   6   12//等待内核对象semTwo的信号,如果有信号,继续执行;如果没有信号,等待WaitForSingleObject(semTwo, INFINITE); printf("beginning read\n"); //4  10   16scanf("%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   13sum += num;printf("sum = %d \n", sum);  // 8   14ReleaseSemaphore(semTwo, 1, NULL);}printf("Result: %d \n", sum);return 0;
}

线程同步的四种方式对比

比较

互斥量

Mutex

事件对象

Event

信号量对象

Semaphore

关键代码段

Criticalsection

是否为内核对象

速度

较慢

较慢

多个进程中的线程同步

支持

支持

支持

不支持

发生死锁

组成

一个线程ID;用来标识哪个线程拥有该互斥量;一个计数器:用来知名该线程用于互斥对象的次数

一个使用计数;一个布尔值:用来标识该事件是自动重置还是人工重置;一个布尔值:标识该事件处于有信号状态还是无信号状态

一个使用计数;

最大资源数;

标识当前可用的资源数

一个小代码段;

在代码能够执行前,必须占用对某些资源的访问权

相关函数

CreateMutex

WaitForSingleObjects

//被保护的内容

ReleaseMutex

CreateEvent

ResetEvent

WaitforSingleobject

//保护内容

SetEvent

CreateSemaphore

WaitForsingleobject

//被保护的内容

ReleaseSemaPhore

InitialCriticalSection

EnterCritionSection

//被保护的内容

LeaveCritialSection

DeleteCritialSection

注意事项

谁拥有互斥对象,谁释放;

如果多次在同一个线程中请求同一个互斥对象,需要多次调用releaseMutex

为了实现线程间的同步,不应该使用人工重置,应该把第二个参数设置false;设置为自动重置

它允许多个线程在同一时间访问同一个资源;但是需要限制访问此资源的最大线程数目;

防止死锁:使用多个关键代码段变量的时候

类比

一把钥匙

钥匙(自动/人工)

停车场和保安

电话亭

线程死锁

死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值

代码举例

#include <stdio.h>
#include <windows.h>
#include <process.h> int iTickets = 5000;
CRITICAL_SECTION g_csA;
CRITICAL_SECTION g_csB;// A窗口     B窗口DWORD WINAPI SellTicketA(void* lpParam)
{while (1){EnterCriticalSection(&g_csA);//进入临界区ASleep(1);EnterCriticalSection(&g_csB);//进入临界区Bif (iTickets > 0){Sleep(1);iTickets--;printf("A remain %d\n", iTickets);LeaveCriticalSection(&g_csB);//离开临界区BLeaveCriticalSection(&g_csA);//离开临界区A}else{LeaveCriticalSection(&g_csB);//离开临界区BLeaveCriticalSection(&g_csA);//离开临界区Abreak;}}return 0;
}DWORD WINAPI SellTicketB(void* lpParam)
{while (1){EnterCriticalSection(&g_csB);//进入临界区BSleep(1);EnterCriticalSection(&g_csA);//进入临界区Aif (iTickets > 0){Sleep(1);iTickets--;printf("B remain %d\n", iTickets);LeaveCriticalSection(&g_csA);//离开临界区ALeaveCriticalSection(&g_csB);//离开临界区B}else{LeaveCriticalSection(&g_csA);//离开临界区ALeaveCriticalSection(&g_csB);//离开临界区Bbreak;}}return 0;
}int main()
{HANDLE hThreadA, hThreadB;hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL);  //2hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL);  //2CloseHandle(hThreadA); //1CloseHandle(hThreadB); //1InitializeCriticalSection(&g_csA); //初始化关键代码段AInitializeCriticalSection(&g_csB); //初始化关键代码段BSleep(40000);DeleteCriticalSection(&g_csA);//删除临界区DeleteCriticalSection(&g_csB);//删除临界区system("pause");return 0;
}

Window下线程与线程同步总结相关推荐

  1. Ubuntu/Window下X2Go安装连接同步/上传文件夹(一次性成功)

    Ubuntu/Window下X2Go安装&连接&同步/上传文件夹(一次性成功) 一 Window下载安装X2Go Client 二 在ubuntu上安装X2Go 三 Window用X2 ...

  2. window下线程同步之(Event Objects(事件)) / 事件CEvent的使用

    篇一:http://www.cnblogs.com/staring-hxs/p/3666655.html Event 方式是最具弹性的同步机制,因为他的状态完全由你去决定,不会像 Mutex 和 Se ...

  3. Java 编程下的并发线程之间的同步代码块死锁

    并发线程之间的死锁通常发生在同步代码块的相互嵌套使用的过程中,下面先来看段代码: package cn.sunzn.synchronize;public class DeadLock {public ...

  4. 多线程下的进程同步(线程同步问题总结篇)

    之前写过两篇关于线程同步问题的文章(一,二),这篇中将对相关话题进行总结,本文中也对.NET 4.0中新增的一些同步机制进行了介绍. 首先需要说明的是为什么需要线程功能同步.MSDN中有这样一段话很好 ...

  5. 线程创建_同步_通信

    目录 1. 基本概念:程序.进程.线程 2. 并行和并发 3. 创建线程的两种方式 3.1 通过继承Thread方式 3.1.1 步骤: 3.1.2 两个问题 3.1.3 案例演示 3.2 通过实现R ...

  6. C#中的线程(二) 线程同步基础

    1.同步要领 下面的表格列展了.NET对协调或同步线程动作的可用的工具:                       简易阻止方法 构成 目的 Sleep 阻止给定的时间周期 Join 等待另一个线程 ...

  7. java同步锁售票_Java基础学习笔记: 多线程,线程池,同步锁(Lock,synchronized )(Thread类,ExecutorService ,Future类)(卖火车票案例)...

    学习多线程之前,我们先要了解几个关于多线程有关的概念. 进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 线程:线程是 ...

  8. 单例设计模式八种方式——5) 懒汉式(线程安全,同步代码块) 6) 双重检查 7) 静态内部类 8) 枚举

    懒汉式(线程安全,同步代码块)应用实例 优缺点说明: 1) 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低, 改为同步产生实例化的的代码块 2) 但是这种同步并不能起到线程同步的作 ...

  9. linux 线程管理、同步机制等

    线程 学了那么多有关进程的东西,一个作业从一个进程开始,如果你需要执行其他的东西你可以添加一些进程,进程之间可以通信.同步.异步.似乎所有的事情都可以做了. 对的,进程是当初面向执行任务而开发出来的, ...

最新文章

  1. cxf使用wsdl文件生成代码
  2. MFC指定位置画位图
  3. 根据省市区查询对应权限下的数据
  4. 设计模式之工厂模式(三)
  5. java 新区 老区_优学院《土地资源学》答案在线查题2020高校邦《Java核心开发技术【实境编程】》章测试答案...
  6. Apache并发处理模块
  7. 名图1.8智能隐藏功能_自动打包不脏手才是真智能,双11销冠,拓牛自动打包垃圾桶体验...
  8. Spring Boot笔记-目前对Web后端开发的认识
  9. 《Storm入门》中文版
  10. 注册表在各个系统中保存路径
  11. 文本处理工具--正则表达式
  12. Chrome上网问题解决记录
  13. debian网络配置文件的写法
  14. BG2RHE - Arduino控制TM1640芯片LED流水灯测试
  15. C# 读取TXT文本文档 搜索指定字符串所在的行 保存到集合
  16. 腾讯云window阿帕奇服务器开通ssl证书实现https访问
  17. bat脚本从FTP下载文件的方式(下载实践的完整实例):
  18. android网易云信使用方法,网易云信Android版demo修改为自己所用
  19. ultravnc中文版,5步教你如何进行ultravnc中文版配置
  20. 千锋python培训多久

热门文章

  1. 神经开放域信息抽取OpenIE
  2. 基于SVM的乳腺癌数据集分类
  3. 指数分布与幂律分布定义及不同(泊松分布、伽马分布)
  4. 关于wince 下LCD 控制器设置成RGB888 24bit 的相关问题
  5. php根据目录 创建文件,php创建文件目录,及删除目录和文件
  6. Android后台实时定位
  7. 用jQuery写简单的计时器
  8. 儿时的动画,你看过那些?-起步时期
  9. 中投民生:又一药企闯关成功,其业绩因核酸检测产品狂飙
  10. 天合光能怎么样?天合跟踪亮相阿布扎比世界未来能源峰会