继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

关于.net线程

qq_相守许下了今生_0
关注TA
已关注
手记 1
粉丝 0
获赞 31

a:link{/正常的未被访问过的标签;/
text-decoration:none;
font-family:#000000;
color:red;
}
a:hover{
/指鼠标在连接/
text-decoration:underline;
font-size:50px;
color:blue;
}
a:active{
/正在点的链接/

    }

a:visited {} / 已访问的链接 /
a:hover {} / 当有鼠标悬停在链接上 /
a:active {} / 被选择的链接 /
a:link {} / 未访问的链接 /

***线程的简单使用
需要引用System.Threading命名空间
class Program {
static void Main(string[] args) {

    // 使用无参数委托ThreadStart
    Thread t = new Thread(Go);
    t.Start();

    // 使用带参数委托ParameterizedThreadStart
    Thread t2 = new Thread(GoWithParam);
    t2.Start("Message from main.");

    t2.Join();// 等待线程t2完成。

    Console.WriteLine("Thread t2 has ended!");
    Console.ReadKey();
}

static void Go() {
    Console.WriteLine("Go!");
}

static void GoWithParam(object msg) {
    Console.WriteLine("Go With Param! Message: " + msg);
    Thread.Sleep(1000);// 模拟耗时操作
}

}

***并发和异步的区别
class Program {
static void Main(string[] args) {

    Thread t1 = new Thread(Working);
    t1.Name = "Thread1";
    Thread t2 = new Thread(Working);
    t2.Name = "Thread2";
    Thread t3 = new Thread(Working);
    t3.Name = "Thread3";

    // 依次启动3个线程。
    t1.Start();
    t2.Start();
    t3.Start();

    Console.ReadKey();
}

// 每个线程都同时在工作
static void Working() {
    // 模拟1000次写日志操作
    for (int i = 0; i < 1000; i++) {
        //  异步写文件
        Logger.Write(Thread.CurrentThread.Name + " writes a log: " + i + ", on " + DateTime.Now.ToString() + ".\n");
    }// 做一些其它的事件
    for (int i = 0; i < 1000; i++) { }
}

}

***并发控制 - 锁

done用来标识某件事已经做过了(告诉其它线程不要再重复做了)
class Program {
static bool done;
static void Main(string[] args) {

    new Thread(Go).Start(); // 在新的线程上调用Go
    Go(); // 在主线程上调用Go

    Console.ReadKey();
}

输出了两个Done;
static void Go() {
if (!done) {
Thread.Sleep(500); // 模拟耗时操作
Console.WriteLine("Done");
done = true;
}
}
}

 lock语句,可以保证共享数据只能同时被一个线程访问

class Program {

static bool done;
static object locker = new object(); // !!

static void Main(string[] args) {

    new Thread(Go).Start(); // 在新的线程上调用Go
    Go(); // 在主线程上调用Go

    Console.ReadKey();
}

static void Go() {
    lock (locker) {
        if (!done) {
            Thread.Sleep(500); // Doing something.
            Console.WriteLine("Done");
            done = true;
        }
    }
}

}
*线程的信号机制

有时候你需要一个线程在接收到某个信号时,才开始执行,
否则处于等待状态,这是一种基于信号的事件机制。
.NET框架提供一个ManualResetEvent类来处理这类事件,
它的 WaiOne 实例方法可使当前线程一直处于等待状态,
直到接收到某个信号。它的Set方法用于打开发送信号。
下面是一个信号机制的使用示例:

static void Main(string[] args) {

var signal = new ManualResetEvent(false);

new Thread(() => {
    Console.WriteLine("Waiting for signal...");
    signal.WaitOne();
    signal.Dispose();
    Console.WriteLine("Got signal!");
}).Start();
Thread.Sleep(2000);

signal.Set();// 打开“信号”

Console.ReadKey();

}
当执行Set方法后,信号保持打开状态,可通过Reset方法将其关闭,
若不再需要,通过Dispose将其释放。如果预期的等待时间很短,
可以用ManualResetEventSlim代替ManualResetEvent,
前者在等待时间较短时性能更好。
信号机制非常有用,后面的日志案例会用到它。


*线程池中的线程

线程池中的线程是由CLR来管理的。在下面两种条件下,线程池能起到最好的效用:

任务运行的时候比较短(<250ms),这样CLR可以充分调配现有的空闲线程来处理该任务;
大量时间处于等待(或阻塞)的任务不去支配线程池的线程。
要使用线程中的线程,主要有下面两种方式:

/ 方式1:Task.Run,.NET Framework 4.5 才有
Task.Run (() => Console.WriteLine ("Hello from the thread pool"));

// 方式2:ThreadPool.QueueUserWorkItem
ThreadPool.QueueUserWorkItem (t => Console.WriteLine ("Hello from the thread pool"));

线程池使得线程可以充分有效地被使用,减少了任务启动的延迟。
但是不是所有的情况都适合使用线程池中的线程,比如下面要讲的日志案例

  • 异步写文件。这里讲线程池,是为了让大家大致了解什么时候用线程池
    中的线程,什么时候不用。即,耗时长或有阻塞情况的不用线程池中的线程。

创建不走线程池中的线程,可以直接通过new Thread来创建,
也可以通过下面的代码来创建:

Task task = Task.Factory.StartNew (() => ...,TaskCreationOptions.LongRunning);
// 注意必须带TaskCreationOptions.LongRunning参数


案例:支持并发的异步日志组件

上文的“并发和异步的区别”的代码中我们用到了一个Logger类,现在我们就来
做一个这样的Logger。基于上面的知识,我们可以实现应用程序的并发写日
志日志功能。在应用程序中,写日志是常见的功能,简单分析一下该功能的需求:
在后台异步执行,和其它线程互不影响。
根据上文线程池的两个最优使用条件,由写日志线程会长时间处于阻塞
(或运行等待)状态,所以它不适合使用线程池。
即不能使用Task.Run,而最好使用new Thread。
支持并发,即多个任务(分布在不同线程上)
可同时调用写日志功能,但需保证线程安全。
支持并发,必然要用到锁,但要完全保证线程安全,那就要想办法避免“死锁”。
只要我们把“上锁”的操作始终由同一个线程来做即可避免“死锁”问题,但这样的话,
并发请求的任务只能放在队列中由该线程依次执行(因为是后台执行,无需即时响应用户,所以可以这么做)。
单个实例,单个线程。
任何地方调用写日志功能都调用的是同一个Logger实例(显然不能每次写日志都新建一个实例),
即需使用单例模式。不管有多少任务调用写日志功能,都必须始终使用同一个线程来处
理这些写日志操作,以保证不占用过多的线程资源和避免新建线程带来的延迟。
运用上面的知识,我们来写一个这样的类。简单理一下思路:

1.需要一个用来存放写日志任务的队列。
2.需要有一个信号机制来标识是否有新的任务要执行。
3.当有新的写日志任务时,将该任务加入到队列中,并发出信号。
4.用一个方法来处理队列中的任务,当接收新任务信号时,就依次调用队列中的任务。
开发一个功能前需要有个简单的思路,保证心里面有底。
具体开发的时候会发现问题,然后再去补充扩展和完善等。
刚开始很难想得太周全,先有个简单的思路,然后代码写起来!

public class Logger {

// 用于存放写日志任务的队列
private Queue<Action> _queue;

// 用于写日志的线程
private Thread _loggingThread;

// 用于通知是否有新日志要写的“信号器”
private ManualResetEvent _hasNew;

// 构造函数,初始化。
private Logger() {
    _queue = new Queue<Action>();
    _hasNew = new ManualResetEvent(false);

    _loggingThread = new Thread(Process);
    _loggingThread.IsBackground = true;
    _loggingThread.Start();
}

// 使用单例模式,保持一个Logger对象
private static readonly Logger _logger = new Logger();
private static Logger GetInstance() {
    /* 不安全代码
    lock (locker) {
        if (_logger == null) {
            _logger = new Logger();
        }
    }*/
    return _logger;
}

// 处理队列中的任务
private void Process() {
    while (true) {
        // 等待接收信号,阻塞线程。
        _hasNew.WaitOne();

        // 接收到信号后,重置“信号器”,信号关闭。
        _hasNew.Reset(); 

        // 由于队列中的任务可能在极速地增加,这里等待是为了一次能处理更多的任务,减少对队列的频繁“进出”操作。
        Thread.Sleep(100);

        // 开始执行队列中的任务。
        // 由于执行过程中还可能会有新的任务,所以不能直接对原来的 _queue 进行操作,
        // 先将_queue中的任务复制一份后将其清空,然后对这份拷贝进行操作。

        Queue<Action> queueCopy;
        lock (_queue) {
            queueCopy = new Queue<Action>(_queue);
            _queue.Clear();
        }

        foreach (var action in queueCopy) {
            action();
        }
    }
}

private void WriteLog(string content) {
    lock (_queue) { // todo: 这里存在线程安全问题,可能会发生阻塞。
        // 将任务加到队列
        _queue.Enqueue(() => File.AppendAllText("log.txt", content));
    }

    // 打开“信号”
    _hasNew.Set();
}

// 公开一个Write方法供外部调用
public static void Write(string content) {
    // WriteLog 方法只是向队列中添加任务,执行时间极短,所以使用Task.Run。
    Task.Run(() => GetInstance().WriteLog(content));
}

}

打开App,阅读手记
4人推荐
发表评论
随时随地看视频慕课网APP